Full Code of Pong420/google-tasks-desktop for AI

master 100dedb61e77 cached
185 files
266.3 KB
75.4k tokens
258 symbols
1 requests
Download .txt
Showing preview only (307K chars total). Download the full file or copy to clipboard to get everything.
Repository: Pong420/google-tasks-desktop
Branch: master
Commit: 100dedb61e77
Files: 185
Total size: 266.3 KB

Directory structure:
gitextract_58hd8j73/

├── .eslintcache
├── .eslintignore
├── .gitignore
├── .prettierrc
├── LICENSE
├── Procfile
├── README.md
├── common.d.ts
├── config-overrides.js
├── electron/
│   ├── electron.d.ts
│   ├── main.ts
│   ├── menu.ts
│   ├── preload/
│   │   ├── index.ts
│   │   └── theme.ts
│   ├── storage.ts
│   └── tsconfig.json
├── mock-fs.js
├── package.json
├── public/
│   ├── icon/
│   │   └── icon.icns
│   ├── index.html
│   └── manifest.json
├── scripts/
│   ├── component.js
│   ├── electron-wait-react.js
│   ├── redux.js
│   ├── template/
│   │   ├── store/
│   │   │   ├── actions.tmpl
│   │   │   ├── epics.tmpl
│   │   │   ├── reducers.tmpl
│   │   │   └── store.tmpl
│   │   └── useActions.tmpl
│   └── type.js
├── src/
│   ├── App.tsx
│   ├── components/
│   │   ├── AppRegion/
│   │   │   ├── AppRegion.scss
│   │   │   ├── AppRegion.tsx
│   │   │   ├── WindowsTitleBar.tsx
│   │   │   └── index.ts
│   │   ├── KeyboardShortcuts/
│   │   │   ├── KeyboardShortcuts.scss
│   │   │   ├── KeyboardShortcuts.tsx
│   │   │   ├── index.ts
│   │   │   └── shortcuts.json
│   │   ├── Mui/
│   │   │   ├── DeleteIcon.tsx
│   │   │   ├── Dialog/
│   │   │   │   ├── ConfirmDialog.tsx
│   │   │   │   ├── Dialog.scss
│   │   │   │   ├── FormDialog.tsx
│   │   │   │   ├── FullScreenDialog.tsx
│   │   │   │   └── index.ts
│   │   │   ├── Dropdown/
│   │   │   │   ├── Dropdown.scss
│   │   │   │   ├── Dropdown.tsx
│   │   │   │   └── index.ts
│   │   │   ├── EditIcon.tsx
│   │   │   ├── IconButton/
│   │   │   │   ├── IconButton.scss
│   │   │   │   ├── IconButton.tsx
│   │   │   │   └── index.ts
│   │   │   ├── Input/
│   │   │   │   ├── Input.scss
│   │   │   │   ├── Input.tsx
│   │   │   │   └── index.ts
│   │   │   ├── Menu/
│   │   │   │   ├── Menu.scss
│   │   │   │   ├── Menu.tsx
│   │   │   │   ├── MenuItem.tsx
│   │   │   │   ├── index.ts
│   │   │   │   └── useMuiMenu.ts
│   │   │   ├── Tooltip.tsx
│   │   │   └── index.ts
│   │   ├── Preferences/
│   │   │   ├── AccentColor.tsx
│   │   │   ├── Preferences.scss
│   │   │   ├── Preferences.tsx
│   │   │   ├── Storage.tsx
│   │   │   ├── ThemeSelector.tsx
│   │   │   ├── TitleBarSelector.tsx
│   │   │   └── index.ts
│   │   ├── PrivateRoute.tsx
│   │   └── Switch/
│   │       ├── Switch.scss
│   │       ├── Switch.tsx
│   │       └── index.ts
│   ├── constants/
│   │   ├── index.ts
│   │   └── paths.json
│   ├── date.d.ts
│   ├── hooks/
│   │   ├── crud-reducer/
│   │   │   ├── bindDispatch.ts
│   │   │   ├── crudAction.ts
│   │   │   ├── crudReducer.ts
│   │   │   ├── crudSelector.ts
│   │   │   ├── index.ts
│   │   │   ├── useActions.ts
│   │   │   └── useCRUDReducer.ts
│   │   ├── useActions.ts
│   │   ├── useBoolean.ts
│   │   └── useMouseTrap.ts
│   ├── index.scss
│   ├── index.tsx
│   ├── pages/
│   │   ├── Auth/
│   │   │   ├── Auth.scss
│   │   │   ├── Auth.tsx
│   │   │   ├── FileUpload.tsx
│   │   │   └── index.ts
│   │   └── TaskList/
│   │       ├── CompletedTaskList/
│   │       │   ├── CompletedTaskList.scss
│   │       │   ├── CompletedTaskList.tsx
│   │       │   └── index.ts
│   │       ├── NewTask/
│   │       │   ├── NewTask.scss
│   │       │   ├── NewTask.tsx
│   │       │   └── index.ts
│   │       ├── Task/
│   │       │   ├── CompletedTask.tsx
│   │       │   ├── DatePicker/
│   │       │   │   ├── DatePicker.scss
│   │       │   │   ├── DatePicker.tsx
│   │       │   │   └── index.ts
│   │       │   ├── DateTimeDialog/
│   │       │   │   ├── DateTimeDialog.scss
│   │       │   │   ├── DateTimeDialog.tsx
│   │       │   │   └── index.ts
│   │       │   ├── Task.scss
│   │       │   ├── Task.tsx
│   │       │   ├── TaskInput.tsx
│   │       │   ├── TodoTask/
│   │       │   │   ├── TodoTask.scss
│   │       │   │   ├── TodoTask.tsx
│   │       │   │   ├── TodoTaskMenu.tsx
│   │       │   │   └── index.ts
│   │       │   ├── TodoTaskDetails/
│   │       │   │   ├── DateTimeButton.tsx
│   │       │   │   ├── TodoTaskDetails.scss
│   │       │   │   ├── TodoTaskDetails.tsx
│   │       │   │   └── index.ts
│   │       │   ├── ToggleCompleted.tsx
│   │       │   └── index.ts
│   │       ├── TaskList.scss
│   │       ├── TaskList.tsx
│   │       ├── TaskListDropdown/
│   │       │   ├── TaskListDropdown.scss
│   │       │   ├── TaskListDropdown.tsx
│   │       │   ├── TaskListDropdownItem.tsx
│   │       │   └── index.ts
│   │       ├── TaskListHeader/
│   │       │   ├── TaskListHeader.scss
│   │       │   ├── TaskListHeader.tsx
│   │       │   └── index.ts
│   │       ├── TaskListMenu.tsx
│   │       ├── TodoTaskList/
│   │       │   ├── TodoTaskList.scss
│   │       │   ├── TodoTaskList.tsx
│   │       │   ├── TodoTaskListByDate.tsx
│   │       │   └── index.ts
│   │       └── index.ts
│   ├── react-app-env.d.ts
│   ├── scss/
│   │   ├── _functions.scss
│   │   ├── _mixins.scss
│   │   ├── _platform.scss
│   │   ├── _theme.scss
│   │   ├── _variables.scss
│   │   ├── index.scss
│   │   └── mixins/
│   │       ├── _animation.scss
│   │       ├── _background.scss
│   │       ├── _border.scss
│   │       ├── _electron.scss
│   │       ├── _flex.scss
│   │       ├── _font.scss
│   │       ├── _position.scss
│   │       ├── _size.scss
│   │       ├── _textHighlight.scss
│   │       └── _textOverflow.scss
│   ├── service/
│   │   ├── auth.ts
│   │   ├── index.ts
│   │   ├── task.ts
│   │   └── tasksList.ts
│   ├── serviceWorker.ts
│   ├── store/
│   │   ├── actions/
│   │   │   ├── auth.ts
│   │   │   ├── index.ts
│   │   │   ├── preferences.ts
│   │   │   ├── task.ts
│   │   │   └── taskList.ts
│   │   ├── epics/
│   │   │   ├── auth.ts
│   │   │   ├── index.ts
│   │   │   ├── preferences.ts
│   │   │   ├── task.ts
│   │   │   └── taskList.ts
│   │   ├── index.ts
│   │   ├── reducers/
│   │   │   ├── auth.ts
│   │   │   ├── index.ts
│   │   │   ├── preferences.ts
│   │   │   ├── task.ts
│   │   │   └── taskList.ts
│   │   └── selectors/
│   │       ├── index.ts
│   │       ├── preferences.ts
│   │       ├── task.ts
│   │       └── taskList.ts
│   ├── theme.ts
│   ├── typings/
│   │   └── index.ts
│   └── utils/
│       ├── date.ts
│       ├── form/
│       │   ├── form.ts
│       │   ├── index.ts
│       │   ├── typings.ts
│       │   └── validators.ts
│       ├── nprogress.ts
│       └── uuid.ts
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintcache
================================================
[{"/Users/Pong/Desktop/google-tasks-desktop/src/index.tsx":"1","/Users/Pong/Desktop/google-tasks-desktop/src/theme.ts":"2","/Users/Pong/Desktop/google-tasks-desktop/src/serviceWorker.ts":"3","/Users/Pong/Desktop/google-tasks-desktop/src/utils/date.ts":"4","/Users/Pong/Desktop/google-tasks-desktop/src/App.tsx":"5","/Users/Pong/Desktop/google-tasks-desktop/src/store/index.ts":"6","/Users/Pong/Desktop/google-tasks-desktop/src/components/PrivateRoute.tsx":"7","/Users/Pong/Desktop/google-tasks-desktop/src/components/AppRegion/index.ts":"8","/Users/Pong/Desktop/google-tasks-desktop/src/constants/index.ts":"9","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/index.ts":"10","/Users/Pong/Desktop/google-tasks-desktop/src/pages/Auth/index.ts":"11","/Users/Pong/Desktop/google-tasks-desktop/src/store/epics/index.ts":"12","/Users/Pong/Desktop/google-tasks-desktop/src/store/reducers/index.ts":"13","/Users/Pong/Desktop/google-tasks-desktop/src/store/selectors/index.ts":"14","/Users/Pong/Desktop/google-tasks-desktop/src/store/actions/index.ts":"15","/Users/Pong/Desktop/google-tasks-desktop/src/components/AppRegion/AppRegion.tsx":"16","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TaskList.tsx":"17","/Users/Pong/Desktop/google-tasks-desktop/src/pages/Auth/Auth.tsx":"18","/Users/Pong/Desktop/google-tasks-desktop/src/store/selectors/task.ts":"19","/Users/Pong/Desktop/google-tasks-desktop/src/store/actions/auth.ts":"20","/Users/Pong/Desktop/google-tasks-desktop/src/store/actions/taskList.ts":"21","/Users/Pong/Desktop/google-tasks-desktop/src/store/actions/preferences.ts":"22","/Users/Pong/Desktop/google-tasks-desktop/src/store/actions/task.ts":"23","/Users/Pong/Desktop/google-tasks-desktop/src/store/selectors/preferences.ts":"24","/Users/Pong/Desktop/google-tasks-desktop/src/store/selectors/taskList.ts":"25","/Users/Pong/Desktop/google-tasks-desktop/src/store/reducers/preferences.ts":"26","/Users/Pong/Desktop/google-tasks-desktop/src/store/reducers/auth.ts":"27","/Users/Pong/Desktop/google-tasks-desktop/src/store/reducers/taskList.ts":"28","/Users/Pong/Desktop/google-tasks-desktop/src/store/reducers/task.ts":"29","/Users/Pong/Desktop/google-tasks-desktop/src/store/epics/preferences.ts":"30","/Users/Pong/Desktop/google-tasks-desktop/src/store/epics/taskList.ts":"31","/Users/Pong/Desktop/google-tasks-desktop/src/store/epics/task.ts":"32","/Users/Pong/Desktop/google-tasks-desktop/src/store/epics/auth.ts":"33","/Users/Pong/Desktop/google-tasks-desktop/src/components/AppRegion/WindowsTitleBar.tsx":"34","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/TodoTask/TodoTaskMenu.tsx":"35","/Users/Pong/Desktop/google-tasks-desktop/src/utils/nprogress.ts":"36","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/CompletedTaskList/index.ts":"37","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TodoTaskList/index.ts":"38","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/index.ts":"39","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/NewTask/index.ts":"40","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TaskListHeader/index.ts":"41","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/TodoTaskDetails/index.ts":"42","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/DateTimeDialog/index.ts":"43","/Users/Pong/Desktop/google-tasks-desktop/src/hooks/crud-reducer/index.ts":"44","/Users/Pong/Desktop/google-tasks-desktop/src/pages/Auth/FileUpload.tsx":"45","/Users/Pong/Desktop/google-tasks-desktop/src/service/index.ts":"46","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/DeleteIcon.tsx":"47","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/DateTimeDialog/DateTimeDialog.tsx":"48","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/TodoTaskDetails/TodoTaskDetails.tsx":"49","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Tooltip.tsx":"50","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TaskListHeader/TaskListHeader.tsx":"51","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/NewTask/NewTask.tsx":"52","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/EditIcon.tsx":"53","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TodoTaskList/TodoTaskList.tsx":"54","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/CompletedTaskList/CompletedTaskList.tsx":"55","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Input/index.ts":"56","/Users/Pong/Desktop/google-tasks-desktop/src/hooks/crud-reducer/bindDispatch.ts":"57","/Users/Pong/Desktop/google-tasks-desktop/src/hooks/useBoolean.ts":"58","/Users/Pong/Desktop/google-tasks-desktop/src/service/auth.ts":"59","/Users/Pong/Desktop/google-tasks-desktop/src/service/tasksList.ts":"60","/Users/Pong/Desktop/google-tasks-desktop/src/service/task.ts":"61","/Users/Pong/Desktop/google-tasks-desktop/src/hooks/crud-reducer/useCRUDReducer.ts":"62","/Users/Pong/Desktop/google-tasks-desktop/src/hooks/crud-reducer/useActions.ts":"63","/Users/Pong/Desktop/google-tasks-desktop/src/hooks/crud-reducer/crudSelector.ts":"64","/Users/Pong/Desktop/google-tasks-desktop/src/hooks/crud-reducer/crudReducer.ts":"65","/Users/Pong/Desktop/google-tasks-desktop/src/hooks/crud-reducer/crudAction.ts":"66","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Menu/index.ts":"67","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/IconButton/index.ts":"68","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Dropdown/index.ts":"69","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Dialog/index.ts":"70","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TaskListMenu.tsx":"71","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/TodoTaskDetails/DateTimeButton.tsx":"72","/Users/Pong/Desktop/google-tasks-desktop/src/utils/uuid.ts":"73","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Input/Input.tsx":"74","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TodoTaskList/TodoTaskListByDate.tsx":"75","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Menu/useMuiMenu.ts":"76","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/index.ts":"77","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TaskListDropdown/index.ts":"78","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Menu/MenuItem.tsx":"79","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Dialog/FormDialog.tsx":"80","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Dialog/FullScreenDialog.tsx":"81","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Dialog/ConfirmDialog.tsx":"82","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Dropdown/Dropdown.tsx":"83","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/IconButton/IconButton.tsx":"84","/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Menu/Menu.tsx":"85","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/DatePicker/index.ts":"86","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/CompletedTask.tsx":"87","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/Task.tsx":"88","/Users/Pong/Desktop/google-tasks-desktop/src/components/KeyboardShortcuts/index.ts":"89","/Users/Pong/Desktop/google-tasks-desktop/src/components/Preferences/index.ts":"90","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TaskListDropdown/TaskListDropdown.tsx":"91","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/DatePicker/DatePicker.tsx":"92","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/TodoTask/index.ts":"93","/Users/Pong/Desktop/google-tasks-desktop/src/components/KeyboardShortcuts/KeyboardShortcuts.tsx":"94","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/TaskInput.tsx":"95","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/ToggleCompleted.tsx":"96","/Users/Pong/Desktop/google-tasks-desktop/src/components/Preferences/Preferences.tsx":"97","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TaskListDropdown/TaskListDropdownItem.tsx":"98","/Users/Pong/Desktop/google-tasks-desktop/src/components/Preferences/Storage.tsx":"99","/Users/Pong/Desktop/google-tasks-desktop/src/components/Preferences/TitleBarSelector.tsx":"100","/Users/Pong/Desktop/google-tasks-desktop/src/components/Preferences/AccentColor.tsx":"101","/Users/Pong/Desktop/google-tasks-desktop/src/components/Preferences/ThemeSelector.tsx":"102","/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/TodoTask/TodoTask.tsx":"103","/Users/Pong/Desktop/google-tasks-desktop/src/utils/form/index.ts":"104","/Users/Pong/Desktop/google-tasks-desktop/src/components/Switch/index.ts":"105","/Users/Pong/Desktop/google-tasks-desktop/src/hooks/useMouseTrap.ts":"106","/Users/Pong/Desktop/google-tasks-desktop/src/utils/form/validators.ts":"107","/Users/Pong/Desktop/google-tasks-desktop/src/utils/form/typings.ts":"108","/Users/Pong/Desktop/google-tasks-desktop/src/utils/form/form.ts":"109","/Users/Pong/Desktop/google-tasks-desktop/src/components/Switch/Switch.tsx":"110"},{"size":1110,"mtime":1592723488168,"results":"111","hashOfConfig":"112"},{"size":464,"mtime":1592723488209,"results":"113","hashOfConfig":"112"},{"size":5201,"mtime":1583162203748,"results":"114","hashOfConfig":"112"},{"size":6372,"mtime":1596981268283,"results":"115","hashOfConfig":"112"},{"size":637,"mtime":1602170576947,"results":"116","hashOfConfig":"112"},{"size":1495,"mtime":1592723488204,"results":"117","hashOfConfig":"112"},{"size":411,"mtime":1592723488156,"results":"118","hashOfConfig":"112"},{"size":109,"mtime":1592723488144,"results":"119","hashOfConfig":"112"},{"size":49,"mtime":1583162203742,"results":"120","hashOfConfig":"112"},{"size":105,"mtime":1592723488194,"results":"121","hashOfConfig":"112"},{"size":89,"mtime":1592723488170,"results":"122","hashOfConfig":"112"},{"size":298,"mtime":1592723488203,"results":"123","hashOfConfig":"112"},{"size":591,"mtime":1592723488206,"results":"124","hashOfConfig":"112"},{"size":83,"mtime":1602160477461,"results":"125","hashOfConfig":"112"},{"size":107,"mtime":1602241962356,"results":"126","hashOfConfig":"112"},{"size":944,"mtime":1603281840712,"results":"127","hashOfConfig":"112"},{"size":1803,"mtime":1602253317901,"results":"128","hashOfConfig":"112"},{"size":1882,"mtime":1596635930990,"results":"129","hashOfConfig":"112"},{"size":2086,"mtime":1592723488208,"results":"130","hashOfConfig":"112"},{"size":418,"mtime":1603022608231,"results":"131","hashOfConfig":"112"},{"size":1773,"mtime":1603022632549,"results":"132","hashOfConfig":"112"},{"size":501,"mtime":1603022622282,"results":"133","hashOfConfig":"112"},{"size":3465,"mtime":1603025000505,"results":"134","hashOfConfig":"112"},{"size":280,"mtime":1603198543118,"results":"135","hashOfConfig":"112"},{"size":1035,"mtime":1592723488208,"results":"136","hashOfConfig":"112"},{"size":594,"mtime":1602247007353,"results":"137","hashOfConfig":"112"},{"size":483,"mtime":1596981267967,"results":"138","hashOfConfig":"112"},{"size":2116,"mtime":1603026502618,"results":"139","hashOfConfig":"112"},{"size":6780,"mtime":1603199415382,"results":"140","hashOfConfig":"112"},{"size":2578,"mtime":1603198543117,"results":"141","hashOfConfig":"112"},{"size":3438,"mtime":1603024786959,"results":"142","hashOfConfig":"112"},{"size":9421,"mtime":1603023706197,"results":"143","hashOfConfig":"112"},{"size":917,"mtime":1592723488202,"results":"144","hashOfConfig":"112"},{"size":1437,"mtime":1613657053795,"results":"145","hashOfConfig":"112"},{"size":2267,"mtime":1602163027639,"results":"146","hashOfConfig":"112"},{"size":278,"mtime":1592723488210,"results":"147","hashOfConfig":"112"},{"size":141,"mtime":1592723488174,"results":"148","hashOfConfig":"112"},{"size":121,"mtime":1592723488194,"results":"149","hashOfConfig":"112"},{"size":218,"mtime":1602249256395,"results":"150","hashOfConfig":"112"},{"size":101,"mtime":1592723488175,"results":"151","hashOfConfig":"112"},{"size":129,"mtime":1592723488191,"results":"152","hashOfConfig":"112"},{"size":133,"mtime":1592723488185,"results":"153","hashOfConfig":"112"},{"size":129,"mtime":1592723488178,"results":"154","hashOfConfig":"112"},{"size":189,"mtime":1603021063330,"results":"155","hashOfConfig":"112"},{"size":2226,"mtime":1598365016422,"results":"156","hashOfConfig":"112"},{"size":77,"mtime":1592723488198,"results":"157","hashOfConfig":"112"},{"size":414,"mtime":1592723488146,"results":"158","hashOfConfig":"112"},{"size":1866,"mtime":1592723488178,"results":"159","hashOfConfig":"112"},{"size":4456,"mtime":1603281875394,"results":"160","hashOfConfig":"112"},{"size":1019,"mtime":1602252141332,"results":"161","hashOfConfig":"112"},{"size":1741,"mtime":1592723488191,"results":"162","hashOfConfig":"112"},{"size":924,"mtime":1592723488175,"results":"163","hashOfConfig":"112"},{"size":439,"mtime":1592723488150,"results":"164","hashOfConfig":"112"},{"size":2678,"mtime":1602165733249,"results":"165","hashOfConfig":"112"},{"size":1222,"mtime":1603198551652,"results":"166","hashOfConfig":"112"},{"size":93,"mtime":1592723488152,"results":"167","hashOfConfig":"112"},{"size":529,"mtime":1603021069621,"results":"168","hashOfConfig":"112"},{"size":333,"mtime":1592584359292,"results":"169","hashOfConfig":"112"},{"size":1231,"mtime":1592723488198,"results":"170","hashOfConfig":"112"},{"size":515,"mtime":1603022578891,"results":"171","hashOfConfig":"112"},{"size":1577,"mtime":1603023631481,"results":"172","hashOfConfig":"112"},{"size":1304,"mtime":1603021063330,"results":"173","hashOfConfig":"112"},{"size":389,"mtime":1603021063330,"results":"174","hashOfConfig":"112"},{"size":783,"mtime":1603021063330,"results":"175","hashOfConfig":"112"},{"size":5623,"mtime":1603026628327,"results":"176","hashOfConfig":"112"},{"size":4455,"mtime":1603026549170,"results":"177","hashOfConfig":"112"},{"size":147,"mtime":1592723488154,"results":"178","hashOfConfig":"112"},{"size":113,"mtime":1592723488151,"results":"179","hashOfConfig":"112"},{"size":105,"mtime":1592723488150,"results":"180","hashOfConfig":"112"},{"size":124,"mtime":1592723488149,"results":"181","hashOfConfig":"112"},{"size":4687,"mtime":1603024406906,"results":"182","hashOfConfig":"112"},{"size":782,"mtime":1592723488183,"results":"183","hashOfConfig":"112"},{"size":196,"mtime":1592723488210,"results":"184","hashOfConfig":"112"},{"size":536,"mtime":1602248414255,"results":"185","hashOfConfig":"112"},{"size":1442,"mtime":1592723488193,"results":"186","hashOfConfig":"112"},{"size":1736,"mtime":1592723488154,"results":"187","hashOfConfig":"112"},{"size":150,"mtime":1592723488186,"results":"188","hashOfConfig":"112"},{"size":137,"mtime":1592723488190,"results":"189","hashOfConfig":"112"},{"size":1270,"mtime":1592723488153,"results":"190","hashOfConfig":"112"},{"size":1797,"mtime":1592723488148,"results":"191","hashOfConfig":"112"},{"size":1991,"mtime":1606554789272,"results":"192","hashOfConfig":"112"},{"size":1528,"mtime":1603281840713,"results":"193","hashOfConfig":"112"},{"size":1618,"mtime":1592723488150,"results":"194","hashOfConfig":"112"},{"size":1054,"mtime":1592723488151,"results":"195","hashOfConfig":"112"},{"size":824,"mtime":1592723488153,"results":"196","hashOfConfig":"112"},{"size":113,"mtime":1592723488177,"results":"197","hashOfConfig":"112"},{"size":747,"mtime":1592723488176,"results":"198","hashOfConfig":"112"},{"size":1354,"mtime":1603198978400,"results":"199","hashOfConfig":"112"},{"size":141,"mtime":1592723488145,"results":"200","hashOfConfig":"112"},{"size":117,"mtime":1592723488155,"results":"201","hashOfConfig":"112"},{"size":2306,"mtime":1600613861713,"results":"202","hashOfConfig":"112"},{"size":3926,"mtime":1592723488176,"results":"203","hashOfConfig":"112"},{"size":105,"mtime":1592723488182,"results":"204","hashOfConfig":"112"},{"size":1158,"mtime":1606554168813,"results":"205","hashOfConfig":"112"},{"size":1292,"mtime":1592723488179,"results":"206","hashOfConfig":"112"},{"size":1347,"mtime":1603198986682,"results":"207","hashOfConfig":"112"},{"size":6039,"mtime":1606554168828,"results":"208","hashOfConfig":"112"},{"size":680,"mtime":1600613251466,"results":"209","hashOfConfig":"112"},{"size":445,"mtime":1602247724151,"results":"210","hashOfConfig":"112"},{"size":2056,"mtime":1603281840714,"results":"211","hashOfConfig":"112"},{"size":696,"mtime":1603198543110,"results":"212","hashOfConfig":"112"},{"size":685,"mtime":1603198543112,"results":"213","hashOfConfig":"112"},{"size":6951,"mtime":1603198952197,"results":"214","hashOfConfig":"112"},{"size":119,"mtime":1602247007354,"results":"215","hashOfConfig":"112"},{"size":97,"mtime":1592723488157,"results":"216","hashOfConfig":"112"},{"size":591,"mtime":1592723488167,"results":"217","hashOfConfig":"112"},{"size":3097,"mtime":1602247007355,"results":"218","hashOfConfig":"112"},{"size":1869,"mtime":1602247007355,"results":"219","hashOfConfig":"112"},{"size":8074,"mtime":1602249855641,"results":"220","hashOfConfig":"112"},{"size":697,"mtime":1592723488157,"results":"221","hashOfConfig":"112"},{"filePath":"222","messages":"223","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"b11wxz",{"filePath":"224","messages":"225","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"226","messages":"227","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"228","messages":"229","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"230","messages":"231","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"232","messages":"233","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"234","messages":"235","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"236","messages":"237","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"238","messages":"239","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"240","messages":"241","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"242","messages":"243","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"244","messages":"245","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"246","messages":"247","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"248","messages":"249","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"250","messages":"251","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"252","messages":"253","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"254","messages":"255","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"256","messages":"257","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"258","messages":"259","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"260","messages":"261","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"262","messages":"263","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"264","messages":"265","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"266","messages":"267","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"268","messages":"269","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"270","messages":"271","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"272","messages":"273","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"274","messages":"275","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"276","messages":"277","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"278","messages":"279","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"280","messages":"281","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"282","messages":"283","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"284","messages":"285","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"286","messages":"287","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"288","messages":"289","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"290","messages":"291","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"292","messages":"293","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"294","messages":"295","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"296","messages":"297","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"298","messages":"299","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"300","messages":"301","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"302","messages":"303","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"304","messages":"305","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"306","messages":"307","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"308","messages":"309","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"310","messages":"311","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"312","messages":"313","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"314","messages":"315","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"316","messages":"317","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"318","messages":"319","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"320","messages":"321","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"322","messages":"323","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"324","messages":"325","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"326","messages":"327","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"328","messages":"329","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"330","messages":"331","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"332","messages":"333","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"334","messages":"335","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"336","messages":"337","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"338","messages":"339","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"340","messages":"341","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"342","messages":"343","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"344","messages":"345","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"346","messages":"347","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"348","messages":"349","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"350","messages":"351","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"352","messages":"353","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"354","messages":"355","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"356","messages":"357","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"358","messages":"359","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"360","messages":"361","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"362","messages":"363","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"364","messages":"365","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"366","messages":"367","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"368","messages":"369","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"370","messages":"371","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"372","messages":"373","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"374","messages":"375","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"376","messages":"377","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"378","messages":"379","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"380","messages":"381","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"382","messages":"383","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"384","messages":"385","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"386","messages":"387","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"388","messages":"389","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"390","messages":"391","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"392","messages":"393","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"394","messages":"395","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"396","messages":"397","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"398","messages":"399","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"400","messages":"401","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"402","messages":"403","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"404","messages":"405","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"406","messages":"407","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"408","messages":"409","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"410","messages":"411","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"412","messages":"413","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"414","messages":"415","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"416","messages":"417","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"418","messages":"419","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"420","messages":"421","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"422","messages":"423","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"424","messages":"425","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"426","messages":"427","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"428","messages":"429","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"430","messages":"431","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"432","messages":"433","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"434","messages":"435","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"436","messages":"437","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"438","messages":"439","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"440","messages":"441","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/Pong/Desktop/google-tasks-desktop/src/index.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/theme.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/serviceWorker.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/utils/date.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/App.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/store/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/PrivateRoute.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/AppRegion/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/constants/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/Auth/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/store/epics/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/store/reducers/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/store/selectors/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/store/actions/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/AppRegion/AppRegion.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TaskList.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/Auth/Auth.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/store/selectors/task.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/store/actions/auth.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/store/actions/taskList.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/store/actions/preferences.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/store/actions/task.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/store/selectors/preferences.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/store/selectors/taskList.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/store/reducers/preferences.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/store/reducers/auth.ts",["442"],"/Users/Pong/Desktop/google-tasks-desktop/src/store/reducers/taskList.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/store/reducers/task.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/store/epics/preferences.ts",["443"],"/Users/Pong/Desktop/google-tasks-desktop/src/store/epics/taskList.ts",["444"],"/Users/Pong/Desktop/google-tasks-desktop/src/store/epics/task.ts",["445"],"/Users/Pong/Desktop/google-tasks-desktop/src/store/epics/auth.ts",["446"],"/Users/Pong/Desktop/google-tasks-desktop/src/components/AppRegion/WindowsTitleBar.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/TodoTask/TodoTaskMenu.tsx",["447"],"/Users/Pong/Desktop/google-tasks-desktop/src/utils/nprogress.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/CompletedTaskList/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TodoTaskList/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/NewTask/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TaskListHeader/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/TodoTaskDetails/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/DateTimeDialog/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/hooks/crud-reducer/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/Auth/FileUpload.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/service/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/DeleteIcon.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/DateTimeDialog/DateTimeDialog.tsx",["448"],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/TodoTaskDetails/TodoTaskDetails.tsx",["449"],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Tooltip.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TaskListHeader/TaskListHeader.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/NewTask/NewTask.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/EditIcon.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TodoTaskList/TodoTaskList.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/CompletedTaskList/CompletedTaskList.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Input/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/hooks/crud-reducer/bindDispatch.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/hooks/useBoolean.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/service/auth.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/service/tasksList.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/service/task.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/hooks/crud-reducer/useCRUDReducer.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/hooks/crud-reducer/useActions.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/hooks/crud-reducer/crudSelector.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/hooks/crud-reducer/crudReducer.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/hooks/crud-reducer/crudAction.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Menu/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/IconButton/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Dropdown/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Dialog/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TaskListMenu.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/TodoTaskDetails/DateTimeButton.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/utils/uuid.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Input/Input.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TodoTaskList/TodoTaskListByDate.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Menu/useMuiMenu.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TaskListDropdown/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Menu/MenuItem.tsx",["450"],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Dialog/FormDialog.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Dialog/FullScreenDialog.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Dialog/ConfirmDialog.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Dropdown/Dropdown.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/IconButton/IconButton.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Mui/Menu/Menu.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/DatePicker/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/CompletedTask.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/Task.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/KeyboardShortcuts/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Preferences/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TaskListDropdown/TaskListDropdown.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/DatePicker/DatePicker.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/TodoTask/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/KeyboardShortcuts/KeyboardShortcuts.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/TaskInput.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/ToggleCompleted.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Preferences/Preferences.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/TaskListDropdown/TaskListDropdownItem.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Preferences/Storage.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Preferences/TitleBarSelector.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Preferences/AccentColor.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Preferences/ThemeSelector.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/pages/TaskList/Task/TodoTask/TodoTask.tsx",[],"/Users/Pong/Desktop/google-tasks-desktop/src/utils/form/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Switch/index.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/hooks/useMouseTrap.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/utils/form/validators.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/utils/form/typings.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/utils/form/form.ts",[],"/Users/Pong/Desktop/google-tasks-desktop/src/components/Switch/Switch.tsx",[],{"ruleId":"451","severity":1,"message":"452","line":11,"column":1,"nodeType":"453","endLine":28,"endColumn":2},{"ruleId":"451","severity":1,"message":"454","line":83,"column":1,"nodeType":"453","endLine":83,"endColumn":52},{"ruleId":"451","severity":1,"message":"454","line":107,"column":1,"nodeType":"453","endLine":114,"endColumn":3},{"ruleId":"451","severity":1,"message":"454","line":320,"column":1,"nodeType":"453","endLine":329,"endColumn":3},{"ruleId":"451","severity":1,"message":"454","line":32,"column":1,"nodeType":"453","endLine":32,"endColumn":39},{"ruleId":"455","severity":1,"message":"456","line":33,"column":7,"nodeType":"457","messageId":"458","endLine":33,"endColumn":14},{"ruleId":"455","severity":1,"message":"456","line":33,"column":7,"nodeType":"457","messageId":"458","endLine":33,"endColumn":14},{"ruleId":"455","severity":1,"message":"456","line":46,"column":14,"nodeType":"457","messageId":"458","endLine":46,"endColumn":21},{"ruleId":"459","severity":1,"message":"460","line":45,"column":10,"nodeType":"457","endLine":45,"endColumn":21},"import/no-anonymous-default-export","Unexpected default export of anonymous function","ExportDefaultDeclaration","Assign array to a variable before exporting as module default","@typescript-eslint/no-redeclare","'Context' is already defined.","Identifier","redeclared","react-hooks/exhaustive-deps","React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead."]

================================================
FILE: .eslintignore
================================================
# Logs
logs
*.log

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
.eslintcache

# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules

# OSX
.DS_Store

# flow-typed
flow-typed/npm/*
!flow-typed/npm/module_vx.x.x.js

# App packaged
release
build
electron/*.js

.idea
npm-debug.log.*
__snapshots__

# Package.json
package.json
.travis.yml


================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

release
electron/**/**/*.js
electron/**/**/*.js.map
*.tsbuildinfo


================================================
FILE: .prettierrc
================================================
{
  "overrides": [
    {
      "files": [".prettierrc", ".babelrc", ".eslintrc", ".stylelintrc"],
      "options": {
        "parser": "json"
      }
    }
  ],
  "singleQuote": true,
  "trailingComma": "none",
  "arrowParens": "avoid"
}


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2015-present C. T. Lin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Procfile
================================================
react: npm run app:dev
electron: node scripts/electron-wait-react.js && yarn electron:dev

================================================
FILE: README.md
================================================
## Google Tasks Desktop

> Unofficial google tasks desktop application. Using React and google tasks api

<div>
  <img src="./screenshot/1.png" width="24%">
  <img src="./screenshot/2.png" width="24%">
  <img src="./screenshot/3.png" width="24%">
  <img src="./screenshot/4.png" width="24%">
</div>

#### [Download](https://github.com/Pong420/google-tasks-desktop/releases)

#### :warning: You will need to enable your own [Google Tasks API](https://console.developers.google.com/apis/library/tasks.googleapis.com) whether you are user or developer.

#### Step to enable Google Tasks API.

1. Follow the instruction in https://support.google.com/cloud/answer/6158849 to setup your `OAuth consent screen` and `Credentials` ( In step 6, you should select `Desktop app` as the application type )

2. After the OAuth client created, you could download the `oAuth.json` by clicking this button

<img src="./screenshot/guide-1.png" />

And the `oAuth.json` looks like this

```json
{
  "installed": {
    "client_id": "...",
    "project_id": "...",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://oauth2.googleapis.com/token",
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_secret": "...",
    "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"]
  }
}
```

3. Start and drag the `oAuth.json` into the application.

4. Enable [Google Tasks API](https://console.developers.google.com/apis/library/tasks.googleapis.com)

5. Click on the `Get Code` button and will require authentication. Just ignore the `This app isn't verified` warning and continue because you are the app owner.

6. Paste the code into the input filed and click the `Confirm` button.

### Development

```
yarn dev
```

### Packaging

To package apps for the local platform:

```
yarn package
```

First, refer to the [Multi Platform Build docs](https://www.electron.build/multi-platform-build) for dependencies. Then,

```
yarn package-all
```

### TODO

- [x] Support Window & Linux
- [x] Keyboard shortcuts
- [x] Dark Theme
- [x] Add Note
- [x] Add Date
- [x] Animation
- [x] Sync data periodically
- [x] Move task to another list
- [x] Improve / check performace
- [ ] Subtask
- [ ] Error handling

### Known issue

- Add time / repeat is not supported as API limitation
- Tasks sorting type (My order / Date) is not synced to the official platform (Web/App)
- The position of the task which marks as complete to incomplete may be different after refresh


================================================
FILE: common.d.ts
================================================
type Theme = 'light' | 'dark';
type AccentColor = 'red' | 'blue' | 'amber' | 'green' | 'purple' | 'grey';
type TitleBar = 'native' | 'frameless';

interface Schema$Storage<T> {
  get(): T;
  save(value: NonNullable<T>): void;
}

interface OAuthKeys {
  installed: {
    client_id: string;
    project_id: string;
    auth_uri: string;
    token_uri: string;
    client_secret: string;
    redirect_uris: string[];
  };
}

interface SyncConfig {
  enabled: boolean;
  reconnection: boolean;
  inactiveHours: number;
}

type Schema$Preferences = {
  accentColor: AccentColor;
  maxTasks: number;
  sync: SyncConfig;
  theme: Theme;
  titleBar: TitleBar;
};

declare interface Window {
  __setTheme(theme?: Theme): void;
  __setAccentColor(color?: AccentColor): void;
  __setTitleBar(titleBar?: TitleBar, shouldRelaunch?: boolean): void;
  platform: NodeJS.Platform;
  openExternal: Electron.Shell['openExternal'];
  getCurrentWindow(): Electron.BrowserWindow;
  oAuth2Storage: Schema$Storage<OAuthKeys | undefined>;
  tokenStorage: Schema$Storage<any>;
  preferencesStorage: Schema$Storage<Schema$Preferences>;
  taskListSortByDateStorage: Schema$Storage<string[]>;
  logout: () => void;
  TOKEN_PATH: string;
  OAUTH2_KEYS_PATH: string;
  PREFERENCES_PATH: string;
  TASKLIST_SORT_BY_DATE_PATH: string;
  STORAGE_DIRECTORY: string;
  relaunch: () => void;
}


================================================
FILE: config-overrides.js
================================================
const path = require('path');

/** @typedef {import('webpack').Configuration} Configuration */

/**
 * @param {Configuration} config
 * @param {*} env
 * @returns {Configuration}
 */
module.exports = function (config, env) {
  config.resolve.alias = {
    ...config.resolve.alias,
    fs: path.resolve(__dirname, 'mock-fs.js')
  };

  config.module.rules = config.module.rules.map(r => {
    if (r.oneOf) {
      return {
        ...r,
        oneOf: r.oneOf.map(r => {
          if (Array.isArray(r.use)) {
            return {
              ...r,
              use: r.use.map(u =>
                typeof u === 'object' &&
                typeof u.options === 'object' &&
                u.loader.includes('sass-loader')
                  ? {
                      ...u,
                      options: {
                        ...u.options,
                        additionalData: `@import 'scss/index.scss';`,
                        sassOptions: {
                          includePaths: [path.resolve(__dirname, 'src')]
                        }
                      }
                    }
                  : u
              )
            };
          }
          return r;
        })
      };
    }
    return r;
  });

  return config;
};


================================================
FILE: electron/electron.d.ts
================================================
declare module 'electron-devtools-installer';


================================================
FILE: electron/main.ts
================================================
import path from 'path';
import url from 'url';
import {
  app,
  shell,
  nativeTheme,
  BrowserWindow,
  BrowserWindowConstructorOptions
} from 'electron';
import { MenuBuilder } from './menu';
import { initStorage } from './storage';

let mainWindow: BrowserWindow | null = null;

const isDevelopment = process.env.NODE_ENV === 'development';

const { preferencesStorage } = initStorage(app, nativeTheme);

app.allowRendererProcessReuse = true;

async function createWindow() {
  if (isDevelopment) {
    const {
      default: installExtension,
      REACT_DEVELOPER_TOOLS,
      REDUX_DEVTOOLS
    } = await import('electron-devtools-installer');
    await installExtension(REACT_DEVELOPER_TOOLS);
    await installExtension(REDUX_DEVTOOLS);
  }

  let options: BrowserWindowConstructorOptions = {
    height: 500,
    width: 300,
    show: false,
    webPreferences: {
      enableRemoteModule: true,
      preload: path.join(__dirname, './preload/index.js')
    }
  };

  const titleBar: TitleBar = preferencesStorage.get().titleBar;

  if (
    titleBar === 'frameless' ||
    process.platform === 'win32' ||
    process.platform === 'darwin'
  ) {
    options = {
      ...options,
      frame: false,
      titleBarStyle: titleBar === 'frameless' ? 'hidden' : 'hiddenInset'
    };
  }

  mainWindow = new BrowserWindow(options);
  mainWindow.setMenuBarVisibility(false);

  const startUrl =
    process.env.ELECTRON_START_URL ||
    url.format({
      pathname: path.join(__dirname, '../build/index.html'),
      protocol: 'file:',
      slashes: true
    });

  mainWindow.loadURL(startUrl);

  mainWindow.webContents.on('did-finish-load', () => {
    mainWindow && mainWindow.show();
  });

  mainWindow.webContents.on('new-window', (event, url) => {
    event.preventDefault();
    shell.openExternal(url);
  });

  mainWindow.on('closed', () => {
    mainWindow = null;
  });

  const menuBuilder = new MenuBuilder(mainWindow);
  menuBuilder.buildMenu();
}

app.on('ready', createWindow);

app.on('window-all-closed', () => {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (mainWindow === null) {
    createWindow();
  }
});


================================================
FILE: electron/menu.ts
================================================
// borrow from
// https://github.com/electron-react-boilerplate/examples/blob/master/examples/typescript/app/main.dev.ts

import {
  app,
  Menu,
  shell,
  BrowserWindow,
  MenuItemConstructorOptions
} from 'electron';

const help = {
  label: 'Help',
  submenu: [
    {
      label: 'Github',
      click() {
        shell.openExternal('https://github.com/Pong420/google-tasks-desktop');
      }
    },
    {
      label: 'Search Issues',
      click() {
        shell.openExternal(
          'https://github.com/Pong420/google-tasks-desktop/issues'
        );
      }
    }
  ]
};

export class MenuBuilder {
  mainWindow: BrowserWindow;

  constructor(mainWindow: BrowserWindow) {
    this.mainWindow = mainWindow;
  }

  buildMenu() {
    if (
      process.env.NODE_ENV === 'development' ||
      process.env.DEBUG_PROD === 'true'
    ) {
      this.setupDevelopmentEnvironment();
    }

    const template =
      process.platform === 'darwin'
        ? this.buildDarwinTemplate()
        : this.buildDefaultTemplate();

    const menu = Menu.buildFromTemplate(template);
    Menu.setApplicationMenu(menu);

    return menu;
  }

  setupDevelopmentEnvironment() {
    this.mainWindow.webContents.toggleDevTools();
    this.mainWindow.webContents.on('context-menu', (e, props) => {
      const { x, y } = props;

      Menu.buildFromTemplate([
        {
          label: 'Inspect element',
          click: () => {
            this.mainWindow.webContents.inspectElement(x, y);
          }
        }
      ]).popup({ window: this.mainWindow });
    });
  }

  buildDarwinTemplate(): MenuItemConstructorOptions[] {
    const subMenuAbout = {
      label: 'Google Tasks',
      submenu: [
        {
          label: 'About Google Tasks',
          selector: 'orderFrontStandardAboutPanel:'
        },
        { type: 'separator' },
        { label: 'Services', submenu: [] },
        { type: 'separator' },
        {
          label: 'Hide Google Tasks',
          accelerator: 'Command+H',
          selector: 'hide:'
        },
        {
          label: 'Hide Others',
          accelerator: 'Command+Shift+H',
          selector: 'hideOtherApplications:'
        },
        { label: 'Show All', selector: 'unhideAllApplications:' },
        { type: 'separator' },
        {
          label: 'Quit',
          accelerator: 'Command+Q',
          click: () => {
            app.quit();
          }
        }
      ]
    };
    const subMenuEdit = {
      label: 'Edit',
      submenu: [
        { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' },
        { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' },
        { type: 'separator' },
        { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' },
        { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' },
        { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' },
        {
          label: 'Select All',
          accelerator: 'Command+A',
          selector: 'selectAll:'
        }
      ]
    };
    const subMenuWindow = {
      label: 'Window',
      submenu: [
        {
          label: 'Minimize',
          accelerator: 'Command+M',
          selector: 'performMiniaturize:'
        },
        { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' },
        { type: 'separator' },
        { label: 'Bring All to Front', selector: 'arrangeInFront:' }
      ]
    };
    const subMenuHelp = help;
    const subMenuView = {
      label: 'View',
      submenu: [
        { role: 'Reload', accelerator: 'CmdOrCtrl+R' },
        {
          label: 'Toggle Full Screen',
          accelerator: 'Ctrl+Command+F',
          click: () => {
            this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
          }
        },
        { type: 'separator' },
        { role: 'toggledevtools', accelerator: 'Option+CmdOrCtrl+i' }
      ]
    };
    return [
      subMenuAbout,
      subMenuEdit,
      subMenuView,
      subMenuWindow,
      subMenuHelp
    ] as MenuItemConstructorOptions[];
  }

  buildDefaultTemplate(): MenuItemConstructorOptions[] {
    const templateDefault = [
      {
        label: '&File',
        submenu: [
          { label: '&Open', accelerator: 'Ctrl+O' },
          {
            label: '&Close',
            accelerator: 'Ctrl+W',
            click: () => {
              this.mainWindow.close();
            }
          }
        ]
      },
      {
        label: '&View',
        submenu: [
          {
            label: '&Reload',
            accelerator: 'Ctrl+R',
            click: () => {
              this.mainWindow.webContents.reload();
            }
          },
          {
            label: 'Toggle &Full Screen',
            accelerator: 'F11',
            click: () => {
              this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
            }
          },
          {
            label: 'Toggle &Developer Tools',
            accelerator: 'Alt+Ctrl+I',
            click: () => {
              this.mainWindow.webContents.toggleDevTools();
            }
          }
        ]
      },
      help
    ];

    return templateDefault;
  }
}


================================================
FILE: electron/preload/index.ts
================================================
import fs from 'fs';
import { remote } from 'electron';
import { handleOSTheme } from './theme';
import { initStorage } from '../storage';

function relaunch() {
  remote.app.relaunch();
  remote.app.exit();
}

const storage = initStorage(remote.app, remote.nativeTheme);
const { preferencesStorage } = storage;

window.__setAccentColor = (newColor?: AccentColor) => {
  const preferences = preferencesStorage.get();
  const accentColor = newColor || preferences.accentColor;

  document.documentElement.setAttribute('data-accent-color', accentColor);

  if (newColor) {
    preferencesStorage.save({
      ...preferences,
      accentColor
    });
  }
};

window.__setTitleBar = (newTitleBar?: TitleBar, shouldRelaunch?: boolean) => {
  const preferences = preferencesStorage.get();
  const titleBar = newTitleBar || preferences.titleBar;
  document.documentElement.setAttribute('data-title-bar', titleBar);
  if (titleBar) {
    preferencesStorage.save({
      ...preferences,
      titleBar
    });

    if (shouldRelaunch) {
      relaunch();
    }
  }
};

Object.assign(window, storage);

window.platform = process.platform;
window.getCurrentWindow = remote.getCurrentWindow;
window.openExternal = remote.shell.openExternal;
window.logout = () => fs.unlinkSync(storage.TOKEN_PATH);
window.relaunch = relaunch;

process.once('loaded', () => {
  handleOSTheme();
});


================================================
FILE: electron/preload/theme.ts
================================================
import { remote } from 'electron';

export const setTheme = (newTheme?: Theme) => {
  const preferences = window.preferencesStorage.get();
  let theme = newTheme || preferences.theme;

  document.documentElement.setAttribute('data-theme', theme);

  if (newTheme) {
    window.preferencesStorage.save({
      ...preferences,
      theme
    });
  }
};

window.__setTheme = setTheme;

export function handleOSTheme() {
  if (process.platform === 'darwin') {
    const { systemPreferences } = remote;
    systemPreferences.subscribeNotification(
      'AppleInterfaceThemeChangedNotification',
      () => setTheme()
    );
  }
}


================================================
FILE: electron/storage.ts
================================================
import fs from 'fs';
import path from 'path';
import { App, NativeTheme } from 'electron';

function FileStorage<T extends {}>(path: string): Schema$Storage<T | undefined>;
function FileStorage<T extends {}>(path: string, defaultValue: T): Schema$Storage<T> // prettier-ignore
// prettier-ignore
function FileStorage<T extends {}>(path: string, defaultValue?: T): Schema$Storage<T | undefined> { 
  return {
    get() {
      try {
        const val = fs.readFileSync(path, 'utf8');
        return JSON.parse(val);
      } catch (error) {}

      return defaultValue;
    },
    save(value) {
      fs.writeFileSync(path, JSON.stringify(value, null, 2));
    }
  };
}

export function initStorage(app: App, nativeTheme: NativeTheme) {
  const STORAGE_DIRECTORY = path.join(
    app.getPath('userData'),
    'google-tasks-desktop'
  );

  const TOKEN_PATH = path.join(STORAGE_DIRECTORY, 'token.json');

  const OAUTH2_KEYS_PATH = path.join(STORAGE_DIRECTORY, 'oauth2.json');

  const PREFERENCES_PATH = path.join(STORAGE_DIRECTORY, 'preferences.json');

  const TASKLIST_SORT_BY_DATE_PATH = path.join(
    STORAGE_DIRECTORY,
    'tasklist-order.json'
  );

  if (!fs.existsSync(STORAGE_DIRECTORY)) {
    fs.mkdirSync(STORAGE_DIRECTORY);
  }

  const oAuth2Storage = FileStorage<any>(OAUTH2_KEYS_PATH);

  const tokenStorage = FileStorage<any>(TOKEN_PATH);

  const taskListSortByDateStorage = FileStorage<any>(
    TASKLIST_SORT_BY_DATE_PATH,
    []
  );

  const defaultPrefrences: Schema$Preferences = {
    accentColor: 'blue',
    maxTasks: 100,
    theme: nativeTheme.shouldUseDarkColors ? 'dark' : 'light',
    titleBar: 'native',
    sync: {
      enabled: true,
      reconnection: true,
      inactiveHours: 12
    }
  };

  const preferencesStorage = FileStorage<Schema$Preferences>(
    PREFERENCES_PATH,
    defaultPrefrences
  );

  // for version <= v3.0.2
  let preferences = preferencesStorage.get();
  preferencesStorage.save(
    Object.entries(defaultPrefrences).reduce((results, [key, value]) => {
      const currentValue: unknown =
        preferences[key as keyof typeof preferences];
      return {
        ...results,
        [key]: typeof currentValue === 'undefined' ? value : currentValue
      };
    }, {} as Schema$Preferences)
  );

  return {
    STORAGE_DIRECTORY,
    TOKEN_PATH,
    PREFERENCES_PATH,
    TASKLIST_SORT_BY_DATE_PATH,
    oAuth2Storage,
    tokenStorage,
    taskListSortByDateStorage,
    preferencesStorage
  };
}


================================================
FILE: electron/tsconfig.json
================================================
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "module": "commonjs",
    "noEmit": false,
    "rootDir": "./",
    "incremental": true
  },
  "exclude": ["../node_modules"],
  "include": ["./*.ts", "./preload/*.ts", "../common.d.ts"]
}


================================================
FILE: mock-fs.js
================================================
// https://github.com/googleapis/google-api-nodejs-client/issues/1775#issuecomment-520572247

module.exports = {
  readFile() {},
  readFileSync() {}
};


================================================
FILE: package.json
================================================
{
  "name": "google-tasks-desktop",
  "version": "3.1.3",
  "scripts": {
    "dev": "nf start",
    "build": "yarn app:build && yarn electron:compile",
    "package": "rimraf release && yarn build && electron-builder build --publish never",
    "package-all": "rimraf release && yarn build && electron-builder build -mwl",
    "lint": "eslint 'electron/**/*.ts?(x)' && eslint 'src/**/*.ts?(x)'",
    "app:dev": "cross-env BROWSER=false react-app-rewired start",
    "app:build": "react-app-rewired build",
    "electron:compile": "tsc --project electron/tsconfig.json",
    "electron:dev": "cross-env NODE_ENV=development & electron electron/main.js",
    "component": "node scripts/component.js",
    "get": "node scripts/type.js",
    "redux": "node scripts/redux.js",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  },
  "homepage": ".",
  "main": "./electron/main.js",
  "build": {
    "productName": "Google Tasks",
    "appId": "Google Tasks",
    "directories": {
      "buildResources": "public",
      "output": "release"
    },
    "extraMetadata": {
      "main": "electron/main.js"
    },
    "files": [
      "build/index.html",
      "build/**/*",
      "electron/**/*.js",
      "package.json"
    ],
    "extraFiles": [
      "credentials"
    ],
    "mac": {
      "target": [
        "dmg",
        "pkg",
        "zip"
      ],
      "darkModeSupport": true,
      "icon": "public/icon/icon.png",
      "type": "distribution"
    },
    "dmg": {
      "contents": [
        {
          "x": 130,
          "y": 220
        },
        {
          "x": 410,
          "y": 220,
          "type": "link",
          "path": "/Applications"
        }
      ]
    },
    "pkg": {
      "license": "LICENSE"
    },
    "win": {
      "target": [
        "nsis",
        "portable",
        "zip"
      ],
      "icon": "public/icon/icon.ico"
    },
    "nsis": {
      "installerIcon": "public/icon/icon.ico",
      "license": "LICENSE",
      "warningsAsErrors": false
    },
    "linux": {
      "target": [
        "AppImage",
        "deb",
        "snap"
      ],
      "icon": "./public/icon/512x512.png",
      "desktop": {
        "Type": "Application",
        "Encoding": "UTF-8",
        "Name": "Google Tasks",
        "Comment": "Unofficial google tasks desktop application",
        "Icon": "google-tasks-desktop",
        "Terminal": "false",
        "StartupWMClass": "google-tasks-desktop"
      }
    },
    "snap": {
      "grade": "stable"
    }
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/Pong420/google-tasks-desktop"
  },
  "author": {
    "name": "Pong",
    "email": "samfunghp@gmial.com",
    "url": "https://pong420.netlify.app"
  },
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/Pong420/google-tasks-desktop/issues"
  },
  "eslintConfig": {
    "extends": "react-app",
    "rules": {
      "react/self-closing-comp": "warn",
      "import/no-anonymous-default-export": 0
    }
  },
  "husky": {
    "hooks": {
      "pre-co3mit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --max-warnings=0",
      "prettier --ignore-path .eslintignore --write"
    ],
    "{*.json,.{babelrc,eslintrc,prettierrc}}": [
      "prettier --ignore-path .eslintignore --parser json --write"
    ],
    "*.{css,scss}": [
      "prettier --ignore-path .eslintignore --single-quote --write"
    ],
    "*.{yml,md}": [
      "prettier --ignore-path .eslintignore --single-quote --write"
    ]
  },
  "devDependencies": {
    "@types/history": "^4.7.8",
    "@types/lodash": "^4.14.168",
    "@types/mousetrap": "^1.6.5",
    "@types/node": "^14.14.37",
    "@types/nprogress": "^0.2.0",
    "@types/react": "^17.0.3",
    "@types/react-dom": "^17.0.3",
    "@types/react-router-dom": "^5.1.7",
    "cross-env": "^7.0.3",
    "electron": "11.5.0",
    "electron-builder": "^22.10.5",
    "electron-devtools-installer": "^3.1.1",
    "foreman": "^3.0.1",
    "husky": "^4.2.3",
    "lint-staged": "^10.5.4",
    "react-scripts": "4.0.3",
    "prettier": "^2.2.1",
    "react-desktop": "^0.3.9",
    "rimraf": "^3.0.2",
    "sass": "^1.32.8",
    "typescript": "4.2.3"
  },
  "dependencies": {
    "@material-ui/core": "^4.11.3",
    "@material-ui/icons": "^4.11.2",
    "connected-react-router": "^6.9.1",
    "customize-cra": "^1.0.0",
    "googleapis": "^47.0.0",
    "history": "^4.10.1",
    "lodash": "^4.17.21",
    "mousetrap": "^1.6.5",
    "nprogress": "^0.2.0",
    "rc-field-form": "^1.20.0",
    "react": "^17.0.2",
    "react-app-rewired": "^2.1.8",
    "react-dom": "^17.0.2",
    "react-redux": "^7.2.3",
    "react-router-dom": "^5.2.0",
    "react-sortable-hoc": "^2.0.0",
    "redux": "^4.0.5",
    "redux-observable": "^1.2.0",
    "reselect": "^4.0.0",
    "rxjs": "^6.6.6",
    "typeface-nunito-sans": "^1.1.13",
    "typeface-roboto": "^1.1.13",
    "use-rx-hooks": "1.6.2"
  },
  "devEngines": {
    "node": ">=7.x",
    "npm": ">=4.x",
    "yarn": ">=0.21.3"
  },
  "browserslist": [
    "last 1 chrome version"
  ]
}


================================================
FILE: public/index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <meta name="theme-color" content="#000000" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Google Tasks</title>
    <script>
      __setTheme();
      __setAccentColor();
      __setTitleBar();
      document.documentElement.setAttribute('data-platform', window.platform);
    </script>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>


================================================
FILE: public/manifest.json
================================================
{
  "short_name": "Google Tasks",
  "name": "Create Google Tasks Sample",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}


================================================
FILE: scripts/component.js
================================================
const fs = require('fs');
const path = require('path');
const pkg = require('../package.json');

const useTypescript =
  (pkg['devDependencies'] || {}).typescript ||
  (pkg['dependencies'] || {}).typescript;

const componentName = process.argv
  .slice(2)
  .find(v => !/-/.test(v))
  .replace(/^\w/, function (chr) {
    return chr.toUpperCase();
  });
const componentOnly = process.argv.find(v => v === '-s');
const dir = path.join(
  __dirname,
  `../src/components/`,
  componentOnly ? '' : componentName
);

const write = (path, content) => {
  fs.writeFileSync(
    path,
    content.replace(/^ {2}/gm, '').replace(/^ *\n/, ''),
    'utf-8'
  );
};

if (!fs.existsSync(dir)) {
  fs.mkdirSync(dir);
}

const index = `
  import './${componentName}.scss';
  
  export * from './${componentName}';
  export { ${componentName} as default } from './${componentName}';
  `;

const reactComponent = `
import React from 'react';
  
  export function ${componentName}() {
    return (
      <div className="${componentName
        .split(/(?=[A-Z])/)
        .map(str => str.toLocaleLowerCase())
        .join('-')}"></div>
    );
  }
  `;

const scss = '';

if (!componentOnly) {
  write(`${dir}/index.${useTypescript ? 'ts' : 'js'}`, index);
  write(`${dir}/${componentName}.scss`, scss);
}

write(
  `${dir}/${componentName}.${useTypescript ? 'tsx' : 'jsx'}`,
  reactComponent
);


================================================
FILE: scripts/electron-wait-react.js
================================================
// @ts-check
const exec = require('child_process').exec;
const net = require('net');

let client = new net.Socket();
const port = process.env.PORT ? Number(process.env.PORT) - 100 : 3000;

process.env.ELECTRON_START_URL = `http://localhost:${port}`;

let startedElectron = false;
const tryConnection = () =>
  client.connect({ port }, () => {
    client.end();
    if (!startedElectron) {
      process.off('uncaughtException', uncaughtException);
      client.destroy();
      console.log('starting electron');
      startedElectron = true;
      exec('npm run electron:dev');
    }
  });

exec('npm run electron:compile', tryConnection);

function uncaughtException() {
  client.destroy();
  client = new net.Socket();
  setTimeout(tryConnection, 1000);
}

process.on('uncaughtException', uncaughtException);


================================================
FILE: scripts/redux.js
================================================
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
const [, , ...args] = process.argv;

const execPromise = command => {
  return new Promise((resolve, reject) => {
    const cmd = exec(command, (error, stdout) => {
      if (error) {
        reject(error);
        return;
      }

      resolve();
    });

    cmd.stdout.on('data', data => {
      console.log(data.trim());
    });
  });
};

const root = path.join(__dirname, '../src');
const templatePath = path.join(__dirname, 'template');
const store = path.join(root, 'store');
const subdir = ['actions', 'epics', 'reducers'].map(dir =>
  path.join(store, dir)
);

if (args[0] === 'init') {
  const commands = [
    `yarn add redux`,
    `yarn add react-redux && yarn add --dev @types/react-redux`,
    `yarn add rxjs && yarn add redux-observable`
  ];

  (async () => {
    for (const cmd of commands) {
      await execPromise(cmd);
    }
  })();

  [store, ...subdir].forEach(dir => {
    const key = dir.split('/').slice(-1)[0];
    fs.readFile(
      path.join(templatePath, 'store', `${key}.tmpl`),
      (error, content) => {
        if (!error) {
          !fs.existsSync(dir) && fs.mkdirSync(dir);
          fs.writeFileSync(path.join(dir, 'index.ts'), content);
        }
      }
    );
  });

  fs.readFile(path.join(templatePath, 'useActions.tmpl'), (error, content) => {
    if (!error) {
      fs.writeFileSync(path.join(root, 'hooks', 'useActions.ts'), content);
    }
  });
} else if (args[0]) {
  const filename = args[0];
  subdir.map(dir => fs.writeFileSync(path.join(dir, `${filename}.ts`), ''));
}


================================================
FILE: scripts/template/store/actions.tmpl
================================================


================================================
FILE: scripts/template/store/epics.tmpl
================================================
import { combineEpics } from 'redux-observable';

export default combineEpics(

);


================================================
FILE: scripts/template/store/reducers.tmpl
================================================
import { combineReducers } from 'redux';

const rootReducer = () => combineReducers({

});

export type RootState = ReturnType<ReturnType<typeof rootReducer>>;

export default rootReducer;


================================================
FILE: scripts/template/store/store.tmpl
================================================
import { createStore, applyMiddleware, compose } from 'redux';
import { createEpicMiddleware, Epic } from 'redux-observable';
import { BehaviorSubject } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import rootEpic from './epics';
import createRootReducer from './reducers';

const epic$ = new BehaviorSubject(rootEpic);
const hotReloadingEpic: Epic = (...args) =>
  epic$.pipe(switchMap(epic => epic(...args)));

export default function configureStore() {
  const epicMiddleware = createEpicMiddleware();
  const composeEnhancers =
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
  const enhancer = composeEnhancers(applyMiddleware(epicMiddleware));

  const store = createStore(createRootReducer(), undefined, enhancer);

  epicMiddleware.run(hotReloadingEpic);

  if (process.env.NODE_ENV !== 'production') {
    if (module.hot) {
      module.hot.accept('./reducers', () => {
        store.replaceReducer(createRootReducer());
      });

      module.hot.accept('./epics', () => {
        const nextRootEpic = require('./epics').default;
        epic$.next(nextRootEpic);
      });
    }
  }

  return store;
}

export * from './actions';
export * from './reducers';
export * from './epics';


================================================
FILE: scripts/template/useActions.tmpl
================================================
import { useMemo, useRef, Dispatch as ReactDispatch } from 'react';
import { AnyAction, Dispatch } from 'redux';
import { useDispatch } from 'react-redux';

interface ActionCreators {
  [k: string]: (...args: any[]) => AnyAction;
}

type Handler<A extends ActionCreators> = {
  [X in keyof A]: (...args: Parameters<A[X]>) => void;
};

export function withDispatch<A extends ActionCreators>(
  creators: A,
  dispatch: Dispatch | ReactDispatch<any>
) {
  const handler = {} as Handler<A>;
  for (const key in creators) {
    const creator = creators[key];
    handler[key] = (...args: Parameters<typeof creator>) => {
      dispatch(creator(...args));
    };
  }

  return handler;
}

export function useActions<A extends ActionCreators>(creators: A): Handler<A> {
  const dispatch = useDispatch();
  const creatorsRef = useRef(creators);

  return useMemo(() => withDispatch(creatorsRef.current, dispatch), [dispatch]);
}

================================================
FILE: scripts/type.js
================================================
const { exec } = require('child_process');

const [, , ...args] = process.argv;

const install = pkg => {
  return `yarn add ${pkg} && yarn add --dev @types/${pkg}`;
};

const execPromise = command => {
  return new Promise((resolve, reject) => {
    const cmd = exec(command, (error, stdout) => {
      if (error) {
        reject(error);
        return;
      }

      resolve();
    });

    cmd.stdout.on('data', data => {
      console.log(data.trim());
    });
  });
};

const promises = args.map(pkg => execPromise(install(pkg)));

Promise.all(promises).then(() => {
  process.exit(0);
});


================================================
FILE: src/App.tsx
================================================
import React from 'react';
import { Route, Switch, Redirect, generatePath } from 'react-router-dom';
import { AppRegion } from './components/AppRegion';
import { PrivateRoute } from './components/PrivateRoute';
import { Auth } from './pages/Auth';
import { TaskList } from './pages/TaskList';
import { PATHS } from './constants';

const App = () => {
  return (
    <>
      <AppRegion />
      <Switch>
        <Route path={PATHS.AUTH} component={Auth} />
        <PrivateRoute path={PATHS.TASKLIST} component={TaskList} />
        <Redirect to={generatePath(PATHS.TASKLIST, {})} />
      </Switch>
    </>
  );
};

export default App;


================================================
FILE: src/components/AppRegion/AppRegion.scss
================================================
.app-region {
  @include absolute(0, null, 0);
  @include dimen(100%, var(--header-height));

  .simple-title-bar {
    @include sq-dimen(100%);
    @include app-region-drag;
    padding: 10px 0px 0px 10px;
    button {
      @include app-region-no-drag;
      z-index: $app-region-z-index;
    }
  }

  // drawin
  .app-region-drag {
    @include app-region-drag;
    @include sq-dimen(100%);
  }
}

[data-platform^='win32'][data-title-bar^='native'] {
  .app-region {
    height: auto;
    > div {
      @include relative();
      z-index: $app-region-z-index;
    }
  }

  .windows-title {
    @include flex(center);
    svg {
      @include sq-dimen(14px);
      margin-top: 1px;
      margin-right: 5px;
    }
  }
}

body {
  > [role='dialog'],
  > [role='presentation'] {
    @include app-region-no-drag;
    z-index: $app-region-z-index !important;
  }
}


================================================
FILE: src/components/AppRegion/AppRegion.tsx
================================================
import React, { ReactNode } from 'react';
import { useSelector } from 'react-redux';
import { IconButton } from '../Mui';
import { titleBarSelector } from '../../store';
import { WindowsTitleBar } from './WindowsTitleBar';
import Close from '@material-ui/icons/Close';

const { close } = window.getCurrentWindow();

export function AppRegion() {
  const titleBar = useSelector(titleBarSelector);
  let content: ReactNode = null;

  if (window.platform === 'darwin') {
    content = <div className="app-region-drag" />;
  } else if (titleBar === 'frameless') {
    content = (
      <div className="simple-title-bar">
        {/* should not pass `close` function directly into `onClick` props */}
        <IconButton icon={Close} onClick={() => close()} />
      </div>
    );
  } else {
    // native
    if (window.platform === 'win32') {
      content = <WindowsTitleBar />;
    }
  }

  return <div className="app-region">{content}</div>;
}


================================================
FILE: src/components/AppRegion/WindowsTitleBar.tsx
================================================
import React, { useState, useEffect, Suspense } from 'react';
import { useSelector } from 'react-redux';
import { themeSelector } from '../../store';
import { ReactComponent as Logo } from '../../assets/logo.svg';

const WindowsTitleBarComp = React.lazy(() =>
  import('react-desktop/windows').then(({ TitleBar }) => ({
    default: TitleBar
  }))
);

const {
  isMaximized,
  minimize,
  maximize,
  unmaximize,
  close
} = window.getCurrentWindow();

function toggleMaximize() {
  isMaximized() ? unmaximize() : maximize();
}

export function WindowsTitleBar() {
  const [isFullscreen, setIsFullscreen] = useState(isMaximized());
  const theme = useSelector(themeSelector);

  useEffect(() => {
    function onResize() {
      setIsFullscreen(isMaximized());
    }

    onResize();

    window.addEventListener('resize', onResize);

    return () => window.removeEventListener('resize', onResize);
  }, []);

  return (
    <Suspense fallback={null}>
      <WindowsTitleBarComp
        title={
          <div className="windows-title">
            <Logo />
            Google Tasks
          </div>
        }
        controls
        theme={theme}
        background="var(--main-color)"
        isMaximized={isFullscreen}
        onCloseClick={() => close()}
        onMinimizeClick={() => minimize()}
        onMaximizeClick={() => toggleMaximize()}
        onRestoreDownClick={() => toggleMaximize()}
      />
    </Suspense>
  );
}


================================================
FILE: src/components/AppRegion/index.ts
================================================
import './AppRegion.scss';

export * from './AppRegion';
export { AppRegion as default } from './AppRegion';


================================================
FILE: src/components/KeyboardShortcuts/KeyboardShortcuts.scss
================================================
.keyboard-shortcuts {
  color: var(--text-color);

  .keyboard-shortcuts-label {
    @include dimen(100%);
  }

  .keyboard-shortcuts-key {
    min-width: 100px;
    text-align: right;
    padding-left: 10px;
  }
}


================================================
FILE: src/components/KeyboardShortcuts/KeyboardShortcuts.tsx
================================================
import React from 'react';
import { FullScreenDialog, FullScreenDialogProps } from '../Mui/Dialog';
import shortcuts from './shortcuts.json';

function normalizeKeyName(str: string) {
  switch (window.platform) {
    case 'darwin':
      return str.replace('Alt', '⌥');

    default:
      return str;
  }
}

export function KeyboardShortcuts(props: FullScreenDialogProps) {
  return (
    <FullScreenDialog
      {...props}
      className="keyboard-shortcuts"
      title="Keyboard shortcuts"
    >
      <div className="keyboard-shortcuts-content">
        {Object.entries(shortcuts).map(([type, rows]) => (
          <FullScreenDialog.Section key={type}>
            <FullScreenDialog.Title children={type} />
            {rows.map(({ label, key }, index) => (
              <FullScreenDialog.Row key={index}>
                <div className="keyboard-shortcuts-label">{label}</div>
                <div className="keyboard-shortcuts-key">
                  {normalizeKeyName(key)}
                </div>
              </FullScreenDialog.Row>
            ))}
          </FullScreenDialog.Section>
        ))}
      </div>
    </FullScreenDialog>
  );
}


================================================
FILE: src/components/KeyboardShortcuts/index.ts
================================================
import './KeyboardShortcuts.scss';

export * from './KeyboardShortcuts';
export { KeyboardShortcuts as default } from './KeyboardShortcuts';


================================================
FILE: src/components/KeyboardShortcuts/shortcuts.json
================================================
{
  "Actions": [
    { "label": "Add a task", "key": "Enter" },
    { "label": "Focus on previous/next task", "key": "Up/Down" },
    { "label": "Move task up/down", "key": "Alt + Up/Down" },
    { "label": "Enter details view", "key": "Shift + Enter" }
  ],
  "Details view": [{ "label": "Exit details view", "key": "Esc" }]
}


================================================
FILE: src/components/Mui/DeleteIcon.tsx
================================================
import React from 'react';
import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';

export const DeleteIcon = React.forwardRef<SVGSVGElement, SvgIconProps>(
  (props, ref) => {
    return (
      <SvgIcon {...props} ref={ref}>
        <path d="M15 4V3H9v1H4v2h1v13c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V6h1V4h-5zm2 15H7V6h10v13z" />
        <path d="M9 8h2v9H9zm4 0h2v9h-2z" />
      </SvgIcon>
    );
  }
);


================================================
FILE: src/components/Mui/Dialog/ConfirmDialog.tsx
================================================
import React, { useMemo, ReactNode } from 'react';
import Button from '@material-ui/core/Button';
import Dialog, { DialogProps } from '@material-ui/core/Dialog';
import mergeWith from 'lodash/mergeWith';

export interface ConfirmDialogProps extends DialogProps {
  title?: string;
  confirmLabel?: string;
  open: boolean;
  children?: ReactNode;
  onClose(): void;
  onConfirm: () => unknown;
  autoFocusConfirmButon?: boolean;
}

const backdropProps = { classes: { root: 'mui-menu-backdrop' } };

export function ConfirmDialog({
  title,
  confirmLabel = 'Confirm',
  children,
  onClose,
  onConfirm,
  autoFocusConfirmButon = true,
  classes,
  ...props
}: ConfirmDialogProps) {
  const mergedClasses = useMemo<ConfirmDialogProps['classes']>(
    () =>
      mergeWith(
        { root: 'mui-dialog', paper: 'mui-dialog-paper' },
        classes,
        (a, b) => a + ' ' + b
      ),
    [classes]
  );

  return (
    <Dialog
      {...props}
      onClose={onClose}
      classes={mergedClasses}
      BackdropProps={backdropProps}
    >
      <div className="dialog-scroll-content">
        <div className="dialog-title">{title}</div>
        <div className="dialog-content">{children}</div>
        <div className="dialog-actions">
          <Button onClick={onClose}>Cancel</Button>
          <Button
            onClick={() => onConfirm() !== false && onClose()}
            autoFocus={autoFocusConfirmButon}
          >
            {confirmLabel}
          </Button>
        </div>
      </div>
    </Dialog>
  );
}


================================================
FILE: src/components/Mui/Dialog/Dialog.scss
================================================
.mui-dialog-paper.mui-dialog-paper {
  @include dimen(100%);
  @include margin-x(25px);
  color: var(--text--color);
  background-color: var(--main-color-diff2);
  border-radius: 8px;
  max-width: 280px;
  padding: 0;

  [data-theme^='light'] & {
    box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14),
      0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2);
  }
  [data-theme^='dark'] & {
    box-shadow: 0px 0px 3px 1px var(--shadow-color);
  }

  .dialog-scroll-content {
    max-height: 100%;
    padding: 15px 20px;
  }

  .dialog-title {
    @include typeface('Nunito Sans', 700);
    color: var(--text-color);
    margin-bottom: 10px;
  }

  .dialog-actions {
    @include flex(center, flex-end);
    flex: 0 0 auto;
    margin-top: 10px;

    button {
      @include typeface('Nunito Sans', 700);

      background: none;
      text-transform: initial;

      + button {
        color: var(--accent-dark-color);
        margin-left: 10px;
      }
    }
  }
}

.form-dialog-error-message {
  @include typeface('Nunito Sans', 600);
  color: var(--error-color);
  font-size: 13px;
  margin-top: 3px;
  padding-left: 3px;
}

.fullscreen-dialog-paper.fullscreen-dialog-paper {
  background-color: var(--main-color);
  color: var(--text-color);
}

.fullscreen-diaglog-header {
  @include app-region-drag;
  @include dimen(100%, 70px);
  @include flex(center, space-between);
  @include fake-border($borderWidth: 1px, $color: var(--main-color-diff));
  @include padding-x(15px 10px);
  flex: 0 0 auto;

  .mui-icon-button {
    @include app-region-no-drag;
  }

  h4 {
    @include margin-y(0 15px);
    color: var(--text-color);
    font-size: 18px;
    font-weight: 500;
    margin-bottom: 0;
  }

  [data-platform^='darwin'] & {
    height: 70px;
    padding-bottom: 5px;

    h4 {
      margin-bottom: 7.5px;
      align-self: flex-end;
    }
  }
}

.fullscreen-dialog-content {
  $padding-x: 15px;
  @include flex();
  @include sq-dimen(100%);
  overflow: auto;

  .fullscreen-dialog-inner-content {
    @include dimen(100%);
    @include padding-x($padding-x);
  }

  .fullscreen-dialog-section {
    .fullscreen-dialog-section-title {
      @include margin-x(-$padding-x);
      @include padding-x($padding-x);
      @include padding-y(12px);
      background-color: var(--main-color-diff);
      border-top: 1px solid var(--border-color);
      border-bottom: 1px solid var(--border-color);
      font-weight: 500;
      font-size: 15px;
    }

    .fullscreen-dialog-row {
      @include flex(center, space-between);
      @include padding-y(12px);
      min-height: 50px;

      + .fullscreen-dialog-row {
        border-top: 1px solid var(--border-color);
      }
    }
  }
}


================================================
FILE: src/components/Mui/Dialog/FormDialog.tsx
================================================
import React, { useCallback, useState, useRef, FormEvent } from 'react';
import { ConfirmDialog, ConfirmDialogProps } from './ConfirmDialog';
import { Input } from '../Input';

interface Props extends Omit<ConfirmDialogProps, 'onConfirm' | 'confirmLabel'> {
  defaultValue?: string;
  errorMsg?: string;
  onConfirm(payload: string): void;
}

const INPUT_NAME = 'NAME';

export function FormDialog({
  defaultValue = '',
  errorMsg,
  onClose,
  onConfirm,
  ...props
}: Props) {
  const formRef = useRef<HTMLFormElement>(null);
  const [error, setError] = useState(false);

  const submitCallback = useCallback(
    (evt?: FormEvent<HTMLFormElement>) => {
      evt && evt.preventDefault();

      const formEl = formRef.current;
      if (formEl) {
        const formData = new FormData(formEl);
        const trimedValue = (formData.get(INPUT_NAME) as string).trim();

        if (!trimedValue) {
          setError(true);
          return false;
        } else if (trimedValue !== defaultValue) {
          onConfirm(trimedValue);
          onClose();
        }
      }
    },
    [onClose, onConfirm, defaultValue]
  );

  const onExitedCallback = useCallback(() => {
    setError(false);
  }, []);

  return (
    <ConfirmDialog
      {...props}
      autoFocusConfirmButon={false}
      confirmLabel="Done"
      onConfirm={submitCallback}
      onClose={onClose}
      onExited={onExitedCallback}
    >
      <form onSubmit={submitCallback} ref={formRef}>
        <Input
          autoFocus
          className="filled bottom-border"
          defaultValue={defaultValue}
          error={error}
          name={INPUT_NAME}
          placeholder="Enter name"
        />
        <div className="form-dialog-error-message">{error && errorMsg}</div>
      </form>
    </ConfirmDialog>
  );
}


================================================
FILE: src/components/Mui/Dialog/FullScreenDialog.tsx
================================================
import React, { ReactNode } from 'react';
import Dialog, { DialogProps } from '@material-ui/core/Dialog';
import { SlideProps } from '@material-ui/core/Slide';
import { IconButton } from '../IconButton';
import Slide from '@material-ui/core/Slide';
import CloseIcon from '@material-ui/icons/Close';

export interface FullScreenDialogProps extends Omit<DialogProps, 'title'> {
  title?: string;
  headerComponents?: ReactNode;
  onClose(): void;
}

interface ContainerProps {
  children?: ReactNode;
}

export const FULLSCREEN_DIALOG_TRANSITION = 300;

const Transition = React.forwardRef<unknown, SlideProps>((props, ref) => {
  return <Slide direction="left" ref={ref} {...props} />;
});

const backdropProps = {
  open: false
};

const paperClasses = { paper: 'fullscreen-dialog-paper' };

export function FullScreenDialog({
  children,
  title,
  onClose,
  headerComponents,
  ...props
}: FullScreenDialogProps) {
  return (
    <Dialog
      {...props}
      fullScreen
      BackdropProps={backdropProps}
      classes={paperClasses}
      onClose={onClose}
      transitionDuration={FULLSCREEN_DIALOG_TRANSITION}
      TransitionComponent={Transition}
    >
      <div className="fullscreen-diaglog-header">
        <h4>{title}</h4>
        <div>
          {headerComponents}
          <IconButton tooltip="Close task" icon={CloseIcon} onClick={onClose} />
        </div>
      </div>
      <div className="fullscreen-dialog-content">
        <div className="fullscreen-dialog-inner-content">{children}</div>
      </div>
    </Dialog>
  );
}

FullScreenDialog.Section = ({ children }: ContainerProps) => {
  return <div className="fullscreen-dialog-section">{children}</div>;
};

FullScreenDialog.Title = ({ children }: ContainerProps) => {
  return <div className="fullscreen-dialog-section-title">{children}</div>;
};

FullScreenDialog.Row = ({ children }: ContainerProps) => {
  return <div className="fullscreen-dialog-row">{children}</div>;
};

export default FullScreenDialog;


================================================
FILE: src/components/Mui/Dialog/index.ts
================================================
import './Dialog.scss';

export * from './ConfirmDialog';
export * from './FormDialog';
export * from './FullScreenDialog';


================================================
FILE: src/components/Mui/Dropdown/Dropdown.scss
================================================
.mui-dropdown-button {
  &.mui-dropdown-button {
    @include flex(center, space-between);
    font-size: inherit;
    text-transform: initial;
    padding: 0 0 0 10px;

    > span:nth-child(1) {
      div {
        @include text-overflow-ellipsis();
      }
    }
  }
}


================================================
FILE: src/components/Mui/Dropdown/Dropdown.tsx
================================================
import React, { useMemo, ReactNode, MouseEvent, forwardRef } from 'react';
import { Menu, MenuProps } from '../Menu';
import Button, { ButtonProps } from '@material-ui/core/Button';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDownRounded';
import mergeWith from 'lodash/mergeWith';

export interface DropdownProps extends Omit<MenuProps, 'onClick' | 'ref'> {
  label?: string;
  onClick(evt: MouseEvent<HTMLButtonElement>): void;
  children?: ReactNode;
  buttonProps?: ButtonProps;
}

export const Dropdown = forwardRef<HTMLButtonElement, DropdownProps>(
  ({ buttonProps, children, classes, label, onClick, ...props }, ref) => {
    const mergedMenuClasses = useMemo<MenuProps['classes']>(
      () =>
        mergeWith(
          { paper: 'dropdown-menu-paper' },
          classes,
          (a, b) => a + ' ' + b
        ),
      [classes]
    );

    const { classes: buttonClasses, ...otherButtonProps } = buttonProps || {
      classes: ''
    };

    const mergedButtonClasses = useMemo<ButtonProps['classes']>(
      () =>
        mergeWith(
          { root: 'mui-dropdown-button' },
          buttonClasses,
          (a, b) => a + ' ' + b
        ),
      [buttonClasses]
    );

    return (
      <>
        <Button
          ref={ref}
          classes={mergedButtonClasses}
          onClick={onClick}
          disableFocusRipple
          {...otherButtonProps}
        >
          <div>{label}</div>
          <ArrowDropDownIcon fontSize="default" />
        </Button>
        <Menu {...props} classes={mergedMenuClasses}>
          {children}
        </Menu>
      </>
    );
  }
);


================================================
FILE: src/components/Mui/Dropdown/index.ts
================================================
import './Dropdown.scss';

export * from './Dropdown';
export { Dropdown as default } from './Dropdown';


================================================
FILE: src/components/Mui/EditIcon.tsx
================================================
import React from 'react';
import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';

export const EditIcon = React.forwardRef<SVGSVGElement, SvgIconProps>(
  (props, ref) => {
    return (
      <SvgIcon {...props} ref={ref}>
        <path d="M20.41 4.94l-1.35-1.35c-.78-.78-2.05-.78-2.83 0L3 16.82V21h4.18L20.41 7.77c.79-.78.79-2.05 0-2.83zm-14 14.12L5 19v-1.36l9.82-9.82 1.41 1.41-9.82 9.83z" />
      </SvgIcon>
    );
  }
);


================================================
FILE: src/components/Mui/IconButton/IconButton.scss
================================================
.mui-icon-button.mui-icon-button {
  color: var(--text-secondary-color);
}


================================================
FILE: src/components/Mui/IconButton/IconButton.tsx
================================================
import React, { ComponentType, ReactElement } from 'react';
import { SvgIconProps } from '@material-ui/core/SvgIcon';
import MuiIconButton, { IconButtonProps } from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip';

interface Props extends Partial<IconButtonProps> {
  tooltip?: string;
  icon?: ComponentType<Omit<SvgIconProps, 'ref'>>;
  iconProps?: SvgIconProps;
  children?: ReactElement<{}>;
}

const PopperProps = {
  popperOptions: {
    modifiers: {
      offset: {
        fn: (data: any) => {
          data.offsets.reference.top = data.offsets.reference.top + 75;
          return data;
        }
      }
    }
  }
};

export function IconButton({
  className = '',
  tooltip = '',
  icon: Icon,
  iconProps,
  children,
  ...props
}: Props) {
  return (
    <MuiIconButton className={`mui-icon-button ${className}`.trim()} {...props}>
      <Tooltip title={tooltip} PopperProps={PopperProps}>
        {Icon ? <Icon {...iconProps} /> : children ? children : <div />}
      </Tooltip>
    </MuiIconButton>
  );
}


================================================
FILE: src/components/Mui/IconButton/index.ts
================================================
import './IconButton.scss';

export * from './IconButton';
export { IconButton as default } from './IconButton';


================================================
FILE: src/components/Mui/Input/Input.scss
================================================
.mui-input-base.mui-input-base {
  font-size: inherit;
  min-height: 40px;
  padding: 1px 0px 0px 10px;

  input,
  textarea {
    color: var(--text-color);
    outline: none;
    overflow: hidden;

    &:hover:not(:focus) {
      cursor: default;
    }
  }

  &.filled {
    @include textHighlight();
    background-color: var(--task-highlight-background-color);
  }

  &.bottom-border {
    border-bottom: 1px solid var(--border-color);
  }

  &.error {
    &:after {
      border-color: var(--error-color) !important;
    }
  }
}


================================================
FILE: src/components/Mui/Input/Input.tsx
================================================
import React, { useMemo } from 'react';
import InputBase, { InputBaseProps } from '@material-ui/core/InputBase';

export type InputProps = Omit<InputBaseProps, 'ref'>;

export function Input({ className = '', ...props }: InputProps) {
  const mergedClasses = useMemo<InputBaseProps['classes']>(
    () => ({
      root: `mui-input-base ${className}`.trim(),
      focused: 'focused',
      multiline: 'multiline',
      error: 'error'
    }),
    [className]
  );

  return <InputBase {...props} fullWidth classes={mergedClasses} />;
}


================================================
FILE: src/components/Mui/Input/index.ts
================================================
import './Input.scss';

export * from './Input';
export { Input as default } from './Input';


================================================
FILE: src/components/Mui/Menu/Menu.scss
================================================
.mui-menu-paper {
  &.mui-menu-paper {
    border-radius: 8px;
    background-color: var(--paper-background-color);
    // prettier-ignore
    box-shadow: 
      0 0 0 1px  var(--shadow-color),
      0 2px 4px var(--shadow-color), 
      0 8px 24px var(--shadow-color);
    color: var(--text-color);
  }

  .menu-content {
    @include padding-y(8px);
  }
}

.mui-menu-item {
  &.mui-menu-item {
    @include padding-y(8px);
    color: var(--text-color);
    font-size: inherit;
    min-height: 0;

    &:hover {
      background-color: var(--menu-item-hover-color);
    }
  }

  > div {
    @include flex(center, space-between);
    flex: 1 1 auto;
    max-width: 100%;

    .text {
      @include text-overflow-ellipsis();
      flex: 1 1 auto;
      padding-right: 25px;
    }
  }
}


================================================
FILE: src/components/Mui/Menu/Menu.tsx
================================================
import React, { useMemo, ReactNode } from 'react';
import Popover, { PopoverProps } from '@material-ui/core/Popover';
import mergeWith from 'lodash/mergeWith';

export interface MenuProps extends PopoverProps {
  onClose(): void;
  footer?: ReactNode;
}

const backdropProps = {
  classes: { root: 'mui-menu-backdrop' },
  invisible: true
};

export const Menu = ({ classes, children, footer, ...props }: MenuProps) => {
  const mergedClasses = useMemo<PopoverProps['classes']>(
    () =>
      mergeWith({ paper: 'mui-menu-paper' }, classes, (a, b) => a + ' ' + b),
    [classes]
  );

  return (
    <Popover classes={mergedClasses} BackdropProps={backdropProps} {...props}>
      <div className="menu-content">
        <div className="scroll-content">{children}</div>
        {footer}
      </div>
    </Popover>
  );
};


================================================
FILE: src/components/Mui/Menu/MenuItem.tsx
================================================
import React, { forwardRef, useCallback, MouseEvent } from 'react';
import MuiMenuItem from '@material-ui/core/MenuItem';
import TickIcon from '@material-ui/icons/Check';

type DefaultMenuItemProps = Parameters<typeof MuiMenuItem>[0];

export interface MenuItemProps extends DefaultMenuItemProps {
  text?: string;
}

interface Props {
  onClose(): void;
}

const menuItemClasses = { root: 'mui-menu-item' };

export const MenuItem = forwardRef<HTMLLIElement, Props & MenuItemProps>(
  ({ children, text, onClick, onClose, selected, ...props }, ref) => {
    const onClickCallback = useCallback(
      (evt: MouseEvent<HTMLLIElement>) => {
        onClose();
        onClick && onClick(evt);
      },
      [onClose, onClick]
    );

    return (
      <MuiMenuItem
        classes={menuItemClasses}
        onClick={onClickCallback}
        ref={ref}
        {...props}
      >
        <div>
          <div className="text">{text}</div>
          {selected && <TickIcon />}
        </div>
        {children}
      </MuiMenuItem>
    );
  }
);

export function useMuiMenuItem({ onClose }: Props) {
  // eslint-disable-next-line
  return useCallback(
    forwardRef<HTMLLIElement, MenuItemProps>((props, ref) => (
      <MenuItem onClose={onClose} {...props} ref={ref} />
    )),
    [onClose]
  );
}


================================================
FILE: src/components/Mui/Menu/index.ts
================================================
import './Menu.scss';

export * from './Menu';
export * from './MenuItem';
export * from './useMuiMenu';
export { Menu as default } from './Menu';


================================================
FILE: src/components/Mui/Menu/useMuiMenu.ts
================================================
import {
  useState,
  useCallback,
  useEffect,
  SyntheticEvent,
  MouseEvent
} from 'react';
import { MenuProps } from '@material-ui/core/Menu';

export type AnchorEl = HTMLElement | null;
export type AnchorPosition = MenuProps['anchorPosition'];

function instanceOfAnchorEl(object: any): object is AnchorEl {
  return object === null || object instanceof HTMLElement;
}

function instanceOfAnchorPosition(object: any): object is AnchorPosition {
  return typeof object === 'undefined' || (object.top && object.left);
}

export function useMuiMenu() {
  const [anchorEl, setAnchorEl] = useState<AnchorEl>(null);
  const [anchorPosition, setAnchorPosition] = useState<AnchorPosition>();
  const onClose = useCallback(() => {
    setAnchorEl(null);
    setAnchorPosition(undefined);
  }, []);

  useEffect(() => {
    setAnchorEl(anchorEl);
  }, [anchorEl]);

  useEffect(() => {
    setAnchorPosition(anchorPosition);
  }, [anchorPosition]);

  const setAnchorElCallback = useCallback(
    (props: SyntheticEvent<HTMLElement> | AnchorEl) => {
      if (instanceOfAnchorEl(props)) {
        setAnchorEl(props);
      } else {
        setAnchorEl(props.currentTarget);
      }
    },
    []
  );

  const setAnchorPositionCallback = useCallback(
    (props: MouseEvent<HTMLElement> | AnchorPosition) => {
      if (instanceOfAnchorPosition(props)) {
        setAnchorPosition(props);
      } else {
        const evt = props;
        evt && evt.preventDefault();
        setAnchorPosition({
          top: evt.pageY,
          left: evt.pageX
        });
      }
    },
    []
  );

  return {
    anchorEl,
    anchorPosition,
    setAnchorEl: setAnchorElCallback,
    setAnchorPosition: setAnchorPositionCallback,
    onClose
  };
}


================================================
FILE: src/components/Mui/Tooltip.tsx
================================================
import React from 'react';
import { makeStyles, Theme } from '@material-ui/core/styles';
import MuiTooltip, {
  TooltipProps as MuiTooltipProps
} from '@material-ui/core/Tooltip';

export type TooltipProps = Omit<MuiTooltipProps, 'ref'>;

const fontSize = 14;
const PopperProps: TooltipProps['PopperProps'] = { disablePortal: true };

const useStyles = makeStyles((theme: Theme) => ({
  tooltip: {
    fontSize
  }
}));

const useErrorStyles = makeStyles((theme: Theme) => ({
  arrow: {
    color: theme.palette.error.main
  },
  tooltip: {
    fontSize,
    color: theme.palette.error.contrastText,
    backgroundColor: theme.palette.error.main
  }
}));

export function Tooltip(props: TooltipProps) {
  const classes = useStyles();
  return (
    <MuiTooltip {...props} classes={classes} PopperProps={PopperProps} arrow />
  );
}

export function ErrorTooltip(props: TooltipProps) {
  const classes = useErrorStyles();
  return (
    <MuiTooltip {...props} classes={classes} PopperProps={PopperProps} arrow />
  );
}


================================================
FILE: src/components/Mui/index.ts
================================================
export * from './DeleteIcon';
export * from './Dialog';
export * from './Dropdown';
export * from './EditIcon';
export * from './IconButton';
export * from './Input';
export * from './Menu';
export * from './Tooltip';


================================================
FILE: src/components/Preferences/AccentColor.tsx
================================================
import React from 'react';
import { FullScreenDialog } from '../Mui/Dialog/FullScreenDialog';
import { Control } from '../../utils/form';

const accentColors: AccentColor[] = [
  'blue',
  'purple',
  'red',
  'amber',
  'green',
  'grey'
];

export function AccentColor({ onChange }: Control) {
  return (
    <FullScreenDialog.Row>
      <div className="preferences-label">Accent color</div>
      <div className="preferences-accent-color-selector">
        {accentColors.map((color, index) => (
          <div
            key={index}
            className={color}
            onClick={() => onChange && onChange(color)}
          />
        ))}
      </div>
    </FullScreenDialog.Row>
  );
}


================================================
FILE: src/components/Preferences/Preferences.scss
================================================
.preferences {
  color: var(--text-color);

  input[type='number']::-webkit-inner-spin-button,
  input[type='number']::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
  }

  .preferences-label {
    @include flex(center);
    @include typeface('Nunito Sans', 600);
    flex: 0 0 auto;
    padding-right: 10px;

    svg {
      margin-left: 3px;
    }
  }

  .preferences-theme-selector {
    @include flex(center);
  }

  .preferences-theme {
    text-align: center;
    font-size: 12px;
    line-height: 1.5em;

    > div {
      @include dimen(70px, 45px);
      @include relative();

      border-radius: 5px;
      border: 2px solid transparent;
      cursor: pointer;
      overflow: hidden;

      &:after {
        $size: 7px;
        @include absolute(6px, null, 6px);
        @include sq-dimen($size);

        content: '';
        background: #ff5f57;
        border-radius: 50%;
        box-shadow: $size * 1.5 0 0 #ffbd2e, $size * 3 0 0 #28ca41;
      }

      &:before {
        @include absolute(0, null, 0);
        @include sq-dimen(100%);
        border-radius: 3px;
        content: '';
      }
    }

    &.light {
      &:after {
        content: 'Light';
      }

      > div {
        [data-theme^='light'] & {
          border-color: var(--accent-color);
        }

        &:before {
          background-color: #eee;
        }
      }
    }

    &.dark {
      margin-left: 20px;

      &:after {
        content: 'Dark';
      }

      > div {
        [data-theme^='dark'] & {
          border-color: var(--accent-color);
        }

        &:before {
          background-color: #232323;
        }
      }
    }
  }

  .preferences-accent-color-selector {
    @include flex(center);

    > div {
      @include sq-dimen(20px);
      @include relative();

      border-radius: 50%;
      cursor: pointer;
      overflow: hidden;

      &:nth-child(n + 2) {
        margin-left: 10px;
      }
    }

    @each $name, $colors in $accent-colors {
      .#{'' + $name} {
        background-color: map-get($colors, primary);
        box-shadow: 2px 2px 10px -3px map-get($colors, dark);

        &:after {
          @include absolute();
          @include sq-dimen(100%);
          content: '';
        }

        [data-accent-color^='#{"" + $name}'] & {
          &:before {
            @include absolute(0px, 0, 0px, 0);
            @include sq-dimen(8px);

            content: '';
            border-radius: 50%;
            background-color: #fff;
            margin: auto;
          }
        }
      }
    }
  }

  .preferences-hours,
  .preferences-max-tasks-selector {
    .mui-input-base {
      @include padding-y(0);
      @include padding-x(10px);
      margin-right: 5px;
      max-width: 4em;
      min-height: 30px;

      input {
        text-align: center;
      }
    }
  }

  .preferences-max-tasks-selector {
    .mui-input-base {
      max-width: 6em;
    }
  }

  .preferences-hours {
    .mui-input-base {
      max-width: 4em;
    }
  }
}


================================================
FILE: src/components/Preferences/Preferences.tsx
================================================
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import {
  Input,
  ErrorTooltip,
  FullScreenDialog,
  FullScreenDialogProps
} from '../Mui';
import { Switch } from '../Switch';
import { ThemeSelector } from './ThemeSelector';
import { AccentColor } from './AccentColor';
import { TitleBarSelector } from './TitleBarSelector';
import { Storage } from './Storage';
import { preferencesSelector, usePreferenceActions } from '../../store';
import { createForm, validators } from '../../utils/form';

const { Form, FormItem, useForm } = createForm<Schema$Preferences>();

const normalizeNumber = (value: string) => {
  const num = Number(value);
  return value === '' ||
    value === '-0' ||
    isNaN(num) ||
    /^0\d+/.test(value) ||
    /\.(0+)?$/.test(value)
    ? String(value)
    : num;
};

export function Preferences(props: FullScreenDialogProps) {
  const preferences = useSelector(preferencesSelector);
  const [form] = useForm();
  const [errors, setErrors] = useState<string[]>([]);
  const { updatePreferences } = usePreferenceActions();

  return (
    <FullScreenDialog {...props} className="preferences" title="Preferences">
      <Form
        form={form}
        initialValues={preferences}
        onValuesChange={changes => {
          setTimeout(() => {
            form
              .validateFields()
              .then(() => updatePreferences(changes))
              .catch(() => {
                setErrors(
                  form
                    .getFieldsError(['maxTasks', ['sync', 'inactiveHours']])
                    .map(payload => payload.errors[0])
                );
              });
          }, 0);
        }}
      >
        <FullScreenDialog.Section>
          <FullScreenDialog.Title children="Appearances" />

          <FormItem name="theme" noStyle>
            <ThemeSelector />
          </FormItem>

          <FormItem name="accentColor" noStyle>
            <AccentColor />
          </FormItem>

          {window.platform === 'darwin' ? null : (
            <FormItem name="titleBar" noStyle>
              <TitleBarSelector />
            </FormItem>
          )}
        </FullScreenDialog.Section>

        <FullScreenDialog.Section>
          <FullScreenDialog.Title children="Tasks" />

          <FullScreenDialog.Row>
            <div className="preferences-label">Maximum Tasks</div>
            <div className="preferences-max-tasks-selector">
              <ErrorTooltip
                enterDelay={0}
                placement="top"
                title={errors[0] || ''}
                open={!!errors[0]}
              >
                <span>
                  <FormItem
                    name="maxTasks"
                    normalize={normalizeNumber}
                    validators={[
                      validators.required('Cannot be empty'),
                      validators.number,
                      validators.integer('Plase input integer only'),
                      validators.min(0, 'Cannot less then 1')
                    ]}
                    noStyle
                  >
                    <Input className="filled" />
                  </FormItem>
                </span>
              </ErrorTooltip>
            </div>
          </FullScreenDialog.Row>

          <FullScreenDialog.Row>
            <div className="preferences-label">Enable synchronization</div>
            <div className="preferences-switch">
              <FormItem
                name={['sync', 'enabled']}
                valuePropName="checked"
                noStyle
              >
                <Switch />
              </FormItem>
            </div>
          </FullScreenDialog.Row>

          <FormItem deps={[['sync', 'enabled']]} noStyle>
            {({ sync }) => {
              if (sync.enabled) {
                return (
                  <>
                    <FullScreenDialog.Row>
                      <div className="preferences-label">
                        Sync on reconnection
                      </div>
                      <div className="preferences-hours">
                        <FormItem
                          name={['sync', 'reconnection']}
                          valuePropName="checked"
                          noStyle
                        >
                          <Switch />
                        </FormItem>
                      </div>
                    </FullScreenDialog.Row>

                    <FullScreenDialog.Row>
                      <div className="preferences-label">
                        Sync after inactive
                      </div>
                      <div className="preferences-hours">
                        <ErrorTooltip
                          enterDelay={0}
                          placement="top"
                          title={errors[1] || ''}
                          open={!!errors[1]}
                        >
                          <span>
                            <FormItem
                              name={['sync', 'inactiveHours']}
                              normalize={normalizeNumber}
                              validators={[
                                validators.required('Cannot be empty'),
                                validators.number,
                                validators.integer('Plase input integer only'),
                                validators.min(1, 'Cannot less then 1')
                              ]}
                              noStyle
                            >
                              <Input className="filled" />
                            </FormItem>
                          </span>
                        </ErrorTooltip>
                        Hours
                      </div>
                    </FullScreenDialog.Row>
                  </>
                );
              }
              return <div />;
            }}
          </FormItem>
        </FullScreenDialog.Section>

        <Storage />
      </Form>
    </FullScreenDialog>
  );
}


================================================
FILE: src/components/Preferences/Storage.tsx
================================================
import React from 'react';
import { Input, FullScreenDialog } from '../Mui';

export function Storage() {
  return (
    <FullScreenDialog.Section>
      <FullScreenDialog.Title children="Storage ( Read-Only )" />
      <FullScreenDialog.Row>
        <div className="preferences-label">Path</div>
        <Input value={window.STORAGE_DIRECTORY} readOnly className="filled" />
      </FullScreenDialog.Row>
    </FullScreenDialog.Section>
  );
}


================================================
FILE: src/components/Preferences/ThemeSelector.tsx
================================================
import React from 'react';
import { FullScreenDialog } from '../Mui/Dialog/FullScreenDialog';
import { Control } from '../../utils/form';

export function ThemeSelector({ value, onChange }: Control<Theme>) {
  const handleChange = onChange || (() => {});
  return (
    <FullScreenDialog.Row>
      <div className="preferences-label">Theme</div>
      <div className="preferences-theme-selector">
        <div className="preferences-theme light">
          <div onClick={() => handleChange('light')} />
        </div>
        <div className="preferences-theme dark">
          <div onClick={() => handleChange('dark')} />
        </div>
      </div>
    </FullScreenDialog.Row>
  );
}


================================================
FILE: src/components/Preferences/TitleBarSelector.tsx
================================================
import React, { useState } from 'react';
import { FullScreenDialog, ConfirmDialog } from '../Mui';
import { Dropdown, MenuItem, useMuiMenu } from '../Mui';
import { Control } from '../../utils/form';

const exlucded: Array<Window['platform']> = ['darwin', 'win32'];
const shouldRelaunch = !exlucded.includes(window.platform);

export function TitleBarSelector({ value, onChange }: Control<TitleBar>) {
  const { anchorEl, setAnchorEl, onClose } = useMuiMenu();
  const [change, setChange] = useState<TitleBar | null>(null);
  const [shouldClose, setShoudClose] = useState(false);
  const handleChange = onChange || (() => {});
  const hadnleSelect = shouldRelaunch ? setChange : handleChange;

  return (
    <FullScreenDialog.Row>
      <div className="preferences-label">Title Bar</div>
      <div className="preferences-title-bar-selector">
        <Dropdown
          open={!!anchorEl}
          anchorEl={anchorEl}
          onClick={setAnchorEl}
          onClose={onClose}
          label={value?.replace(/^./, match => match.toUpperCase())}
        >
          <MenuItem
            text="Native"
            selected={value === 'native'}
            onClick={() => hadnleSelect('native')}
            onClose={onClose}
          />
          <MenuItem
            text="Frameless"
            selected={value === 'frameless'}
            onClick={() => hadnleSelect('frameless')}
            onClose={onClose}
          />
        </Dropdown>
        {shouldRelaunch && (
          <ConfirmDialog
            title="Notice"
            open={!!change}
            onClose={() => setChange(null)}
            onConfirm={() => {
              if (change && change !== value) {
                handleChange(change);
                setShoudClose(true);
              }
            }}
            onExited={() => shouldClose && window.relaunch()}
          >
            This will relaunch your application and the changes to take effect
            after relaunch
          </ConfirmDialog>
        )}
      </div>
    </FullScreenDialog.Row>
  );
}


================================================
FILE: src/components/Preferences/index.ts
================================================
import './Preferences.scss';

export * from './Preferences';
export { Preferences as default } from './Preferences';


================================================
FILE: src/components/PrivateRoute.tsx
================================================
import React from 'react';
import { Route, Redirect, RouteProps } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { RootState } from '../store';
import { PATHS } from '../constants';

export function PrivateRoute(props: RouteProps) {
  const loggedIn = useSelector((state: RootState) => state.auth.loggedIn);
  return loggedIn ? <Route {...props} /> : <Redirect to={PATHS.AUTH} />;
}


================================================
FILE: src/components/Switch/Switch.scss
================================================
$defaultWidth: 47.5px;

@mixin shadow($x) {
  box-shadow: $x 0.5px 3px 0px lighten(#000, 50%);
}

.switch {
  @include dimen($defaultWidth);
  @include relative();

  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: rgba(255, 255, 255, 0);

  .switch-bar {
    @include animate(background-color);
    @include dimen(100%, 0);
    @include relative();
    background-color: var(--border-color);
    border-radius: 50px;
    padding-bottom: 50%;
  }

  .switch-icon {
    @include animate(transform);
    @include absolute(-0.4px);
    @include dimen(50%, 100%);
    padding: round(percentage(2.5 / strip-unit($defaultWidth)));
    transform: translateX(0);

    &:before {
      @include sq-dimen(100%);
      @include shadow(1px);
      content: '';
      background-color: #fff;
      border-radius: 50%;
      display: block;
    }
  }

  &.checked {
    .switch-bar {
      background-color: var(--accent-color);
    }
    .switch-icon {
      transform: translateX(100%);
      &:before {
        @include shadow(-1px);
      }
    }
  }
}


================================================
FILE: src/components/Switch/Switch.tsx
================================================
import React, { useMemo, useCallback, CSSProperties } from 'react';

interface Props {
  checked?: boolean;
  onChange?(checked: boolean): void;
  width?: number;
}

export const Switch = React.memo<Props>(
  ({ checked = false, width, onChange }) => {
    const style = useMemo<CSSProperties>(() => ({ width }), [width]);

    const onClick = useCallback(() => {
      onChange && onChange(!checked);
    }, [onChange, checked]);

    return (
      <div
        className={['switch', checked && 'checked'].filter(Boolean).join(' ')}
        onClick={onClick}
        style={style}
      >
        <div className="switch-bar" />
        <div className="switch-icon" />
      </div>
    );
  }
);


================================================
FILE: src/components/Switch/index.ts
================================================
import './Switch.scss';

export * from './Switch';
export { Switch as default } from './Switch';


================================================
FILE: src/constants/index.ts
================================================
export { default as PATHS } from './paths.json';


================================================
FILE: src/constants/paths.json
================================================
{
  "AUTH": "/auth",
  "TASKLIST": "/tasklist/:taskListId?"
}


================================================
FILE: src/date.d.ts
================================================
type DayCn = '日' | '一' | '二' | '三' | '四' | '五' | '六';
type MonthFullName =
  | 'January'
  | 'February'
  | 'March'
  | 'April'
  | 'May'
  | 'June'
  | 'July'
  | 'August'
  | 'September'
  | 'October'
  | 'November'
  | 'December';

type MonthAbbr =
  | 'Jan'
  | 'Feb'
  | 'Mar'
  | 'Apr'
  | 'May'
  | 'Jun'
  | 'Jul'
  | 'Aug'
  | 'Sep'
  | 'Oct'
  | 'Nov'
  | 'Dec';

type DayFullName =
  | 'Sunday'
  | 'Monday'
  | 'Tuesday'
  | 'Wednesday'
  | 'Thursday'
  | 'Friday'
  | 'Saturday';

type DayAbbr = 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thur' | 'Fri' | 'Sat';

type DaySuffix = 'th' | 'st' | 'nd' | 'rd';

interface Date {
  getDayCn(): DayCn;

  addDays: (n: number) => Date;

  addMonths: (n: number) => Date;

  getMonthName(): MonthFullName;

  getMonthAbbr(): MonthAbbr;

  getDayFull(): DayFullName;

  getDayAbbr(): DayAbbr;

  getDayOfYear(): number;

  getDaySuffix(): DaySuffix;

  getWeekOfYear(): number;

  isLeapYear(): boolean;

  getMonthDayCount(): number;

  compare(
    d: Date
  ): {
    sameYear: boolean;
    sameDate: boolean;
    sameMonth: boolean;
    lastMonth: boolean;
    nextMonth: boolean;
  };

  isToday(): boolean;

  dayDiff(d?: Date): number;

  toISODateString(): string;

  format(dateFormat: string): string;
}


================================================
FILE: src/hooks/crud-reducer/bindDispatch.ts
================================================
import { Dispatch } from 'react';
import { ActionCreators } from './crudAction';

export type Dispatched<A extends ActionCreators> = {
  [X in keyof A]: (...args: Parameters<A[X]>) => void;
};

export function bindDispatch<A extends ActionCreators>(
  creators: A,
  dispatch: Dispatch<any>
) {
  const handler = {} as Dispatched<A>;
  for (const key in creators) {
    const creator = creators[key];
    handler[key] = (...args: Parameters<typeof creator>) => {
      dispatch(creator(...args));
    };
  }

  return handler;
}


================================================
FILE: src/hooks/crud-reducer/crudAction.ts
================================================
/* eslint-disable @typescript-eslint/ban-types */

// https://medium.com/dailyjs/typescript-create-a-condition-based-subset-types-9d902cea5b8c
export type FilterFlags<Base, Condition> = {
  [Key in keyof Base]: Base[Key] extends Condition ? Key : never;
};

export type AllowedNames<Base, Condition> = FilterFlags<
  Base,
  Condition
>[keyof Base];

export type Key<I> = AllowedNames<I, string>;

export interface AnyAction {
  type: string;
  [extraProps: string]: any;
}

export interface ActionCreators {
  [k: string]: (...args: any[]) => AnyAction;
}

export type GetCreatorsAction<
  T extends Record<string, (...args: any[]) => any>
> = ReturnType<T[keyof T]>;

export type CRUDActionType =
  | 'LIST'
  | 'CREATE'
  | 'UPDATE'
  | 'DELETE'
  | 'PAGINATE'
  | 'PARAMS'
  | 'RESET';

export type CRUDActionTypes<Type extends string = any> = {
  [K in CRUDActionType]?: Type;
};

export type CustomActionTypes<
  M extends Partial<CRUDActionTypes> = CRUDActionTypes
> = M & Omit<typeof DefaultCRUDActionTypes, keyof M>;

export type UpdatePayload<I, K extends Key<I>> = Partial<I> &
  { [T in K]: string };

export type PaginatePayload<I> =
  | I[]
  | {
      data: I[];
      total: number;
      pageNo: number;
      pageSize?: number;
    };

export type List<Type extends string, I> = {
  type: Type;
  payload: I[];
};

export type Create<Type extends string, I> = {
  type: Type;
  payload: I;
};

export interface Update<Type extends string, I, K extends Key<I>> {
  type: Type;
  payload: UpdatePayload<I, K>;
}

export interface Delete<Type extends string, I, K extends Key<I>> {
  type: Type;
  payload: { [T in K]: string };
}

export interface Paginate<Type extends string, I> {
  type: Type;
  payload: PaginatePayload<I>;
}

export interface Params<Type extends string> {
  type: Type;
  payload: Record<string, any>;
}

export interface Reset<Type extends string> {
  type: Type;
}

export type CRUDActionCreators<
  I,
  K extends Key<I>,
  M extends CRUDActionTypes = CRUDActionTypes
> = {
  list: (payload: I[]) => List<M['LIST'], I>;
  create: (payload: I) => Create<M['CREATE'], I>;
  update: (
    payload: Update<string, I, K>['payload']
  ) => Update<M['UPDATE'], I, K>;
  delete: (payload: { [T in K]: string }) => Delete<M['DELETE'], I, K>;
  paginate: (payload: PaginatePayload<I>) => Paginate<M['PAGINATE'], I>;
  params: (payload: Record<string, any>) => Params<M['PARAMS']>;
  reset: () => Reset<M['RESET']>;
};

export type CRUDActions<
  I,
  K extends Key<I>,
  M extends CRUDActionTypes = CRUDActionTypes
> = GetCreatorsAction<CRUDActionCreators<I, K, M>>;

export type ExtractAction<
  CustomActionTypes extends AnyAction,
  T2 extends CustomActionTypes['type']
> = CustomActionTypes extends { type: T2 } ? CustomActionTypes : never;

export function isAction<
  I,
  K extends Key<I>,
  M extends CRUDActionTypes = CRUDActionTypes,
  BaseType extends keyof M = keyof M
>(
  actionTypes: M,
  action: CRUDActions<I, K, M>,
  type: BaseType
): action is ExtractAction<CRUDActions<I, K, M>, M[BaseType]> {
  return action.type === actionTypes[type];
}

export const DefaultCRUDActionTypes = {
  LIST: 'LIST',
  CREATE: 'CREATE',
  UPDATE: 'UPDATE',
  DELETE: 'DELETE',
  PAGINATE: 'PAGINATE',
  PARAMS: 'PARAMS',
  RESET: 'RESET'
} as const;

export function getCRUDActionsCreator<I, K extends Key<I>>() {
  // prettier-ignore
  function create(): [CRUDActionCreators<I, K, typeof DefaultCRUDActionTypes>, typeof DefaultCRUDActionTypes]
  // prettier-ignore
  function create<M extends CRUDActionTypes = CRUDActionTypes>(actionTypes: M): [CRUDActionCreators<I, K, CustomActionTypes<M>>, CustomActionTypes<M>]
  // prettier-ignore
  function create<M extends CRUDActionTypes = CRUDActionTypes>(actionTypes = DefaultCRUDActionTypes as M) {
    actionTypes = { ...DefaultCRUDActionTypes, ...actionTypes };
    const creators: CRUDActionCreators<I, K, CustomActionTypes<M>> = {
      list: payload => ({ type: actionTypes['LIST'], payload }),
      create: payload => ({ type: actionTypes['CREATE'], payload }),
      update: payload => ({ type: actionTypes['UPDATE'], payload }),
      delete: payload => ({ type: actionTypes['DELETE'], payload }),
      paginate: payload => ({ type: actionTypes['PAGINATE'], payload }),
      params: payload => ({ type: actionTypes['PARAMS'], payload }),
      reset: () => ({ type: actionTypes['RESET'] })
    };
    return [creators, actionTypes] as const;
  }
  return create;
}


================================================
FILE: src/hooks/crud-reducer/crudReducer.ts
================================================
/* eslint-disable @typescript-eslint/ban-types */

import {
  Key,
  CRUDActions,
  CRUDActionTypes,
  PaginatePayload,
  DefaultCRUDActionTypes,
  isAction,
  List
} from './crudAction';

export interface CRUDState<I, Prefill extends boolean = true> {
  ids: string[];
  byIds: Record<string, I>;
  list: Prefill extends true ? Array<I | null> : I[];
  pageNo: number;
  pageSize: number;
  total: number;
  params: any;
}

export type CRUDReducer<
  I,
  K extends Key<I>,
  Prefill extends boolean = true,
  M extends CRUDActionTypes = CRUDActionTypes
> = (
  state: CRUDState<I, Prefill>,
  action: CRUDActions<I, K, M>
) => CRUDState<I, Prefill>;

export interface CreateCRUDReducerOptions<
  M extends CRUDActionTypes = CRUDActionTypes
> {
  prefill?: boolean;
  actionTypes?: M;
  keyGenerator?: (index: number) => string;
}

export const defaultKeyGenerator = (() => {
  let count = 0;
  return function () {
    count++;
    return `mock-${count}`;
  };
})();

export function parsePaginatePayload<I>(payload: PaginatePayload<I>) {
  return Array.isArray(payload)
    ? {
        total: payload.length,
        data: payload,
        pageNo: 1
      }
    : payload;
}

export function createCRUDReducer<
  I,
  K extends Key<I>,
  M extends CRUDActionTypes = CRUDActionTypes
>(
  key: K,
  options: CreateCRUDReducerOptions<M> & { prefill: false }
): [CRUDState<I, false>, CRUDReducer<I, K, false>];

export function createCRUDReducer<
  I,
  K extends Key<I>,
  M extends CRUDActionTypes = CRUDActionTypes
>(
  key: K,
  options?: CreateCRUDReducerOptions<M>
): [CRUDState<I, true>, CRUDReducer<I, K, true>];

export function createCRUDReducer<
  I,
  K extends Key<I>,
  M extends CRUDActionTypes = CRUDActionTypes
>(
  key: K,
  options?: CreateCRUDReducerOptions<M>
): [CRUDState<I, boolean>, CRUDReducer<I, K, boolean, M>] {
  const defaultState: CRUDState<I, boolean> = {
    byIds: {},
    ids: [],
    list: [],
    pageNo: 1,
    pageSize: 10,
    total: 0,
    params: {}
  };

  const {
    prefill = true,
    keyGenerator = defaultKeyGenerator,
    actionTypes = DefaultCRUDActionTypes as M
  } = options || {};

  const reducer: CRUDReducer<I, K, boolean, M> = (
    state = defaultState,
    action
  ) => {
    if (isAction(actionTypes, action, 'PAGINATE')) {
      return (() => {
        const {
          data,
          pageNo,
          total,
          pageSize = state.pageSize
        } = parsePaginatePayload(action.payload);

        if (prefill === false) {
          return reducer(state, { type: actionTypes['LIST'], payload: data });
        }

        const start = (pageNo - 1) * pageSize;

        const insert = <T1, T2>(arr: T1[], ids: T2[]) => {
          return [
            ...arr.slice(0, start),
            ...ids,
            ...arr.slice(start + pageSize)
          ];
        };

        const { list, ids, byIds } = reducer(defaultState, {
          type: actionTypes['LIST'],
          payload: data
        });

        const length = total - state.ids.length;

        return {
          ...state,
          total,
          pageNo,
          pageSize,
          byIds: {
            ...state.byIds,
            ...byIds
          },
          ids: insert(
            [
              ...state.ids,
              ...Array.from({ length }, (_, index) => keyGenerator(index))
            ],
            ids
          ),
          list: insert(
            [...state.list, ...Array.from({ length }, () => null)],
            list
          )
        };
      })();
    }

    if (isAction(actionTypes, action, 'LIST')) {
      return (action as List<'', I>).payload.reduce(
        (state, payload) =>
          reducer(state, { type: actionTypes['CREATE'], payload }),
        defaultState
      );
    }

    if (isAction(actionTypes, action, 'CREATE')) {
      const id: string = action.payload[key] as any;
      return {
        ...state,
        byIds: { ...state.byIds, [id]: action.payload },
        list: [...state.list, action.payload],
        ids: [...state.ids, id]
      };
    }

    if (isAction(actionTypes, action, 'UPDATE')) {
      const id = action.payload[key] as string;
      const updated = { ...state.byIds[id], ...action.payload };
      const index = state.ids.indexOf(id);
      return index === -1
        ? state
        : {
            ...state,
            byIds: { ...state.byIds, [id]: updated },
            list: [
              ...state.list.slice(0, index),
              updated,
              ...state.list.slice(index + 1)
            ]
          };
    }

    if (isAction(actionTypes, action, 'DELETE')) {
      const id = action.payload[key];
      const index = state.ids.indexOf(id);
      const { [id]: _deleted, ...byIds } = state.byIds;
      return {
        ...state,
        byIds,
        ids: removeFromArray(state.ids, index),
        list: removeFromArray(state.list, index)
      };
    }

    if (isAction(actionTypes, action, 'PARAMS')) {
      const { pageNo, pageSize, ...params } = action.payload;
      const toNum = (value: unknown, num: number) =>
        typeof value === 'undefined' || isNaN(Number(value))
          ? num
          : Number(value);

      return {
        ...state,
        pageNo: toNum(pageNo, state.pageNo),
        pageSize: toNum(pageSize, state.pageSize),
        params
      };
    }

    if (isAction(actionTypes, action, 'RESET')) {
      return defaultState;
    }

    return state;
  };

  return [defaultState, reducer];
}

export function removeFromArray<T>(arr: T[], index: number) {
  return index < 0 ? arr : [...arr.slice(0, index), ...arr.slice(index + 1)];
}


================================================
FILE: src/hooks/crud-reducer/crudSelector.ts
================================================
import { CRUDState } from './crudReducer';

export interface PaginateState<S extends CRUDState<unknown, any>> {
  ids: S['ids'];
  list: S['list'];
  pageNo: number;
  pageSize: number;
  total: number;
  params: any;
  hasData: boolean;
}

export function paginateSelector<S extends CRUDState<unknown, any>>({
  list,
  ids,
  pageNo,
  pageSize,
  params,
  total
}: S): PaginateState<S> {
  const start = (pageNo - 1) * pageSize;
  const _list = list.slice(start, start + pageSize);
  const _ids = ids.slice(start, start + pageSize);

  let hasData = !!_list.length;
  for (const item of _list) {
    if (item === null) {
      hasData = false;
      break;
    }
  }

  return {
    list: _list,
    ids: _ids,
    pageNo,
    pageSize,
    total,
    params,
    hasData
  };
}


================================================
FILE: src/hooks/crud-reducer/index.ts
================================================
export * from './bindDispatch';
export * from './crudAction';
export * from './crudReducer';
export * from './crudSelector';
export * from './useActions';
export * from './useCRUDReducer';


================================================
FILE: src/hooks/crud-reducer/useActions.ts
================================================
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { ActionCreators } from './crudAction';
import { Dispatched, bindDispatch } from './bindDispatch';

export function useActions<A extends ActionCreators>(
  creators: A
): Dispatched<A> {
  const dispatch = useDispatch();
  const [actions] = useState(bindDispatch(creators, dispatch));
  return actions;
}


================================================
FILE: src/hooks/crud-reducer/useCRUDReducer.ts
================================================
/* eslint-disable @typescript-eslint/ban-types */

import { useReducer, useState } from 'react';
import {
  CRUDState,
  createCRUDReducer,
  CreateCRUDReducerOptions
} from './crudReducer';
import { Key, getCRUDActionsCreator, CRUDActionCreators } from './crudAction';
import { bindDispatch, Dispatched } from './bindDispatch';

export type UseCRUDReducer<
  I,
  K extends Key<I>,
  Prefill extends boolean = true
> = () => [CRUDState<I, Prefill>, Dispatched<CRUDActionCreators<I, K>>];

export function createUseCRUDReducer<I, K extends Key<I>>(
  key: K,
  options: CreateCRUDReducerOptions & { prefill: false }
): UseCRUDReducer<I, K, false>;

export function createUseCRUDReducer<I, K extends Key<I>>(
  key: K,
  options?: CreateCRUDReducerOptions
): UseCRUDReducer<I, K, true>;

export function createUseCRUDReducer<I, K extends Key<I>>(
  key: K,
  options?: CreateCRUDReducerOptions
): UseCRUDReducer<I, K, boolean> {
  const [intialState, reducer] = createCRUDReducer<I, K>(key, options);
  return function useCRUDReducer() {
    const [state, dispatch] = useReducer(reducer, intialState);
    const [actions] = useState(() => {
      const [actions] = getCRUDActionsCreator<I, K>()();
      return { dispatch, ...bindDispatch(actions, dispatch) };
    });
    return [state, actions];
  };
}


================================================
FILE: src/hooks/useActions.ts
================================================
import { useMemo, useRef, Dispatch as ReactDispatch } from 'react';
import { AnyAction, Dispatch } from 'redux';
import { useDispatch } from 'react-redux';

interface ActionCreators {
  [k: string]: (...args: any[]) => AnyAction;
}

type Handler<A extends ActionCreators> = {
  [X in keyof A]: (...args: Parameters<A[X]>) => void;
};

export function withDispatch<A extends ActionCreators>(
  creators: A,
  dispatch: Dispatch | ReactDispatch<any>
) {
  const handler = {} as Handler<A>;
  for (const key in creators) {
    const creator = creators[key];
    handler[key] = (...args: Parameters<typeof creator>) => {
      dispatch(creator(...args));
    };
  }

  return handler;
}

export function useActions<A extends ActionCreators>(creators: A): Handler<A> {
  const dispatch = useDispatch();
  const creatorsRef = useRef(creators);

  return useMemo(() => withDispatch(creatorsRef.current, dispatch), [dispatch]);
}


================================================
FILE: src/hooks/useBoolean.ts
================================================
import { useState, useMemo } from 'react';

export function useBoolean(initialState = false) {
  const [flag, setFlag] = useState(initialState);
  const actions = useMemo(
    () => [
      () => setFlag(true),
      () => setFlag(false),
      () => setFlag(flag => !flag)
    ],
    []
  );
  return [flag, ...actions] as const;
}


================================================
FILE: src/hooks/useMouseTrap.ts
================================================
import { useEffect } from 'react';
import mousetrap, { MousetrapStatic } from 'mousetrap';

type Params = Parameters<MousetrapStatic['bind']>;

export function useMouseTrap(
  key: Params[0],
  method: Params[1],
  preventDefault = true
) {
  useEffect(() => {
    if (key !== '') {
      const instance = mousetrap(document.body);
      const handler = (...args: Parameters<Params[1]>) => {
        method(...args);
        return preventDefault ? false : true;
      };
      instance.bind(key, handler);
      return () => {
        instance.unbind(key);
      };
    }
  }, [key, method, preventDefault]);
}


================================================
FILE: src/index.scss
================================================
*,
*:before,
*:after {
  box-sizing: border-box;
}

html,
body,
#root {
  min-height: 100vh;
}

html {
  background-color: var(--main-color);

  &:not([data-platform^='darwin']) {
    @include scrollbar;
  }
}

body {
  @include typeface();
  color: var(--text-secondary-color);
  font-size: 14px;
  margin: 0;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img {
  vertical-align: middle;
}

a {
  text-decoration: none;
  color: inherit;
}

#nprogress {
  .bar {
    background: var(--accent-color);
    height: 3px;
  }

  .peg {
    box-shadow: 0 0 10px var(--accent-color), 0 0 5px var(--accent-color);
  }

  .spinner-icon {
    border-top-color: var(--accent-color);
    border-left-color: var(--accent-color);
  }
}


================================================
FILE: src/index.tsx
================================================
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'connected-react-router/immutable'; // https://github.com/supasate/connected-react-router/issues/312
import { MuiThemeProvider } from '@material-ui/core/styles';
import { theme } from './theme';
import configureStore, { history } from './store';
import App from './App';
import * as serviceWorker from './serviceWorker';

import 'typeface-roboto';
import 'typeface-nunito-sans';
import './utils/date';
import './index.scss';

const store = configureStore();

function render() {
  return ReactDOM.render(
    <MuiThemeProvider theme={theme}>
      <Provider store={store}>
        <ConnectedRouter history={history}>
          <App />
        </ConnectedRouter>
      </Provider>
    </MuiThemeProvider>,
    document.getElementById('root')
  );
}

render();

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

if (module.hot) {
  module.hot.accept('./App', render);
}


================================================
FILE: src/pages/Auth/Auth.scss
================================================
.auth {
  @include flex($flex-direction: column);
  @include fixed(0, null, 0);
  @include sq-dimen(100%);

  background-color: var(--main-color);
  color: var(--text-color);
  padding: $padding-x;
  text-align: center;
  z-index: $app-region-z-index - 1;

  .auth-header {
    margin-top: 65px;
  }

  .auth-content {
    @include dimen(100%);
    @include flex(stretch, center, $flex-direction: column);
    @include relative();
    flex: 1 0 auto;
  }

  .auth-confirm-button {
    color: var(--accent-color);
  }

  .auth-go-back {
    color: #999;
  }

  form {
    @include dimen(100%);
    .mui-input-base {
      margin: 5px 0 15px;
    }
  }

  .file-upload {
    @include dimen(100%);
    @include flex($flex-direction: column);
    margin-top: 50px;
    font-weight: bold;
    flex: 1 0 auto;

    .file-upload-header {
      a {
        color: var(--accent-color);
        text-decoration: underline;
      }
    }

    .file-upload-content {
      @include relative();

      border: 2px dashed;
      border-radius: 5px;
      color: var(--text-secondary-color);
      cursor: pointer;
      flex: 1 0 auto;
      margin: 10px 0;

      &.dragover {
        label {
          opacity: 0.5;
        }
      }

      input[type='file'] {
        @include absolute(0, null, 0);
        @include sq-dimen(100%);

        opacity: 0;
        outline: none;
      }

      label {
        @include absolute(0, null, 0);
        @include flex(center, center);
        @include sq-dimen(100%);

        line-height: 1.5em;
        padding: 15px;
      }
    }

    .file-upload-footer {
      @include flex(center);

      color: var(--error-color);
      min-height: 24px;

      svg {
        margin-right: 5px;
      }
    }
  }
}


================================================
FILE: src/pages/Auth/Auth.tsx
================================================
import React, { useState } from 'react';
import { useRxInput, useRxAsync } from 'use-rx-hooks';
import { Button } from '@material-ui/core';
import { Input } from '../../components/Mui/Input';
import { FileUpload } from './FileUpload';
import { generateAuthUrl, getToken } from '../../service';
import { ReactComponent as LogoSvg } from '../../assets/logo.svg';
import { useAuthActions } from '../../store';

export function Auth() {
  const [value, inputProps] = useRxInput();
  const { authenticated } = useAuthActions();
  const [{ loading }, { fetch }] = useRxAsync(getToken, {
    defer: true,
    onSuccess: authenticated
  });
  const [installed, setInstanned] = useState(window.oAuth2Storage.get());

  return (
    <div className="auth">
      <div className="auth-header">
        <LogoSvg className="logo" />
        <h4>Unoffical Google Tasks Client</h4>
      </div>
      <div className="auth-content">
        {installed ? (
          <form>
            Paste the code here:
            <Input
              {...inputProps}
              className="filled bottom-border"
              disabled={loading}
              autoFocus
              fullWidth
            />
            <div>
              <div>
                <Button disabled={loading} onClick={generateAuthUrl}>
                  Get Code
                </Button>
                <Button
                  disabled={loading}
                  className="auth-confirm-button"
                  onClick={() => fetch(value)}
                >
                  Confirm
                </Button>
              </div>
              <Button
                className="auth-go-back"
                onClick={() => setInstanned(undefined)}
              >
                Go Back
              </Button>
            </div>
          </form>
        ) : (
          <FileUpload />
        )}
      </div>
    </div>
  );
}


================================================
FILE: src/pages/Auth/FileUpload.tsx
================================================
import React, { useState } from 'react';
import { useRxAsync } from 'use-rx-hooks';
import { useBoolean } from '../../hooks/useBoolean';
import ErrorOutline from '@material-ui/icons/ErrorOutline';

function readFile(file: File) {
  return new Promise<OAuthKeys>((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = event => {
      if (event.target) {
        try {
          const content = JSON.parse(event.target.result as string);
          content.installed && resolve(content);
        } catch (error) {}
      }
      reject('Invalid JSON file');
    };
    reader.readAsText(file);
  });
}

function onSuccess(payload: OAuthKeys) {
  window.oAuth2Storage.save(payload);
  window.location.reload();
}

export function FileUpload() {
  const [isDragover, dragover, dragleave] = useBoolean();
  const [errorMsg, setErrorMsg] = useState('');
  const [, { fetch }] = useRxAsync(readFile, {
    defer: true,
    onSuccess,
    onFailure: setErrorMsg
  });

  return (
    <div className="file-upload">
      <div className="file-upload-header">
        <a
          href="https://github.com/Pong420/google-tasks-desktop#project-setup"
          target="_blank"
          rel="noopener noreferrer"
        >
          How to get OAuth2 JSON file
        </a>
      </div>
      <div
        className={['file-upload-content', isDragover && 'dragover']
          .filter(Boolean)
          .join(' ')
          .trim()}
      >
        <label htmlFor="file-upload-input">
          Choose the OAuth2 JSON file or drag it here.
        </label>
        <input
          type="file"
          name="files"
          id="file-upload-input"
          accept="application/json"
          onDragOver={dragover}
          onDrop={dragleave}
          onDragLeave={dragleave}
          onChange={evt => {
            const { files } = evt.currentTarget;
            if (files && files.length && files[0].type === 'application/json') {
              fetch(files[0]);
            } else {
              setErrorMsg('Invalid file or format');
            }
          }}
        />
      </div>
      <div className="file-upload-footer">
        {errorMsg && <ErrorOutline />} {errorMsg}
      </div>
    </div>
  );
}


================================================
FILE: src/pages/Auth/index.ts
================================================
import './Auth.scss';

export * from './Auth';
export { Auth as default } from './Auth';


================================================
FILE: src/pages/TaskList/CompletedTaskList/CompletedTaskList.scss
================================================
$completed-task-list-header-height: 54px;

.completed-tasks-list {
  @include dimen(100%, $completed-task-list-header-height);
  @include relative();
  flex: 1 0 auto;
  z-index: $text-highlight-z-index;

  .scroll-content {
    @include dimen(100%);
    overflow: auto;
  }
}

.completed-tasks-list-inner {
  @include absolute(null, 0, 0);
  @include animate(transform);
  @include flex($flex-direction: column);
  @include dimen(100%, 100vh);
  background-color: var(--main-color);
  max-height: calc(100vh - var(--header-height));
  padding-bottom: 2px;
  transform: translateY(calc(100% - #{$completed-task-list-header-height}));
  z-index: 10;

  .completed-tasks-list-content {
    visibility: hidden;
  }

  &.expanded {
    transform: translateY(0);
    .completed-tasks-list-content {
      visibility: visible;
    }
  }
}

.completed-tasks-list-header {
  @include dimen(100%, $completed-task-list-header-height);
  @include flex(center, space-between);
  @include fake-border($borderWidth: 1px, $color: var(--main-color-diff));
  @include typeface('Nunito Sans', 600);
  @include padding-x(($padding-x, 10px));

  border-top: 1px solid var(--border-color);
  cursor: pointer;
  flex: 1 0 auto;
}

.completed-tasks-list-content {
  @include flex();
  @include sq-dimen(100%);
  overflow: hidden;
  padding-bottom: 1px; // avoid scroll shown when there is only one completed task
}


================================================
FILE: src/pages/TaskList/CompletedTaskList/CompletedTaskList.tsx
================================================
import React from 'react';
import { useSelector } from 'react-redux';
import { CompletedTask } from '../Task';
import { IconButton } from '../../../components/Mui';
import { useBoolean } from '../../../hooks/useBoolean';
import { completedTaskIdsSelector } from '../../../store';
import ExpandIcon from '@material-ui/icons/ExpandLess';
import CollapseIcon from '@material-ui/icons/ExpandMore';

export function CompletedTaskList() {
  const tasks = useSelector(completedTaskIdsSelector);
  const [expanded, , , toggle] = useBoolean();

  if (tasks.length === 0) {
    return null;
  }

  return (
    <div className="completed-tasks-list">
      <div
        className={`completed-tasks-list-inner ${
          expanded ? 'expanded' : ''
        }`.trim()}
      >
        <div className="completed-tasks-list-header" onClick={toggle}>
          Completed ({tasks.length})
          <IconButton icon={expanded ? CollapseIcon : ExpandIcon} />
        </div>
        <div className="completed-tasks-list-content">
          <div className="scroll-content">
            {tasks.map(uuid => (
              <CompletedTask key={uuid} uuid={uuid} />
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}


================================================
FILE: src/pages/TaskList/CompletedTaskList/index.ts
================================================
import './CompletedTaskList.scss';

export * from './CompletedTaskList';
export { CompletedTaskList as default } from './CompletedTaskList';


================================================
FILE: src/pages/TaskList/NewTask/NewTask.scss
================================================
.new-task {
  @include dimen(100%, 56px);
  @include flex(center);
  @include padding-x(10px);

  .new-task-button {
    @include flex(center);
    border-radius: 50px;
    cursor: pointer;
    flex: 1 0 auto;
    margin-right: 10px;
    user-select: none;

    &:hover {
      background-color: var(--main-color-diff);
    }

    .mui-icon-button {
      background: none;
      font-weight: 500;
      margin-right: 5px;
    }

    svg {
      color: var(--accent-color);
    }
  }
}

.task-list-menu-paper {
  @include dimen(100%);
  .task-list-menu-title {
    @include padding-x(16px);
    color: var(--text-color);
    font-size: 0.875em !important;
    height: 30px;
    line-height: 30px;
    outline: none;
  }
}


================================================
FILE: src/pages/TaskList/NewTask/NewTask.tsx
================================================
import React from 'react';
import { SvgIconProps } from '@material-ui/core/SvgIcon';
import { IconButton, useMuiMenu } from '../../../components/Mui';
import { TaskListMenu } from '../TaskListMenu';
import { useTaskActions } from '../../../store';
import AddIcon from '@material-ui/icons/Add';
import MoreIcon from '@material-ui/icons/MoreVert';

const iconProps: SvgIconProps = { color: 'secondary' };

export function NewTask() {
  const { createTask } = useTaskActions();
  const { anchorEl, setAnchorEl, onClose } = useMuiMenu();

  return (
    <div className="new-task">
      <div className="new-task-button" onClick={() => createTask()}>
        <IconButton icon={AddIcon} iconProps={iconProps} disableTouchRipple />
        <div>Add a task</div>
      </div>
      <IconButton icon={MoreIcon} onClick={setAnchorEl} />
      <TaskListMenu open={!!anchorEl} anchorEl={anchorEl} onClose={onClose} />
    </div>
  );
}


================================================
FILE: src/pages/TaskList/NewTask/index.ts
================================================
import './NewTask.scss';

export * from './NewTask';
export { NewTask as default } from './NewTask';


================================================
FILE: src/pages/TaskList/Task/CompletedTask.tsx
================================================
import React from 'react';
import { useSelector } from 'react-redux';
import { Task, TaskProps } from './Task';
import { DeleteIcon, IconButton } from '../../../components/Mui';
import { useTaskActions, completedTaskSelector } from '../../../store';

interface Props extends TaskProps {}

export function CompletedTask(props: Props) {
  const { deleteTask } = useTaskActions();
  const { title } = useSelector(completedTaskSelector(props.uuid)) || {};

  return (
    <Task
      {...props}
      readOnly
      className="completed-task"
      value={title}
      endAdornment={
        <IconButton
          tooltip="Delete"
          icon={DeleteIcon}
          onClick={() => deleteTask({ uuid: props.uuid })}
        />
      }
    />
  );
}


================================================
FILE: src/pages/TaskList/Task/DatePicker/DatePicker.scss
================================================
.calender-header {
  @include flex(center, space-between);

  .month-year {
    @include typeface('Nunito Sans', 700);
    color: var(--date-color);
  }
}

.calendar-content {
  @include padding-x(6px);
  display: grid;
  grid-template-columns: repeat(7, auto);

  .grid {
    @include relative();
    text-align: center;

    &:before {
      @include dimen(100%, 0);
      content: '';
      display: block;
      padding-bottom: 100%;
    }

    .grid-content {
      @include absolute(0, null, 0);
      @include flex(center, center);
      @include sq-dimen(100%);
      @include typeface('Nunito Sans', 500);

      font-size: 12px;
    }
  }

  .day {
    color: var(--day-color);
    cursor: default;
  }

  .date {
    color: var(--date-color);
    cursor: pointer;

    .grid-content {
      border-radius: 50%;
    }

    @mixin backgorundWidthShadow($color) {
      background-color: $color;
      [data-theme^='light'] & {
        box-shadow: 0px 2px 10px -2px #{$color};
      }
    }

    &.today {
      .grid-content {
        @include backgorundWidthShadow(var(--accent-light-color));
        color: #333;
      }
    }

    &.selected {
      .grid-content {
        @include backgorundWidthShadow(var(--accent-dark-color));
        color: #fff;
      }
    }

    &.lastMonth,
    &.nextMonth {
      color: var(--day-color);
    }
  }
}


================================================
FILE: src/pages/TaskList/Task/DatePicker/DatePicker.tsx
================================================
import React, { useState, useCallback, HTMLAttributes, useMemo } from 'react';
import { IconButton } from '../../../../components/Mui/IconButton';
import LeftArrowIcon from '@material-ui/icons/KeyboardArrowLeftRounded';
import RightArrowIcon from '@material-ui/icons/KeyboardArrowRightRounded';

const days = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];

type GridProps = HTMLAttributes<HTMLDivElement>;

interface DateProps extends Omit<GridProps, 'onClick'> {
  date: Date;
  selected?: boolean;
  onClick(d: Date): void;
}

interface Props {
  value?: Date;
  onChange?(date: Date): void;
}

const Grid = React.memo(({ children, className, ...props }: GridProps) => (
  <div className={`grid ${className}`.trim()} {...props}>
    <div className="grid-content">{children}</div>
  </div>
));

const DateGrid = React.memo(
  ({ date, onClick, selected, className, ...props }: DateProps) => {
    const onClickCallback = useCallback(() => {
      onClick(date);
    }, [date, onClick]);

    return (
      <Grid
        className={['date', className, selected && 'selected']
          .filter(Boolean)
          .join(' ')
          .trim()}
        onClick={onClickCallback}
        {...props}
      />
    );
  }
);

export function DatePicker({ value, onChange }: Props) {
  const [{ dates, date }, setDisplay] = useState(getDisplayData(value));
  const [currDate, setCurrDate] = useState<Date>(date);

  const [prevMonth, nextMonth] = useMemo(() => {
    const handler = (step: number) => () =>
      setDisplay(({ date }) => getDisplayData(date.addMonths(step)));
    return [handler(-1), handler(1)];
  }, []);

  return (
    <div className="date-picker">
      <div className="calender">
        <div className="calender-header">
          <IconButton icon={LeftArrowIcon} onClick={prevMonth} />
          <div className="month-year">
            {date.getMonthName()} {date.getFullYear()}
          </div>
          <IconButton icon={RightArrowIcon} onClick={nextMonth} />
        </div>
        <div className="calendar-content">
          {days.map((day, index) => (
            <Grid className="day" key={index}>
              {day}
            </Grid>
          ))}
          {dates.map((d, index) => (
            <DateGrid
              key={index}
              date={d}
              selected={currDate.compare(d).sameDate}
              className={[
                ...Object.entries(date.compare(d)).reduce(
                  (c, [n, v]) => [...c, v && n],
                  [] as Array<string | boolean>
                ),
                d.isToday() && 'today'
              ]
                .filter(Boolean)
                .join(' ')
                .trim()}
              onClick={(d: Date) => {
                setCurrDate(d);
                onChange && onChange(d);
              }}
            >
              {d.getDate()}
            </DateGrid>
          ))}
        </div>
      </div>
    </div>
  );
}

function getDisplayData(dateObj: Date = new Date()) {
  const currDate = dateObj.getDate(); // current date
  const one = dateObj.addDays(-1 * currDate + 1); // first day of current month
  const index = one.getDay() - 1;
  const MonthDayCount = one.getMonthDayCount();
  const curMonth = [];
  const lastMonth = [];
  const nextMonth = [];
  let dates = [];

  for (let a = 0; a < MonthDayCount; a++) {
    curMonth.push(one.addDays(a));
  }

  for (let b = 0; b < index; b++) {
    lastMonth.push(one.addDays((b + 1) * -1));
  }

  dates = lastMonth.reverse().concat(curMonth);

  const length = dates.length;

  for (let c = length; c < 42; c++) {
    nextMonth.push(curMonth[curMonth.length - 1].addDays(c - length + 1));
  }

  dates = dates.concat(nextMonth);

  const cIndex = lastMonth.length + currDate - 1;
  const wIndex = cIndex - dateObj.getDay() + 1; // index of this week first day in days

  return {
    dates,
    week: dates.slice(wIndex, wIndex + 7),
    date: dateObj
  };
}


================================================
FILE: src/pages/TaskList/Task/DatePicker/index.ts
================================================
import './DatePicker.scss';

export * from './DatePicker';
export { DatePicker as default } from './DatePicker';


================================================
FILE: src/pages/TaskList/Task/DateTimeDialog/DateTimeDialog.scss
================================================
.date-time-dialog-paper.date-time-dialog-paper {
  @include dimen(100%);
  color: var(--text-secondary-color);
  max-height: 420px;
  max-width: 300px;

  button {
    color: var(--text-secondary-color);
    text-transform: inherit;
  }

  .dialog-scroll-content {
    padding: 5px 15px 20px 15px;
  }

  .dialog-actions {
    margin-top: 20px;
  }
}


================================================
FILE: src/pages/TaskList/Task/DateTimeDialog/DateTimeDialog.tsx
================================================
import React, {
  useState,
  useEffect,
  createContext,
  ReactNode,
  useContext
} from 'react';
import {
  ConfirmDialog,
  ConfirmDialogProps
} from '../../../../components/Mui/Dialog';
import { DatePicker } from '../DatePicker';
import { useBoolean } from '../../../../hooks/useBoolean';

interface Props {
  date?: Date;
  onConfirm(date: Date): void;
}

type Control = Omit<
  ConfirmDialogProps,
  'onChange' | 'onConfirm' | 'confirmLabel'
>;

interface DateTimeDialogContext {
  openDateTimeDialog: (props: Props) => void;
}

const dialogClasses: ConfirmDialogProps['classes'] = {
  paper: 'date-time-dialog-paper'
};

const Context = createContext({} as DateTimeDialogContext);

export function useDateTimeDialog() {
  return useContext(Context);
}

export function DateTimeDialogProvider({ children }: { children: ReactNode }) {
  const [props, setProps] = useState<Props & Partial<Control>>();
  const [isOpen, open, close] = useBoolean();

  useEffect(() => {
    props && open();
  }, [props, open]);

  return (
    <Context.Provider value={{ openDateTimeDialog: setProps }}>
      {children}
      {props && (
        <DateTimeDialog
          {...props}
          open={isOpen}
          onClose={close}
          onExited={(...args) => {
            props.onExited && props.onExited(...args);
            setProps(undefined);
          }}
        />
      )}
    </Context.Provider>
  );
}

export const DateTimeDialog = ({
  date: defaultDate,
  onConfirm,
  ...props
}: Props &
  Omit<ConfirmDialogProps, 'onChange' | 'onConfirm' | 'confirmLabel'>) => {
  const [date, setDate] = useState(defaultDate || new Date());

  return (
    <ConfirmDialog
      confirmLabel="OK"
      classes={dialogClasses}
      onConfirm={() => {
        onConfirm(date);
      }}
      {...props}
    >
      <DatePicker value={defaultDate} onChange={setDate} />
    </ConfirmDialog>
  );
};


================================================
FILE: src/pages/TaskList/Task/DateTimeDialog/index.ts
================================================
import './DateTimeDialog.scss';

export * from './DateTimeDialog';
export { DateTimeDialog as default } from './DateTimeDialog';


================================================
FILE: src/pages/TaskList/Task/Task.scss
================================================
$border: 1px solid var(--border-color);
$min-task-height: 50px;

@mixin withTopBottomBorder($color: var(--border-color)) {
  &:before {
    @include absolute(0, null, 0);
    @include dimen(100%, calc(100% + 1px));

    border-top: 1px solid $color;
    border-bottom: 1px solid $color;
    content: '';
  }
}

.task {
  @include dimen(100%);
  @include flex(center);
  @include padding-x(10px);
  @include relative();
  @include withTopBottomBorder(transparent);
  background-color: var(--main-color);

  .task-input-base {
    @include relative();
    @include withTopBottomBorder();
    min-height: $min-task-height;
    min-width: 0;

    .task-input-base-end-adornment {
      visibility: hidden;
    }
  }

  &:hover,
  &.focused,
  &.dragging {
    background-color: var(--task-highlight-background-color);

    .task-input-base::before {
      border-color: transparent;
    }
  }

  &:hover,
  &.focused {
    &:before {
      border-color: var(--border-color);
    }
    &,
    & + .task:not(:last-child) {
      .task-input-base::before {
        border-color: transparent;
      }
    }

    // input endAdornment
    .task-input-base .mui-icon-button {
      visibility: visible;
    }
  }

  &:hover {
    z-index: 1;
  }

  &:first-child {
    .task-input-base:before {
      border-top-color: transparent;
    }
  }

  .toggle-completed {
    @include relative();

    .mui-icon-button {
      + .mui-icon-button {
        @include absolute();
        visibility: hidden;
      }
    }

    .mui-tick-icon {
      color: var(--accent-color);
    }
  }

  .toggle-completed,
  .task-input-base-end-adornment {
    @include flex(center);
    align-self: stretch;
    max-height: 68px;
    margin-top: 1px;
  }

  .task-input-content {
    @include dimen(100%);
    @include flex(flex-start, center, column);
    @include padding-y(12px); // padding-y for multiple line
    @include relative();
    align-self: center;
    overflow: hidden;

    .mui-input-base {
      $line-height: 20px;
      @include sq-dimen(auto);
      font-size: 14px;
      line-height: $line-height;
      min-height: 0;
      padding: 0;
      width: 100%;

      textarea {
        min-height: $line-height;
      }
    }
  }

  .task-notes {
    @include dimen(100%);
    @include multi-line-ellipsis($line-height: 16px);
    @include margin-y(2px);
    color: var(--text-secondary-color);
    font-size: 12px;
    overflow-wrap: break-word;
  }

  .task-due-date-button {
    @include flex(center, center);

    align-self: flex-start;
    background-color: var(--task-highlight-background-color);
    border: 1px solid var(--main-color-diff);
    border-radius: 3px;
    color: var(--text-secondary-color);
    cursor: pointer;
    font-size: 12px;
    line-height: 16px;
    margin-top: 8px;
    padding: 6px 7px 5px;

    svg {
      color: var(--accent-color);
      font-size: 16px;
      margin-right: 6px;
    }

    &:after {
      content: attr(data-date);
    }

    &[data-date^='Today'] {
      &:after {
        color: var(--accent-color);
        font-weight: 500;
      }
    }

    &[data-date^='Yesterday'],
    &[data-date*='ago'] {
      svg {
        color: var(--error-color);
      }
    }

    &:hover {
      background-color: var(--main-color);
      box-shadow: 0px 5px 10px var(--shadow-color);
    }
  }
}

.completed-task {
  textarea {
    text-decoration: line-through;
  }
}


================================================
FILE: src/pages/TaskList/Task/Task.tsx
================================================
import React, { ReactNode } from 'react';
import { useSelector } from 'react-redux';
import { Input, InputProps } from '../../../components/Mui';
import { taskSelector } from '../../../store';
import { ToggleCompleted } from './ToggleCompleted';
import { TaskInput, TaskInputProps } from './TaskInput';

export interface TaskProps
  extends InputProps,
    Pick<TaskInputProps, 'onDueDateBtnClick'> {
  className?: string;
  uuid: string;
  isEmpty?: boolean;
  endAdornment?: ReactNode;
}

export const Task = React.forwardRef<HTMLDivElement, TaskProps>(
  (
    {
      className,
      uuid,
      isEmpty,
      endAdornment,
      onDueDateBtnClick,
      ...inputProps
    },
    ref
  ) => {
    const { due, notes } = useSelector(taskSelector(uuid)) || {};

    return (
      <div
        data-uuid={uuid}
        className={['task', className].filter(Boolean).join(' ').trim()}
        ref={ref}
      >
        <ToggleCompleted isEmpty={!!isEmpty} uuid={uuid} />
        <Input
          {...inputProps}
          fullWidth
          className="task-input-base"
          inputProps={{ due, notes, onDueDateBtnClick }}
          inputComponent={TaskInput as InputProps['inputComponent']}
          endAdornment={
            <div className="task-input-base-end-adornment">{endAdornment}</div>
          }
        />
      </div>
    );
  }
);


================================================
FILE: src/pages/TaskList/Task/TaskInput.tsx
================================================
import React from 'react';
import { Input, InputProps } from '../../../components/Mui';
import { Schema$Task } from '../../../typings';
import EventAvailableIcon from '@material-ui/icons/EventAvailable';

export interface TaskInputProps extends Pick<Schema$Task, 'due' | 'notes'> {
  onDueDateBtnClick?(): void;
}

type Props = TaskInputProps & InputProps;

export function TaskInput({
  due,
  notes,
  onDueDateBtnClick,
  ...inputProps
}: Props) {
  return (
    <div className="task-input-content">
      <Input {...inputProps} multiline />
      {notes && <div className="task-notes">{notes}</div>}
      {due && (
        <div
          className="task-due-date-button"
          onClick={onDueDateBtnClick}
          data-date={dateFormat(new Date(due))}
        >
          <EventAvailableIcon />
        </div>
      )}
    </div>
  );
}

function dateFormat(d: Date) {
  const now = new Date();
  const dayDiff = Math.floor((+now - +d) / 1000 / 60 / 60 / 24);

  if (dayDiff === 0) {
    return 'Today';
  }

  if (dayDiff === -1) {
    return 'Tomorrow';
  }

  if (dayDiff < -1) {
    return d.format('D, j M');
  }

  if (dayDiff === 1) {
    return 'Yesterday';
  }

  if (dayDiff < 7) {
    return `${dayDiff} days ago`;
  }

  return `${Math.floor(dayDiff / 7)} weeks ago`;
}


================================================
FILE: src/pages/TaskList/Task/TodoTask/TodoTask.scss
================================================
.todo-task {
  @include textHighlight();
  user-select: none;

  &.dragging {
    @include padding-x((0px, 20px));

    border-radius: 12.5px;
    border: 1px solid var(--border-color);
    box-shadow: 0px 7px 10px -5px var(--shadow-color);
    left: auto !important;
    right: 5px !important;
    width: calc(100% - 60px) !important;
    z-index: 1000;

    .task-input-base-end-adornment {
      display: none;
    }

    .toggle-completed {
      margin-left: 0;
    }

    // override textHighlight
    &:before,
    &:after {
      visibility: hidden;
    }
  }

  &.highlight-bottom-border {
    &:after {
      @include dimen(100%);
      border-color: var(--accent-color);
      transition: 0s;
    }

    .task-input-base {
      border-color: transparent;
    }
  }

  &:last-child {
    border-bottom: 1px solid var(--border-color);
  }

  .toggle-completed {
    &:hover {
      > button:nth-child(1) {
        visibility: hidden;
      }
      > button:nth-child(2) {
        visibility: visible;
      }
    }
  }
}


================================================
FILE: src/pages/TaskList/Task/TodoTask/TodoTask.tsx
================================================
import React, {
  useRef,
  useMemo,
  useEffect,
  MouseEvent,
  KeyboardEvent
} from 'react';
import { useSelector } from 'react-redux';
import { Task, TaskProps } from '../Task';
import { useTodoTaskDetails, EditTaskButton } from '../TodoTaskDetails';
import { useDateTimeDialog } from '../DateTimeDialog';
import { useTodoTaskMenu } from './TodoTaskMenu';
import {
  focusedSelector,
  useTaskActions,
  todoTaskSelector,
  getDateLabel
} from '../../../../store';
import { useMouseTrap } from '../../../../hooks/useMouseTrap';
import { Schema$Task } from '../../../../typings';

export interface TodoTaskProps extends TaskProps {
  index: number;
  inherit?: (keyof Schema$Task)[];
  prevDue?: string | null;
  sortByDate?: boolean;
  prevIndex?: number;
  nextIndex?: number;
}

export const TodoTask = React.memo(
  ({
    uuid,
    index,
    className,
    inherit,
    prevDue,
    sortByDate,
    prevIndex = index - 1,
    nextIndex = index + 1,
    ...props
  }: TodoTaskProps) => {
    const ref = useRef<HTMLDivElement>(null);
    const {
      createTask,
      deleteTask,
      update: updateTask,
      moveTask,
      setFocus
    } = useTaskActions();

    const focused = useSelector(focusedSelector(uuid));

    const { title, due } = useSelector(todoTaskSelector(uuid)) || {};

    const { onDelete, moveTaskUp, moveTaskDown, ...handler } = useMemo(() => {
      return {
        moveTaskUp: () => moveTask({ uuid, from: index, to: index - 1 }),
        moveTaskDown: () => moveTask({ uuid, from: index, to: index + 1 }),
        onDelete: () => deleteTask({ uuid }),
        // prevent focused task trigger `onBlur` event
        onMouseDown: (event: MouseEvent<HTMLElement>) => {
          !(event.target instanceof HTMLTextAreaElement) &&
            event.preventDefault();
        },
        onClick: (event: MouseEvent<HTMLElement>) => {
          event.currentTarget
            .querySelector<HTMLTextAreaElement>('textarea')!
            .focus();
        },
        onBlur: () => {
          // reduce unnecessary `FOCUS_TASK` action
          setTimeout(() => {
            const el =
              document.activeElement?.parentElement?.parentElement
                ?.parentElement?.parentElement;
            (!el || !el.classList.contains('task')) && setFocus(null);
          }, 0);
        },
        onKeyDown: (event: KeyboardEvent<HTMLTextAreaElement>) => {
          const input = event.currentTarget;

          if (event.key === 'Enter') {
            event.preventDefault();
            createTask({
              prevTask: uuid,
              inherit: inherit && { uuid, keys: inherit }
            });
          }

          if (event.key === 'Backspace' && !input.value.trim()) {
            event.preventDefault();
            deleteTask({ uuid, prevTaskIndex: index - 1 });
          }

          if (event.key === 'Escape') {
            input.blur();
          }

          if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
            const { selectionStart, selectionEnd, value } = input;
            const notHightlighted = selectionStart === selectionEnd;
            const shouldFocusPrev =
              event.key === 'ArrowUp' && selectionStart === 0;
            const shouldFocusNext =
              event.key === 'ArrowDown' && selectionStart === value.length;

            const to = event.key === 'ArrowUp' ? prevIndex : nextIndex;

            if (notHightlighted && (shouldFocusPrev || shouldFocusNext)) {
              event.preventDefault();
              setFocus(to);
            }
          }
        }
      };
    }, [
      uuid,
      index,
      createTask,
      deleteTask,
      moveTask,
      setFocus,
      inherit,
      prevIndex,
      nextIndex
    ]);

    const { updateDue, moveDownByDate, moveUpByDate } = useMemo(() => {
      // const now = new Date();
      const date = due ? new Date(due) : null;
      const updateDue = (date?: Date) => {
        date && updateTask({ uuid, due: date.toISODateString() });
      };

      const moveDownByDate = () => {
        const now = new Date();
        const label = getDateLabel(due, now);
        let newDate: Date | null = null;
        if (label === 'Past') newDate = now;
        else if (label !== 'No date') newDate = date!.addDays(1);

        newDate && updateDue(newDate);
      };

      const moveUpByDate = () => {
        const now = new Date();
        const label = getDateLabel(due, now);
        let newDate: Date | null = null;
        if (label === 'No date') newDate = prevDue ? new Date(prevDue) : now;
        else if (label !== 'Past' && label !== 'Today')
          newDate = date!.addDays(-1);

        newDate && updateDue(newDate);
      };

      return { updateDue, moveUpByDate, moveDownByDate };
    }, [uuid, due, prevDue, updateTask]);

    const { openDateTimeDialog: _openDateTimeDialog } = useDateTimeDialog();
    const openDateTimeDialog = () =>
      _openDateTimeDialog({
        date: due ? new Date(due) : undefined,
        onConfirm: updateDue
      });

    const { openTodoTaskDetails: _openTodoTaskDetails } = useTodoTaskDetails();
    const openTodoTaskDetails = () =>
      _openTodoTaskDetails({ openDateTimeDialog, uuid });

    const { openTodoTaskMenu: _openTodoTaskMenu } = useTodoTaskMenu();
    const openTodoTaskMenu = (event: MouseEvent<HTMLElement>) =>
      _openTodoTaskMenu({
        event,
        uuid,
        onDelete,
        openDateTimeDialog,
        moveToAnotherList: () =>
          _openTodoTaskDetails({
            taskListDropdownOpened: true,
            openDateTimeDialog,
            uuid
          })
      });

    useEffect(() => {
      const el = ref.current;
      const input = el && el.querySelector<HTMLTextAreaElement>('textarea');
      if (input && focused) {
        const { length } = input.value;
        input.focus();
        // make sure cursor place at end of textarea
        input.setSelectionRange(length, length);
      }
    }, [focused]);

    useMouseTrap(focused ? 'shift+enter' : '', openTodoTaskDetails);
    useMouseTrap(
      focused ? 'option+up' : '',
      sortByDate ? moveUpByDate : moveTaskUp
    );
    useMouseTrap(
      focused ? 'option+down' : '',
      sortByDate ? moveDownByDate : moveTaskDown
    );

    return (
      <>
        <Task
          {...props}
          {...handler}
          ref={ref}
          uuid={uuid}
          value={title}
          isEmpty={!(title && title.trim())}
          onContextMenu={openTodoTaskMenu}
          onDueDateBtnClick={openDateTimeDialog}
          onFocus={() => !focused && setFocus(uuid)}
          onChange={event =>
            updateTask({ uuid, title: event.currentTarget.value })
          }
          className={['todo-task', focused ? 'focused' : '', className]
            .join(' ')
            .trim()}
          endAdornment={<EditTaskButton onClick={openTodoTaskDetails} />}
        />
      </>
    );
  }
);


================================================
FILE: src/pages/TaskList/Task/TodoTask/TodoTaskMenu.tsx
================================================
import React, {
  createContext,
  useContext,
  useState,
  MouseEvent,
  ReactNode
} from 'react';
import { useSelector } from 'react-redux';
import {
  useMuiMenuItem,
  Menu,
  MenuProps,
  useMuiMenu
} from '../../../../components/Mui';
import { todoTaskSelector } from '../../../../store';

interface Props {
  uuid: string;
  onDelete?: () => void;
  openDateTimeDialog?: () => void;
  moveToAnotherList?: () => void;
  firstTask?: boolean;
}

interface TodoTaskMenuContext {
  openTodoTaskMenu: (props: Props & { event: MouseEvent<HTMLElement> }) => void;
}

type Control = Omit<MenuProps, 'ref'>;

const classes: MenuProps['classes'] = { paper: 'todo-task-menu-paper' };

const Context = createContext({} as TodoTaskMenuContext);

export function useTodoTaskMenu() {
  return useContext(Context);
}

export function TodoTaskMenuProvider({ children }: { children: ReactNode }) {
  const [props, setProps] = useState<Props & Partial<Control>>();
  const { anchorPosition, setAnchorPosition, onClose } = useMuiMenu();

  return (
    <Context.Provider
      value={{
        openTodoTaskMenu: ({ event, ...props }) => {
          setProps(props);
          setAnchorPosition(event);
        }
      }}
    >
      {children}
      {props && (
        <TodoTaskMenu
          {...props}
          keepMounted={false}
          anchorPosition={anchorPosition}
          anchorReference="anchorPosition"
          open={!!anchorPosition}
          onClose={() => {
            onClose();
            setProps(undefined);
          }}
        />
      )}
    </Context.Provider>
  );
}

export const TodoTaskMenu = ({
  uuid,
  onClose,
  onDelete,
  openDateTimeDialog,
  moveToAnotherList,
  firstTask,
  ...props
}: Props & Control) => {
  const MenuItem = useMuiMenuItem({ onClose });
  const { due } = useSelector(todoTaskSelector(uuid)) || {};

  return (
    <Menu {...props} classes={classes} onClose={onClose}>
      <MenuItem text="Delete" onClick={onDelete} />
      <MenuItem
        text={`${due ? 'Change' : 'Add'} date/time`}
        onClick={openDateTimeDialog}
      />
      <MenuItem text="Add a subtask" disabled />
      {!firstTask && <MenuItem text="Indent" disabled />}
      <MenuItem text="Move to another list" onClick={moveToAnotherList} />
    </Menu>
  );
};


================================================
FILE: src/pages/TaskList/Task/TodoTask/index.ts
================================================
import './TodoTask.scss';

export * from './TodoTask';
export { TodoTask as default } from './TodoTask';


================================================
FILE: src/pages/TaskList/Task/TodoTaskDetails/DateTimeButton.tsx
================================================
import React from 'react';
import { IconButton } from '../../../../components/Mui';
import Button from '@material-ui/core/Button';
import CloseIcon from '@material-ui/icons/Close';

interface Props {
  date?: Date;
  onClick?: () => void;
  onRemove?: () => void;
}

export const DateTimeButton = ({ date, onClick, onRemove }: Props) => {
  if (!date) {
    return <Button onClick={onClick}>Add date/time</Button>;
  }

  return (
    <div className="task-deatails-due-date-button">
      <div className="task-deatails-due-date-clickable" onClick={onClick} />
      <div>
        <div className="date">{date.format('D, j M')}</div>
      </div>
      <IconButton
        tooltip="Remove date and time"
        icon={CloseIcon}
        onClick={onRemove}
      />
    </div>
  );
};


================================================
FILE: src/pages/TaskList/Task/TodoTaskDetails/TodoTaskDetails.scss
================================================
$height: 36px;

.todo-task-details {
  .row,
  .mui-input-base {
    + .row,
    + .mui-input-base {
      margin-top: 8px;
    }
  }

  .mui-input-base {
    @include padding-y(0.5em);
    line-height: 1.42em;

    &.todo-task-details-title-field {
      @include typeface('Nunito Sans', 600);
      font-size: 16px;
    }

    &.todo-task-details-notes-field {
      &,
      textarea::placeholder {
        font-size: 14px;
      }
    }
  }

  .row {
    @include flex(center, stretch);
    color: var(--text-secondary-color);
    min-height: 46px;

    .mui-dropdown-button {
      @include typeface('Nunito Sans', 600);
      background: var(--task-highlight-background-color);
      color: var(--text-color);
      min-height: $height;
      line-height: $height;
    }

    > svg {
      margin-right: 10px;
    }

    &.row-date,
    &.row-subtask {
      button {
        @include typeface('Nunito Sans', 700);
        text-transform: initial;
        color: inherit;
      }
    }
  }

  .task-deatails-due-date-button {
    @include dimen(100%);
    @include flex(center, space-between);
    @include padding-x(15px 0);
    @include relative();

    align-self: stretch;
    border: 1px solid var(--border-color);
    border-radius: 3px;
    user-select: none;

    .date {
      color: var(--accent-dark-color);
      font-weight: 500;
    }

    button {
      background: none;
    }

    .task-deatails-due-date-clickable {
      @include absolute(0, null, 0);
      @include sq-dimen(100%);
      cursor: pointer;
    }
  }
}

.details-task-list-dropdown-paper {
  @include dimen(calc(100% - 60px));

  .menu-content {
    @include padding-y(0);
  }

  .scroll-content {
    max-height: $height * 5;
  }

  .mui-menu-item {
    &.mui-menu-item {
      height: $height;

      > div {
        @include relative();
        flex-direction: row-reverse;

        .text {
          @include padding-x(36px 0);

          + svg {
            @include absolute(null, null, 0);
          }
        }
      }
    }
  }
}


================================================
FILE: src/pages/TaskList/Task/TodoTaskDetails/TodoTaskDetails.tsx
================================================
import React, {
  KeyboardEvent,
  useState,
  createContext,
  ReactNode,
  useEffect,
  useContext
} from 'react';
import { useSelector } from 'react-redux';
import {
  DeleteIcon,
  EditIcon,
  FullScreenDialog,
  FullScreenDialogProps,
  IconButton,
  Input
} from '../../../../components/Mui';
import { TaskListDropdown } from '../../TaskListDropdown';
import { DateTimeButton } from './DateTimeButton';
import { Schema$Task, Schema$TaskList } from '../../../../typings';
import { useBoolean } from '../../../../hooks/useBoolean';
import {
  todoTaskSelector,
  useTaskActions,
  currentTaskListsSelector
} from '../../../../store';
import FormatListBulletedIcon from '@material-ui/icons/FormatListBulleted';
import EventAvailableIcon from '@material-ui/icons/EventAvailable';

interface Props extends Pick<Schema$Task, 'uuid'> {
  taskListDropdownOpened?: boolean;
  openDateTimeDialog: () => void;
}

interface TodoTaskDetailsContext {
  openTodoTaskDetails: (props: Props) => void;
}

const preventStartNewLine = (evt: KeyboardEvent<HTMLDivElement>) =>
  evt.which === 13 && evt.preventDefault();

const dropdownButtonProps = {
  fullWidth: true
};

export const Context = createContext({} as TodoTaskDetailsContext);

export function useTodoTaskDetails() {
  return useContext(Context);
}

export function TodoTaskDetailsProvider({ children }: { children: ReactNode }) {
  const [props, setProps] = useState<Props & Partial<FullScreenDialogProps>>();
  const [isOpen, open, close] = useBoolean();

  useEffect(() => {
    props && open();
  }, [props, open]);

  return (
    <Context.Provider value={{ openTodoTaskDetails: setProps }}>
      {children}
      {props && (
        <TodoTaskDetails
          {...props}
          open={isOpen}
          onClose={close}
          onExited={(...args) => {
            props.onExited && props.onExited(...args);
            setProps(undefined);
          }}
        />
      )}
    </Context.Provider>
  );
}

export const EditTaskButton = ({ onClick }: { onClick(): void }) => {
  return (
    <IconButton
      className="edit-task-button"
      tooltip="Edit details"
      icon={EditIcon}
      onClick={onClick}
    />
  );
};

export function TodoTaskDetails({
  uuid,
  open,
  onClose,
  openDateTimeDialog,
  taskListDropdownOpened,
  ...props
}: Props & FullScreenDialogProps) {
  const [shouldBeDeleted, deleteOnExited] = useBoolean();
  const [moveTo, setMoveTo] = useState<Schema$TaskList>();
  const {
    update: updateTask,
    deleteTask,
    moveToAnotherList
  } = useTaskActions();
  const { title, notes, due } = useSelector(todoTaskSelector(uuid)) || {};
  const currentTaskList = useSelector(currentTaskListsSelector);

  return (
    <FullScreenDialog
      {...props}
      className="todo-task-details"
      open={!shouldBeDeleted && open}
      onClose={onClose}
      headerComponents={
        <IconButton
          tooltip="Delete"
          icon={DeleteIcon}
          onClick={deleteOnExited}
        />
      }
      onExited={() => {
        if (shouldBeDeleted) {
          deleteTask({ uuid });
        } else if (moveTo && moveTo.id !== currentTaskList.id) {
          moveToAnotherList({ tasklistId: moveTo.id, uuid });
        }
      }}
    >
      <Input
        multiline
        autoFocus
        className="filled todo-task-details-title-field"
        placeholder="Enter title"
        onKeyPress={preventStartNewLine}
        value={title}
        onChange={event =>
          updateTask({ uuid, title: event.currentTarget.value })
        }
      />

      <Input
        multiline
        rows={3}
        rowsMax={Infinity}
        value={notes}
        onChange={event =>
          updateTask({ uuid, notes: event.currentTarget.value })
        }
        className="filled todo-task-details-notes-field"
        placeholder="Add details"
      />

      <div className="row row-task-list">
        <FormatListBulletedIcon />
        <TaskListDropdown
          buttonProps={dropdownButtonProps}
          defaultOpen={taskListDropdownOpened}
          onSelect={setMoveTo}
          taskList={moveTo}
          paperClassName="details-task-list-dropdown-paper"
        />
      </div>

      <div className="row row-date">
        <EventAvailableIcon />
        <DateTimeButton
          date={due ? new Date(due) : undefined}
          onClick={openDateTimeDialog}
          onRemove={() => updateTask({ uuid, due: null })}
        />
      </div>
    </FullScreenDialog>
  );
}


================================================
FILE: src/pages/TaskList/Task/TodoTaskDetails/index.ts
================================================
import './TodoTaskDetails.scss';

export * from './TodoTaskDetails';
export { TodoTaskDetails as default } from './TodoTaskDetails';


================================================
FILE: src/pages/TaskList/Task/ToggleCompleted.tsx
================================================
import React from 'react';
import { useSelector } from 'react-redux';
import { IconButton } from '../../../components/Mui';
import { useTaskActions, taskSelector } from '../../../store';
import { Schema$Task } from '../../../typings';
import CircleIcon from '@material-ui/icons/RadioButtonUnchecked';
import TickIcon from '@material-ui/icons/Check';

interface Props extends Pick<Schema$Task, 'uuid'> {
  isEmpty: boolean;
}

const MarkCompleteButton = React.memo(() => (
  <IconButton tooltip="Mark incomplete">
    <CircleIcon />
  </IconButton>
));

const MarkInCompleteButton = React.memo(() => (
  <IconButton tooltip="Mark complete">
    <TickIcon className="mui-tick-icon" />
  </IconButton>
));

export function ToggleCompleted({ uuid, isEmpty }: Props) {
  const { update: updateTask, deleteTask } = useTaskActions();
  const { status, hidden } = useSelector(taskSelector(uuid)) || {};
  const isCompleted = hidden || status === 'completed';

  return (
    <div
      className="toggle-completed"
      onClick={() =>
        isEmpty
          ? deleteTask({ uuid })
          : updateTask({
              uuid,
              hidden: !isCompleted,
              status: isCompleted ? 'needsAction' : 'completed'
            })
      }
    >
      {!isCompleted && <MarkCompleteButton />}
      <MarkInCompleteButton />
    </div>
  );
}


================================================
FILE: src/pages/TaskList/Task/index.ts
================================================
import './Task.scss';

export * from './Task';
export * from './TodoTask';
export * from './CompletedTask';
export { Task as default } from './Task';


================================================
FILE: src/pages/TaskList/TaskList.scss
================================================
.task-list {
  @include dimen(100%, 100vh);
  @include flex($flex-direction: column);

  &.disabled {
    opacity: 0.3;

    &:after {
      @include fixed(0, null, 0);
      @include sq-dimen(100%);
      content: '';
      user-select: none;
      z-index: 100000;
    }
  }

  .task-list-header,
  .new-task {
    flex: 0 0 auto;
  }
}

.task-list-content {
  @include sq-dimen(100%);
  @include flex($flex-direction: column);
  max-height: calc(100vh - var(--header-height));
  overflow: hidden;
  z-index: 1;

  > .scroll-content {
    @include sq-dimen(100%);
    flex: 1 1 auto;
    overflow: auto;
  }
}


================================================
FILE: src/pages/TaskList/TaskList.tsx
================================================
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { TaskListHeader } from './TaskListHeader';
import { TodoTaskList } from './TodoTaskList';
import { NewTask } from './NewTask';
import { CompletedTaskList } from './CompletedTaskList';
import { TodoTaskDetailsProvider } from './Task/TodoTaskDetails';
import { DateTimeDialogProvider } from './Task/DateTimeDialog';
import { TodoTaskMenuProvider } from './Task/TodoTask/TodoTaskMenu';
import {
  useTaskListActions,
  useTaskActions,
  RootState,
  currentTaskListsSelector
} from '../../store';

export function TaskList() {
  const taskListActions = useTaskListActions();
  const taskActions = useTaskActions();
  const currentTasklist = useSelector(currentTaskListsSelector);

  const taskListId = currentTasklist && currentTasklist.id;
  const disabled = useSelector(
    (state: RootState) => state.task.loading || state.taskList.loading
  );

  useEffect(() => {
    taskListActions.getTaskLists();
  }, [taskListActions]);

  useEffect(() => {
    if (taskListId) {
      taskActions.getTasks({ tasklist: taskListId });
    }
  }, [taskActions, taskListId]);

  return (
    <div className={[`task-list`, disabled ? 'disabled' : ''].join(' ').trim()}>
      <TaskListHeader onConfirm={taskListActions.newTaskList} />
      <TodoTaskDetailsProvider>
        <DateTimeDialogProvider>
          <TodoTaskMenuProvider>
            <div className="task-list-content">
              <NewTask />
              <div className="scroll-content">
                <TodoTaskList taskListId={taskListId} />
              </div>
              <CompletedTaskList key={taskListId} />
            </div>
          </TodoTaskMenuProvider>
        </DateTimeDialogProvider>
      </TodoTaskDetailsProvider>
    </div>
  );
}


================================================
FILE: src/pages/TaskList/TaskListDropdown/TaskListDropdown.scss
================================================
$button-height: 26px;
$item-height: 40px;

.task-list-header-dropdown-container {
  @include app-region-no-drag;
  overflow: hidden;

  .task-list-header-dropdown-label {
    color: #80868b;
    font-size: 10px;
    letter-spacing: 1.5px;
    margin-bottom: 2px;
    text-transform: uppercase;
    text-align: right;
    padding-right: 6px;
  }

  .mui-dropdown-button {
    @include dimen(100%, $button-height);
    @include typeface('Nunito Sans', 600);
    color: var(--text-color);
    font-size: 15px;

    &:not(:hover) {
      background-color: transparent;
    }
  }
}

.dropdown-menu-paper.task-list-header-dropdown-paper {
  @include dimen(auto);
  margin-top: $button-height;

  .scroll-content {
    max-height: $item-height * 5;
    min-width: 180px;
    overflow: auto;
  }

  .mui-menu-item {
    @include typeface('Nunito Sans', 500);
    letter-spacing: 0.2px;
    height: $item-height;
  }
}


================================================
FILE: src/pages/TaskList/TaskListDropdown/TaskListDropdown.tsx
================================================
import React, { ReactNode, useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import {
  useMuiMenu,
  Dropdown,
  DropdownProps,
  FULLSCREEN_DIALOG_TRANSITION
} from '../../../components/Mui';
import { TaskListDropdownItem } from './TaskListDropdownItem';
import { taskListIdsSelector, currentTaskListsSelector } from '../../../store';
import { Schema$TaskList } from '../../../typings';

export interface TaskListDropdownProps
  extends Omit<Partial<DropdownProps>, 'onSelect'> {
  defaultOpen?: boolean;
  paperClassName?: string;
  footer?(onClose: () => void): ReactNode;
  onSelect(taskList: Schema$TaskList): void;
  taskList?: Schema$TaskList;
}

export function TaskListDropdown({
  children,
  onSelect,
  defaultOpen,
  footer,
  paperClassName,
  PaperProps,
  taskList: controlled,
  ...props
}: TaskListDropdownProps) {
  const { anchorEl, setAnchorEl, onClose } = useMuiMenu();
  const ids = useSelector(taskListIdsSelector);
  const currentTaskList = useSelector(currentTaskListsSelector);
  const taskList = controlled || currentTaskList;
  const dropdownRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    const el = dropdownRef.current;
    if (defaultOpen && el) {
      setTimeout(() => setAnchorEl(el), FULLSCREEN_DIALOG_TRANSITION / 2);
    }
  }, [setAnchorEl, defaultOpen]);

  return (
    <Dropdown
      {...props}
      ref={dropdownRef}
      PaperProps={{ className: paperClassName }}
      label={(taskList && taskList.title) || 'Loading...'}
      open={!!anchorEl}
      anchorEl={anchorEl}
      onClick={setAnchorEl}
      onClose={onClose}
      onEnter={el => {
        const scroller = el.querySelector<HTMLDivElement>('.scroll-content');
        const item = el.querySelector<SVGElement>('svg')!.parentElement!
          .offsetParent as HTMLElement;
        if (scroller && item) {
          scroller.scrollTop = item.offsetTop - 10 - item.offsetHeight * 2; // 10px padding;
        }
      }}
      footer={footer && 
Download .txt
gitextract_58hd8j73/

├── .eslintcache
├── .eslintignore
├── .gitignore
├── .prettierrc
├── LICENSE
├── Procfile
├── README.md
├── common.d.ts
├── config-overrides.js
├── electron/
│   ├── electron.d.ts
│   ├── main.ts
│   ├── menu.ts
│   ├── preload/
│   │   ├── index.ts
│   │   └── theme.ts
│   ├── storage.ts
│   └── tsconfig.json
├── mock-fs.js
├── package.json
├── public/
│   ├── icon/
│   │   └── icon.icns
│   ├── index.html
│   └── manifest.json
├── scripts/
│   ├── component.js
│   ├── electron-wait-react.js
│   ├── redux.js
│   ├── template/
│   │   ├── store/
│   │   │   ├── actions.tmpl
│   │   │   ├── epics.tmpl
│   │   │   ├── reducers.tmpl
│   │   │   └── store.tmpl
│   │   └── useActions.tmpl
│   └── type.js
├── src/
│   ├── App.tsx
│   ├── components/
│   │   ├── AppRegion/
│   │   │   ├── AppRegion.scss
│   │   │   ├── AppRegion.tsx
│   │   │   ├── WindowsTitleBar.tsx
│   │   │   └── index.ts
│   │   ├── KeyboardShortcuts/
│   │   │   ├── KeyboardShortcuts.scss
│   │   │   ├── KeyboardShortcuts.tsx
│   │   │   ├── index.ts
│   │   │   └── shortcuts.json
│   │   ├── Mui/
│   │   │   ├── DeleteIcon.tsx
│   │   │   ├── Dialog/
│   │   │   │   ├── ConfirmDialog.tsx
│   │   │   │   ├── Dialog.scss
│   │   │   │   ├── FormDialog.tsx
│   │   │   │   ├── FullScreenDialog.tsx
│   │   │   │   └── index.ts
│   │   │   ├── Dropdown/
│   │   │   │   ├── Dropdown.scss
│   │   │   │   ├── Dropdown.tsx
│   │   │   │   └── index.ts
│   │   │   ├── EditIcon.tsx
│   │   │   ├── IconButton/
│   │   │   │   ├── IconButton.scss
│   │   │   │   ├── IconButton.tsx
│   │   │   │   └── index.ts
│   │   │   ├── Input/
│   │   │   │   ├── Input.scss
│   │   │   │   ├── Input.tsx
│   │   │   │   └── index.ts
│   │   │   ├── Menu/
│   │   │   │   ├── Menu.scss
│   │   │   │   ├── Menu.tsx
│   │   │   │   ├── MenuItem.tsx
│   │   │   │   ├── index.ts
│   │   │   │   └── useMuiMenu.ts
│   │   │   ├── Tooltip.tsx
│   │   │   └── index.ts
│   │   ├── Preferences/
│   │   │   ├── AccentColor.tsx
│   │   │   ├── Preferences.scss
│   │   │   ├── Preferences.tsx
│   │   │   ├── Storage.tsx
│   │   │   ├── ThemeSelector.tsx
│   │   │   ├── TitleBarSelector.tsx
│   │   │   └── index.ts
│   │   ├── PrivateRoute.tsx
│   │   └── Switch/
│   │       ├── Switch.scss
│   │       ├── Switch.tsx
│   │       └── index.ts
│   ├── constants/
│   │   ├── index.ts
│   │   └── paths.json
│   ├── date.d.ts
│   ├── hooks/
│   │   ├── crud-reducer/
│   │   │   ├── bindDispatch.ts
│   │   │   ├── crudAction.ts
│   │   │   ├── crudReducer.ts
│   │   │   ├── crudSelector.ts
│   │   │   ├── index.ts
│   │   │   ├── useActions.ts
│   │   │   └── useCRUDReducer.ts
│   │   ├── useActions.ts
│   │   ├── useBoolean.ts
│   │   └── useMouseTrap.ts
│   ├── index.scss
│   ├── index.tsx
│   ├── pages/
│   │   ├── Auth/
│   │   │   ├── Auth.scss
│   │   │   ├── Auth.tsx
│   │   │   ├── FileUpload.tsx
│   │   │   └── index.ts
│   │   └── TaskList/
│   │       ├── CompletedTaskList/
│   │       │   ├── CompletedTaskList.scss
│   │       │   ├── CompletedTaskList.tsx
│   │       │   └── index.ts
│   │       ├── NewTask/
│   │       │   ├── NewTask.scss
│   │       │   ├── NewTask.tsx
│   │       │   └── index.ts
│   │       ├── Task/
│   │       │   ├── CompletedTask.tsx
│   │       │   ├── DatePicker/
│   │       │   │   ├── DatePicker.scss
│   │       │   │   ├── DatePicker.tsx
│   │       │   │   └── index.ts
│   │       │   ├── DateTimeDialog/
│   │       │   │   ├── DateTimeDialog.scss
│   │       │   │   ├── DateTimeDialog.tsx
│   │       │   │   └── index.ts
│   │       │   ├── Task.scss
│   │       │   ├── Task.tsx
│   │       │   ├── TaskInput.tsx
│   │       │   ├── TodoTask/
│   │       │   │   ├── TodoTask.scss
│   │       │   │   ├── TodoTask.tsx
│   │       │   │   ├── TodoTaskMenu.tsx
│   │       │   │   └── index.ts
│   │       │   ├── TodoTaskDetails/
│   │       │   │   ├── DateTimeButton.tsx
│   │       │   │   ├── TodoTaskDetails.scss
│   │       │   │   ├── TodoTaskDetails.tsx
│   │       │   │   └── index.ts
│   │       │   ├── ToggleCompleted.tsx
│   │       │   └── index.ts
│   │       ├── TaskList.scss
│   │       ├── TaskList.tsx
│   │       ├── TaskListDropdown/
│   │       │   ├── TaskListDropdown.scss
│   │       │   ├── TaskListDropdown.tsx
│   │       │   ├── TaskListDropdownItem.tsx
│   │       │   └── index.ts
│   │       ├── TaskListHeader/
│   │       │   ├── TaskListHeader.scss
│   │       │   ├── TaskListHeader.tsx
│   │       │   └── index.ts
│   │       ├── TaskListMenu.tsx
│   │       ├── TodoTaskList/
│   │       │   ├── TodoTaskList.scss
│   │       │   ├── TodoTaskList.tsx
│   │       │   ├── TodoTaskListByDate.tsx
│   │       │   └── index.ts
│   │       └── index.ts
│   ├── react-app-env.d.ts
│   ├── scss/
│   │   ├── _functions.scss
│   │   ├── _mixins.scss
│   │   ├── _platform.scss
│   │   ├── _theme.scss
│   │   ├── _variables.scss
│   │   ├── index.scss
│   │   └── mixins/
│   │       ├── _animation.scss
│   │       ├── _background.scss
│   │       ├── _border.scss
│   │       ├── _electron.scss
│   │       ├── _flex.scss
│   │       ├── _font.scss
│   │       ├── _position.scss
│   │       ├── _size.scss
│   │       ├── _textHighlight.scss
│   │       └── _textOverflow.scss
│   ├── service/
│   │   ├── auth.ts
│   │   ├── index.ts
│   │   ├── task.ts
│   │   └── tasksList.ts
│   ├── serviceWorker.ts
│   ├── store/
│   │   ├── actions/
│   │   │   ├── auth.ts
│   │   │   ├── index.ts
│   │   │   ├── preferences.ts
│   │   │   ├── task.ts
│   │   │   └── taskList.ts
│   │   ├── epics/
│   │   │   ├── auth.ts
│   │   │   ├── index.ts
│   │   │   ├── preferences.ts
│   │   │   ├── task.ts
│   │   │   └── taskList.ts
│   │   ├── index.ts
│   │   ├── reducers/
│   │   │   ├── auth.ts
│   │   │   ├── index.ts
│   │   │   ├── preferences.ts
│   │   │   ├── task.ts
│   │   │   └── taskList.ts
│   │   └── selectors/
│   │       ├── index.ts
│   │       ├── preferences.ts
│   │       ├── task.ts
│   │       └── taskList.ts
│   ├── theme.ts
│   ├── typings/
│   │   └── index.ts
│   └── utils/
│       ├── date.ts
│       ├── form/
│       │   ├── form.ts
│       │   ├── index.ts
│       │   ├── typings.ts
│       │   └── validators.ts
│       ├── nprogress.ts
│       └── uuid.ts
└── tsconfig.json
Download .txt
SYMBOL INDEX (258 symbols across 84 files)

FILE: common.d.ts
  type Theme (line 1) | type Theme = 'light' | 'dark';
  type AccentColor (line 2) | type AccentColor = 'red' | 'blue' | 'amber' | 'green' | 'purple' | 'grey';
  type TitleBar (line 3) | type TitleBar = 'native' | 'frameless';
  type Schema$Storage (line 5) | interface Schema$Storage<T> {
  type OAuthKeys (line 10) | interface OAuthKeys {
  type SyncConfig (line 21) | interface SyncConfig {
  type Schema$Preferences (line 27) | type Schema$Preferences = {
  type Window (line 35) | interface Window {

FILE: electron/main.ts
  function createWindow (line 21) | async function createWindow() {

FILE: electron/menu.ts
  method click (line 17) | click() {
  method click (line 23) | click() {
  class MenuBuilder (line 32) | class MenuBuilder {
    method constructor (line 35) | constructor(mainWindow: BrowserWindow) {
    method buildMenu (line 39) | buildMenu() {
    method setupDevelopmentEnvironment (line 58) | setupDevelopmentEnvironment() {
    method buildDarwinTemplate (line 74) | buildDarwinTemplate(): MenuItemConstructorOptions[] {
    method buildDefaultTemplate (line 160) | buildDefaultTemplate(): MenuItemConstructorOptions[] {

FILE: electron/preload/index.ts
  function relaunch (line 6) | function relaunch() {

FILE: electron/preload/theme.ts
  function handleOSTheme (line 19) | function handleOSTheme() {

FILE: electron/storage.ts
  function FileStorage (line 8) | function FileStorage<T extends {}>(path: string, defaultValue?: T): Sche...
  function initStorage (line 24) | function initStorage(app: App, nativeTheme: NativeTheme) {

FILE: mock-fs.js
  method readFile (line 4) | readFile() {}
  method readFileSync (line 5) | readFileSync() {}

FILE: scripts/electron-wait-react.js
  function uncaughtException (line 25) | function uncaughtException() {

FILE: src/components/AppRegion/AppRegion.tsx
  function AppRegion (line 10) | function AppRegion() {

FILE: src/components/AppRegion/WindowsTitleBar.tsx
  function toggleMaximize (line 20) | function toggleMaximize() {
  function WindowsTitleBar (line 24) | function WindowsTitleBar() {

FILE: src/components/KeyboardShortcuts/KeyboardShortcuts.tsx
  function normalizeKeyName (line 5) | function normalizeKeyName(str: string) {
  function KeyboardShortcuts (line 15) | function KeyboardShortcuts(props: FullScreenDialogProps) {

FILE: src/components/Mui/Dialog/ConfirmDialog.tsx
  type ConfirmDialogProps (line 6) | interface ConfirmDialogProps extends DialogProps {
  function ConfirmDialog (line 18) | function ConfirmDialog({

FILE: src/components/Mui/Dialog/FormDialog.tsx
  type Props (line 5) | interface Props extends Omit<ConfirmDialogProps, 'onConfirm' | 'confirmL...
  constant INPUT_NAME (line 11) | const INPUT_NAME = 'NAME';
  function FormDialog (line 13) | function FormDialog({

FILE: src/components/Mui/Dialog/FullScreenDialog.tsx
  type FullScreenDialogProps (line 8) | interface FullScreenDialogProps extends Omit<DialogProps, 'title'> {
  type ContainerProps (line 14) | interface ContainerProps {
  constant FULLSCREEN_DIALOG_TRANSITION (line 18) | const FULLSCREEN_DIALOG_TRANSITION = 300;
  function FullScreenDialog (line 30) | function FullScreenDialog({

FILE: src/components/Mui/Dropdown/Dropdown.tsx
  type DropdownProps (line 7) | interface DropdownProps extends Omit<MenuProps, 'onClick' | 'ref'> {

FILE: src/components/Mui/IconButton/IconButton.tsx
  type Props (line 6) | interface Props extends Partial<IconButtonProps> {
  function IconButton (line 26) | function IconButton({

FILE: src/components/Mui/Input/Input.tsx
  type InputProps (line 4) | type InputProps = Omit<InputBaseProps, 'ref'>;
  function Input (line 6) | function Input({ className = '', ...props }: InputProps) {

FILE: src/components/Mui/Menu/Menu.tsx
  type MenuProps (line 5) | interface MenuProps extends PopoverProps {

FILE: src/components/Mui/Menu/MenuItem.tsx
  type DefaultMenuItemProps (line 5) | type DefaultMenuItemProps = Parameters<typeof MuiMenuItem>[0];
  type MenuItemProps (line 7) | interface MenuItemProps extends DefaultMenuItemProps {
  type Props (line 11) | interface Props {
  function useMuiMenuItem (line 44) | function useMuiMenuItem({ onClose }: Props) {

FILE: src/components/Mui/Menu/useMuiMenu.ts
  type AnchorEl (line 10) | type AnchorEl = HTMLElement | null;
  type AnchorPosition (line 11) | type AnchorPosition = MenuProps['anchorPosition'];
  function instanceOfAnchorEl (line 13) | function instanceOfAnchorEl(object: any): object is AnchorEl {
  function instanceOfAnchorPosition (line 17) | function instanceOfAnchorPosition(object: any): object is AnchorPosition {
  function useMuiMenu (line 21) | function useMuiMenu() {

FILE: src/components/Mui/Tooltip.tsx
  type TooltipProps (line 7) | type TooltipProps = Omit<MuiTooltipProps, 'ref'>;
  function Tooltip (line 29) | function Tooltip(props: TooltipProps) {
  function ErrorTooltip (line 36) | function ErrorTooltip(props: TooltipProps) {

FILE: src/components/Preferences/AccentColor.tsx
  function AccentColor (line 14) | function AccentColor({ onChange }: Control) {

FILE: src/components/Preferences/Preferences.tsx
  function Preferences (line 30) | function Preferences(props: FullScreenDialogProps) {

FILE: src/components/Preferences/Storage.tsx
  function Storage (line 4) | function Storage() {

FILE: src/components/Preferences/ThemeSelector.tsx
  function ThemeSelector (line 5) | function ThemeSelector({ value, onChange }: Control<Theme>) {

FILE: src/components/Preferences/TitleBarSelector.tsx
  function TitleBarSelector (line 9) | function TitleBarSelector({ value, onChange }: Control<TitleBar>) {

FILE: src/components/PrivateRoute.tsx
  function PrivateRoute (line 7) | function PrivateRoute(props: RouteProps) {

FILE: src/components/Switch/Switch.tsx
  type Props (line 3) | interface Props {

FILE: src/date.d.ts
  type DayCn (line 1) | type DayCn = '日' | '一' | '二' | '三' | '四' | '五' | '六';
  type MonthFullName (line 2) | type MonthFullName =
  type MonthAbbr (line 16) | type MonthAbbr =
  type DayFullName (line 30) | type DayFullName =
  type DayAbbr (line 39) | type DayAbbr = 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thur' | 'Fri' | 'Sat';
  type DaySuffix (line 41) | type DaySuffix = 'th' | 'st' | 'nd' | 'rd';
  type Date (line 43) | interface Date {

FILE: src/hooks/crud-reducer/bindDispatch.ts
  type Dispatched (line 4) | type Dispatched<A extends ActionCreators> = {
  function bindDispatch (line 8) | function bindDispatch<A extends ActionCreators>(

FILE: src/hooks/crud-reducer/crudAction.ts
  type FilterFlags (line 4) | type FilterFlags<Base, Condition> = {
  type AllowedNames (line 8) | type AllowedNames<Base, Condition> = FilterFlags<
  type Key (line 13) | type Key<I> = AllowedNames<I, string>;
  type AnyAction (line 15) | interface AnyAction {
  type ActionCreators (line 20) | interface ActionCreators {
  type GetCreatorsAction (line 24) | type GetCreatorsAction<
  type CRUDActionType (line 28) | type CRUDActionType =
  type CRUDActionTypes (line 37) | type CRUDActionTypes<Type extends string = any> = {
  type CustomActionTypes (line 41) | type CustomActionTypes<
  type UpdatePayload (line 45) | type UpdatePayload<I, K extends Key<I>> = Partial<I> &
  type PaginatePayload (line 48) | type PaginatePayload<I> =
  type List (line 57) | type List<Type extends string, I> = {
  type Create (line 62) | type Create<Type extends string, I> = {
  type Update (line 67) | interface Update<Type extends string, I, K extends Key<I>> {
  type Delete (line 72) | interface Delete<Type extends string, I, K extends Key<I>> {
  type Paginate (line 77) | interface Paginate<Type extends string, I> {
  type Params (line 82) | interface Params<Type extends string> {
  type Reset (line 87) | interface Reset<Type extends string> {
  type CRUDActionCreators (line 91) | type CRUDActionCreators<
  type CRUDActions (line 107) | type CRUDActions<
  type ExtractAction (line 113) | type ExtractAction<
  function isAction (line 118) | function isAction<
  function getCRUDActionsCreator (line 141) | function getCRUDActionsCreator<I, K extends Key<I>>() {

FILE: src/hooks/crud-reducer/crudReducer.ts
  type CRUDState (line 13) | interface CRUDState<I, Prefill extends boolean = true> {
  type CRUDReducer (line 23) | type CRUDReducer<
  type CreateCRUDReducerOptions (line 33) | interface CreateCRUDReducerOptions<
  function parsePaginatePayload (line 49) | function parsePaginatePayload<I>(payload: PaginatePayload<I>) {
  function createCRUDReducer (line 77) | function createCRUDReducer<
  function removeFromArray (line 231) | function removeFromArray<T>(arr: T[], index: number) {

FILE: src/hooks/crud-reducer/crudSelector.ts
  type PaginateState (line 3) | interface PaginateState<S extends CRUDState<unknown, any>> {
  function paginateSelector (line 13) | function paginateSelector<S extends CRUDState<unknown, any>>({

FILE: src/hooks/crud-reducer/useActions.ts
  function useActions (line 6) | function useActions<A extends ActionCreators>(

FILE: src/hooks/crud-reducer/useCRUDReducer.ts
  type UseCRUDReducer (line 12) | type UseCRUDReducer<
  function createUseCRUDReducer (line 28) | function createUseCRUDReducer<I, K extends Key<I>>(

FILE: src/hooks/useActions.ts
  type ActionCreators (line 5) | interface ActionCreators {
  type Handler (line 9) | type Handler<A extends ActionCreators> = {
  function withDispatch (line 13) | function withDispatch<A extends ActionCreators>(
  function useActions (line 28) | function useActions<A extends ActionCreators>(creators: A): Handler<A> {

FILE: src/hooks/useBoolean.ts
  function useBoolean (line 3) | function useBoolean(initialState = false) {

FILE: src/hooks/useMouseTrap.ts
  type Params (line 4) | type Params = Parameters<MousetrapStatic['bind']>;
  function useMouseTrap (line 6) | function useMouseTrap(

FILE: src/index.tsx
  function render (line 18) | function render() {

FILE: src/pages/Auth/Auth.tsx
  function Auth (line 10) | function Auth() {

FILE: src/pages/Auth/FileUpload.tsx
  function readFile (line 6) | function readFile(file: File) {
  function onSuccess (line 22) | function onSuccess(payload: OAuthKeys) {
  function FileUpload (line 27) | function FileUpload() {

FILE: src/pages/TaskList/CompletedTaskList/CompletedTaskList.tsx
  function CompletedTaskList (line 10) | function CompletedTaskList() {

FILE: src/pages/TaskList/NewTask/NewTask.tsx
  function NewTask (line 11) | function NewTask() {

FILE: src/pages/TaskList/Task/CompletedTask.tsx
  type Props (line 7) | interface Props extends TaskProps {}
  function CompletedTask (line 9) | function CompletedTask(props: Props) {

FILE: src/pages/TaskList/Task/DatePicker/DatePicker.tsx
  type GridProps (line 8) | type GridProps = HTMLAttributes<HTMLDivElement>;
  type DateProps (line 10) | interface DateProps extends Omit<GridProps, 'onClick'> {
  type Props (line 16) | interface Props {
  function DatePicker (line 46) | function DatePicker({ value, onChange }: Props) {
  function getDisplayData (line 101) | function getDisplayData(dateObj: Date = new Date()) {

FILE: src/pages/TaskList/Task/DateTimeDialog/DateTimeDialog.tsx
  type Props (line 15) | interface Props {
  type Control (line 20) | type Control = Omit<
  type DateTimeDialogContext (line 25) | interface DateTimeDialogContext {
  function useDateTimeDialog (line 35) | function useDateTimeDialog() {
  function DateTimeDialogProvider (line 39) | function DateTimeDialogProvider({ children }: { children: ReactNode }) {

FILE: src/pages/TaskList/Task/Task.tsx
  type TaskProps (line 8) | interface TaskProps

FILE: src/pages/TaskList/Task/TaskInput.tsx
  type TaskInputProps (line 6) | interface TaskInputProps extends Pick<Schema$Task, 'due' | 'notes'> {
  type Props (line 10) | type Props = TaskInputProps & InputProps;
  function TaskInput (line 12) | function TaskInput({
  function dateFormat (line 35) | function dateFormat(d: Date) {

FILE: src/pages/TaskList/Task/TodoTask/TodoTask.tsx
  type TodoTaskProps (line 22) | interface TodoTaskProps extends TaskProps {

FILE: src/pages/TaskList/Task/TodoTask/TodoTaskMenu.tsx
  type Props (line 17) | interface Props {
  type TodoTaskMenuContext (line 25) | interface TodoTaskMenuContext {
  type Control (line 29) | type Control = Omit<MenuProps, 'ref'>;
  function useTodoTaskMenu (line 35) | function useTodoTaskMenu() {
  function TodoTaskMenuProvider (line 39) | function TodoTaskMenuProvider({ children }: { children: ReactNode }) {

FILE: src/pages/TaskList/Task/TodoTaskDetails/DateTimeButton.tsx
  type Props (line 6) | interface Props {

FILE: src/pages/TaskList/Task/TodoTaskDetails/TodoTaskDetails.tsx
  type Props (line 30) | interface Props extends Pick<Schema$Task, 'uuid'> {
  type TodoTaskDetailsContext (line 35) | interface TodoTaskDetailsContext {
  function useTodoTaskDetails (line 48) | function useTodoTaskDetails() {
  function TodoTaskDetailsProvider (line 52) | function TodoTaskDetailsProvider({ children }: { children: ReactNode }) {
  function TodoTaskDetails (line 89) | function TodoTaskDetails({

FILE: src/pages/TaskList/Task/ToggleCompleted.tsx
  type Props (line 9) | interface Props extends Pick<Schema$Task, 'uuid'> {
  function ToggleCompleted (line 25) | function ToggleCompleted({ uuid, isEmpty }: Props) {

FILE: src/pages/TaskList/TaskList.tsx
  function TaskList (line 17) | function TaskList() {

FILE: src/pages/TaskList/TaskListDropdown/TaskListDropdown.tsx
  type TaskListDropdownProps (line 13) | interface TaskListDropdownProps
  function TaskListDropdown (line 22) | function TaskListDropdown({

FILE: src/pages/TaskList/TaskListDropdown/TaskListDropdownItem.tsx
  type Props (line 7) | interface Props extends Omit<MenuItemProps, 'onClick'> {
  function TaskListDropdownItem (line 13) | function TaskListDropdownItem({

FILE: src/pages/TaskList/TaskListHeader/TaskListHeader.tsx
  type Props (line 11) | interface Props {
  function TaskListHeader (line 15) | function TaskListHeader({ onConfirm }: Props) {

FILE: src/pages/TaskList/TaskListMenu.tsx
  type Props (line 24) | interface Props extends Omit<MenuProps, 'ref'> {}
  function selector (line 28) | function selector(state: RootState) {
  function TaskListMenu (line 38) | function TaskListMenu({ onClose, ...props }: Props) {

FILE: src/pages/TaskList/TodoTaskList/TodoTaskList.tsx
  type InsertAfter (line 13) | interface InsertAfter {
  type SortableListProps (line 17) | interface SortableListProps extends InsertAfter {
  function TodoTaskListByOrder (line 51) | function TodoTaskListByOrder() {
  function TodoTaskList (line 88) | function TodoTaskList({ taskListId = '' }: { taskListId?: string }) {

FILE: src/pages/TaskList/TodoTaskList/TodoTaskListByDate.tsx
  function TodoTaskListByDate (line 12) | function TodoTaskListByDate() {

FILE: src/react-app-env.d.ts
  type Window (line 6) | interface Window {
  type Module (line 12) | interface Module {

FILE: src/service/auth.ts
  constant SCOPES (line 14) | const SCOPES = ['https://www.googleapis.com/auth/tasks'];
  function generateAuthUrl (line 23) | function generateAuthUrl() {
  function authenticate (line 33) | function authenticate() {
  function getToken (line 40) | async function getToken(code: string) {

FILE: src/service/task.ts
  function getAllTasks (line 28) | function getAllTasks(

FILE: src/service/tasksList.ts
  function getAllTasklist (line 12) | function getAllTasklist(

FILE: src/serviceWorker.ts
  type Config (line 23) | type Config = {
  function register (line 28) | function register(config?: Config) {
  function registerValidSW (line 65) | function registerValidSW(swUrl: string, config?: Config) {
  function checkValidServiceWorker (line 109) | function checkValidServiceWorker(swUrl: string, config?: Config) {
  function unregister (line 137) | function unregister() {

FILE: src/store/actions/auth.ts
  function authenticated (line 3) | function authenticated() {
  function logout (line 9) | function logout() {
  type AuthActions (line 17) | type AuthActions = GetCreatorsAction<typeof actions>;

FILE: src/store/actions/preferences.ts
  function updatePreferences (line 4) | function updatePreferences(payload: DeepPartial<Schema$Preferences>) {
  type UpdatePreferences (line 11) | type UpdatePreferences = ReturnType<typeof updatePreferences>;
  type PreferenceActions (line 12) | type PreferenceActions = UpdatePreferences;

FILE: src/store/actions/task.ts
  type Payload$CreateTask (line 12) | interface Payload$CreateTask extends Partial<Schema$Task> {
  type Payload$MoveTask (line 17) | interface Payload$MoveTask {
  type Payload$MoveToAnotherList (line 23) | interface Payload$MoveToAnotherList {
  function getTasks (line 49) | function getTasks(payload: tasks_v1.Params$Resource$Tasks$List) {
  function createTask (line 56) | function createTask(payload: Payload$CreateTask = {}) {
  function deleteTask (line 66) | function deleteTask(payload: { uuid: string; prevTaskIndex?: number }) {
  function setFocus (line 73) | function setFocus(payload?: string | number | null) {
  function createTaskSuccess (line 80) | function createTaskSuccess(payload: Schema$Task) {
  function updateTaskSuccess (line 88) | function updateTaskSuccess(payload: UpdatePayload<Schema$Task, 'uuid'>) {
  function moveTask (line 95) | function moveTask(payload: Payload$MoveTask) {
  function moveTaskSuccess (line 102) | function moveTaskSuccess() {
  function deleteAllCompletedTasks (line 108) | function deleteAllCompletedTasks() {
  function deleteAllCompletedTasksSuccess (line 114) | function deleteAllCompletedTasksSuccess() {
  function syncTasks (line 120) | function syncTasks(payload: PaginatePayload<Schema$Task>) {
  function moveToAnotherList (line 124) | function moveToAnotherList(payload: Payload$MoveToAnotherList) {
  type TaskActions (line 140) | type TaskActions =

FILE: src/store/actions/taskList.ts
  function getTaskLists (line 27) | function getTaskLists(
  function newTaskList (line 33) | function newTaskList(payload: string) {
  function deleteCurrTaskList (line 37) | function deleteCurrTaskList() {
  function sortTaskListBy (line 41) | function sortTaskListBy(payload: {
  function syncTaskList (line 48) | function syncTaskList(payload: PaginatePayload<Schema$TaskList>) {
  type TaskListActions (line 60) | type TaskListActions =

FILE: src/store/epics/auth.ts
  type Actions (line 11) | type Actions = AuthActions | RouterAction;
  type AuthEpic (line 12) | type AuthEpic = Epic<Actions, Actions, RootState>;

FILE: src/store/epics/preferences.ts
  type Actions (line 15) | type Actions = TaskListActions | TaskActions | PreferenceActions;
  type PreferencesEpic (line 16) | type PreferencesEpic = Epic<Actions, Actions, RootState>;

FILE: src/store/epics/task.ts
  type Actions (line 33) | type Actions = TaskActions | RouterAction;
  type TaskEpic (line 34) | type TaskEpic = Epic<Actions, Actions, RootState>;

FILE: src/store/epics/taskList.ts
  type Actions (line 14) | type Actions = TaskListActions | RouterAction;
  type TaskEpic (line 15) | type TaskEpic = Epic<Actions, Actions, RootState>;

FILE: src/store/index.ts
  function configureStore (line 16) | function configureStore() {

FILE: src/store/reducers/auth.ts
  type State (line 3) | interface State {

FILE: src/store/reducers/index.ts
  type RootState (line 17) | type RootState = ReturnType<ReturnType<typeof rootReducer>>;

FILE: src/store/reducers/preferences.ts
  type State (line 3) | interface State extends Schema$Preferences {}
  function preferencesReducer (line 10) | function preferencesReducer(

FILE: src/store/reducers/task.ts
  type State (line 9) | interface State {
  function taskReducer (line 30) | function taskReducer(
  function move (line 232) | function move<T>(arr: T[], from: number, to: number) {

FILE: src/store/reducers/taskList.ts
  type DefaultState (line 10) | type DefaultState = typeof defaultState;
  type State (line 12) | interface State extends DefaultState {
  function taskListReducer (line 28) | function taskListReducer(

FILE: src/typings/index.ts
  type Schema$Task (line 3) | interface Schema$Task {
  type Schema$TaskList (line 23) | interface Schema$TaskList extends tasks_v1.Schema$TaskList {
  type ExtractAction (line 27) | type ExtractAction<

FILE: src/utils/form/form.ts
  type HTMLDivProps (line 11) | type HTMLDivProps = React.DetailedHTMLProps<
  type HTMLLabelProps (line 16) | type HTMLLabelProps = React.DetailedHTMLProps<
  type FieldData (line 21) | interface FieldData<S extends {} = Store, Name = NamePath<S>>
  type FormInstance (line 31) | type FormInstance<S extends {} = Store> = {
  type FormProps (line 52) | interface FormProps<S extends {} = Store, V = S>
  type OmititedRcFieldProps (line 63) | type OmititedRcFieldProps = Omit<
  type FormItemLabelProps (line 68) | interface FormItemLabelProps extends HTMLDivProps {
  type BasicFormItemProps (line 72) | interface BasicFormItemProps<S extends {} = Store>
  type Deps (line 84) | type Deps<S> = Array<NamePath<S>>;
  type FormItemPropsDeps (line 85) | type FormItemPropsDeps<S extends {} = Store> =
  type FormItemProps (line 100) | type FormItemProps<S extends {} = Store> = BasicFormItemProps<S> &
  type FormItemClassName (line 103) | interface FormItemClassName {
  type Rule (line 112) | type Rule = NonNullable<RcFieldProps['rules']>[number];
  function createShouldUpdate (line 117) | function createShouldUpdate(
  function createForm (line 140) | function createForm<S extends {} = Store, V = S>({

FILE: src/utils/form/typings.ts
  type ValueOf (line 1) | type ValueOf<T> = T[keyof T];
  type Cons (line 3) | type Cons<H, T> = T extends readonly any[]
  type Prev (line 9) | type Prev = [
  type Paths (line 36) | type Paths<T, D extends number = 10> = [D] extends [never]
  type DeepPartial (line 54) | type DeepPartial<T> = T extends any[] | (() => void)
  type NextInt (line 62) | interface NextInt {
  type PathType (line 71) | type PathType<T, P extends any[], Index extends keyof P & number = 0> = {
  type NamePath (line 81) | type NamePath<T, D extends number = 4> = keyof T | Paths<T, D>;
  type Control (line 83) | interface Control<T = any> {
  type ControlProps (line 88) | interface ControlProps<T = unknown> extends Control<T> {

FILE: src/utils/form/validators.ts
  type Validator (line 1) | type Validator = (rule: any, value: any) => Promise<void>;

FILE: src/utils/uuid.ts
  class UUID (line 1) | class UUID {
    method next (line 4) | next() {
    method reset (line 10) | reset() {
Condensed preview — 185 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (298K chars).
[
  {
    "path": ".eslintcache",
    "chars": 40284,
    "preview": "[{\"/Users/Pong/Desktop/google-tasks-desktop/src/index.tsx\":\"1\",\"/Users/Pong/Desktop/google-tasks-desktop/src/theme.ts\":\""
  },
  {
    "path": ".eslintignore",
    "chars": 742,
    "preview": "# Logs\nlogs\n*.log\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nl"
  },
  {
    "path": ".gitignore",
    "chars": 377,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": ".prettierrc",
    "chars": 238,
    "preview": "{\n  \"overrides\": [\n    {\n      \"files\": [\".prettierrc\", \".babelrc\", \".eslintrc\", \".stylelintrc\"],\n      \"options\": {\n   "
  },
  {
    "path": "LICENSE",
    "chars": 1084,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2015-present C. T. Lin\n\nPermission is hereby granted, free of charge, to any person"
  },
  {
    "path": "Procfile",
    "chars": 89,
    "preview": "react: npm run app:dev\nelectron: node scripts/electron-wait-react.js && yarn electron:dev"
  },
  {
    "path": "README.md",
    "chars": 2521,
    "preview": "## Google Tasks Desktop\n\n> Unofficial google tasks desktop application. Using React and google tasks api\n\n<div>\n  <img s"
  },
  {
    "path": "common.d.ts",
    "chars": 1357,
    "preview": "type Theme = 'light' | 'dark';\ntype AccentColor = 'red' | 'blue' | 'amber' | 'green' | 'purple' | 'grey';\ntype TitleBar "
  },
  {
    "path": "config-overrides.js",
    "chars": 1249,
    "preview": "const path = require('path');\n\n/** @typedef {import('webpack').Configuration} Configuration */\n\n/**\n * @param {Configura"
  },
  {
    "path": "electron/electron.d.ts",
    "chars": 46,
    "preview": "declare module 'electron-devtools-installer';\n"
  },
  {
    "path": "electron/main.ts",
    "chars": 2315,
    "preview": "import path from 'path';\nimport url from 'url';\nimport {\n  app,\n  shell,\n  nativeTheme,\n  BrowserWindow,\n  BrowserWindow"
  },
  {
    "path": "electron/menu.ts",
    "chars": 5162,
    "preview": "// borrow from\n// https://github.com/electron-react-boilerplate/examples/blob/master/examples/typescript/app/main.dev.ts"
  },
  {
    "path": "electron/preload/index.ts",
    "chars": 1370,
    "preview": "import fs from 'fs';\nimport { remote } from 'electron';\nimport { handleOSTheme } from './theme';\nimport { initStorage } "
  },
  {
    "path": "electron/preload/theme.ts",
    "chars": 628,
    "preview": "import { remote } from 'electron';\n\nexport const setTheme = (newTheme?: Theme) => {\n  const preferences = window.prefere"
  },
  {
    "path": "electron/storage.ts",
    "chars": 2466,
    "preview": "import fs from 'fs';\nimport path from 'path';\nimport { App, NativeTheme } from 'electron';\n\nfunction FileStorage<T exten"
  },
  {
    "path": "electron/tsconfig.json",
    "chars": 251,
    "preview": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"noEmit\": false,\n    \"rootDir\": "
  },
  {
    "path": "mock-fs.js",
    "chars": 153,
    "preview": "// https://github.com/googleapis/google-api-nodejs-client/issues/1775#issuecomment-520572247\n\nmodule.exports = {\n  readF"
  },
  {
    "path": "package.json",
    "chars": 5061,
    "preview": "{\n  \"name\": \"google-tasks-desktop\",\n  \"version\": \"3.1.3\",\n  \"scripts\": {\n    \"dev\": \"nf start\",\n    \"build\": \"yarn app:b"
  },
  {
    "path": "public/index.html",
    "chars": 1776,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"shortcut icon\" href=\"%PUBLIC_URL%/"
  },
  {
    "path": "public/manifest.json",
    "chars": 312,
    "preview": "{\n  \"short_name\": \"Google Tasks\",\n  \"name\": \"Create Google Tasks Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\","
  },
  {
    "path": "scripts/component.js",
    "chars": 1379,
    "preview": "const fs = require('fs');\nconst path = require('path');\nconst pkg = require('../package.json');\n\nconst useTypescript =\n "
  },
  {
    "path": "scripts/electron-wait-react.js",
    "chars": 811,
    "preview": "// @ts-check\nconst exec = require('child_process').exec;\nconst net = require('net');\n\nlet client = new net.Socket();\ncon"
  },
  {
    "path": "scripts/redux.js",
    "chars": 1624,
    "preview": "const fs = require('fs');\nconst path = require('path');\nconst { exec } = require('child_process');\nconst [, , ...args] ="
  },
  {
    "path": "scripts/template/store/actions.tmpl",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "scripts/template/store/epics.tmpl",
    "chars": 83,
    "preview": "import { combineEpics } from 'redux-observable';\n\nexport default combineEpics(\n\n);\n"
  },
  {
    "path": "scripts/template/store/reducers.tmpl",
    "chars": 189,
    "preview": "import { combineReducers } from 'redux';\n\nconst rootReducer = () => combineReducers({\n\n});\n\nexport type RootState = Retu"
  },
  {
    "path": "scripts/template/store/store.tmpl",
    "chars": 1219,
    "preview": "import { createStore, applyMiddleware, compose } from 'redux';\nimport { createEpicMiddleware, Epic } from 'redux-observa"
  },
  {
    "path": "scripts/template/useActions.tmpl",
    "chars": 921,
    "preview": "import { useMemo, useRef, Dispatch as ReactDispatch } from 'react';\nimport { AnyAction, Dispatch } from 'redux';\nimport "
  },
  {
    "path": "scripts/type.js",
    "chars": 597,
    "preview": "const { exec } = require('child_process');\n\nconst [, , ...args] = process.argv;\n\nconst install = pkg => {\n  return `yarn"
  },
  {
    "path": "src/App.tsx",
    "chars": 637,
    "preview": "import React from 'react';\nimport { Route, Switch, Redirect, generatePath } from 'react-router-dom';\nimport { AppRegion "
  },
  {
    "path": "src/components/AppRegion/AppRegion.scss",
    "chars": 862,
    "preview": ".app-region {\n  @include absolute(0, null, 0);\n  @include dimen(100%, var(--header-height));\n\n  .simple-title-bar {\n    "
  },
  {
    "path": "src/components/AppRegion/AppRegion.tsx",
    "chars": 944,
    "preview": "import React, { ReactNode } from 'react';\nimport { useSelector } from 'react-redux';\nimport { IconButton } from '../Mui'"
  },
  {
    "path": "src/components/AppRegion/WindowsTitleBar.tsx",
    "chars": 1437,
    "preview": "import React, { useState, useEffect, Suspense } from 'react';\nimport { useSelector } from 'react-redux';\nimport { themeS"
  },
  {
    "path": "src/components/AppRegion/index.ts",
    "chars": 109,
    "preview": "import './AppRegion.scss';\n\nexport * from './AppRegion';\nexport { AppRegion as default } from './AppRegion';\n"
  },
  {
    "path": "src/components/KeyboardShortcuts/KeyboardShortcuts.scss",
    "chars": 215,
    "preview": ".keyboard-shortcuts {\n  color: var(--text-color);\n\n  .keyboard-shortcuts-label {\n    @include dimen(100%);\n  }\n\n  .keybo"
  },
  {
    "path": "src/components/KeyboardShortcuts/KeyboardShortcuts.tsx",
    "chars": 1156,
    "preview": "import React from 'react';\nimport { FullScreenDialog, FullScreenDialogProps } from '../Mui/Dialog';\nimport shortcuts fro"
  },
  {
    "path": "src/components/KeyboardShortcuts/index.ts",
    "chars": 141,
    "preview": "import './KeyboardShortcuts.scss';\n\nexport * from './KeyboardShortcuts';\nexport { KeyboardShortcuts as default } from '."
  },
  {
    "path": "src/components/KeyboardShortcuts/shortcuts.json",
    "chars": 328,
    "preview": "{\n  \"Actions\": [\n    { \"label\": \"Add a task\", \"key\": \"Enter\" },\n    { \"label\": \"Focus on previous/next task\", \"key\": \"Up"
  },
  {
    "path": "src/components/Mui/DeleteIcon.tsx",
    "chars": 414,
    "preview": "import React from 'react';\nimport SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';\n\nexport const DeleteIcon ="
  },
  {
    "path": "src/components/Mui/Dialog/ConfirmDialog.tsx",
    "chars": 1528,
    "preview": "import React, { useMemo, ReactNode } from 'react';\nimport Button from '@material-ui/core/Button';\nimport Dialog, { Dialo"
  },
  {
    "path": "src/components/Mui/Dialog/Dialog.scss",
    "chars": 2705,
    "preview": ".mui-dialog-paper.mui-dialog-paper {\n  @include dimen(100%);\n  @include margin-x(25px);\n  color: var(--text--color);\n  b"
  },
  {
    "path": "src/components/Mui/Dialog/FormDialog.tsx",
    "chars": 1797,
    "preview": "import React, { useCallback, useState, useRef, FormEvent } from 'react';\nimport { ConfirmDialog, ConfirmDialogProps } fr"
  },
  {
    "path": "src/components/Mui/Dialog/FullScreenDialog.tsx",
    "chars": 1991,
    "preview": "import React, { ReactNode } from 'react';\nimport Dialog, { DialogProps } from '@material-ui/core/Dialog';\nimport { Slide"
  },
  {
    "path": "src/components/Mui/Dialog/index.ts",
    "chars": 124,
    "preview": "import './Dialog.scss';\n\nexport * from './ConfirmDialog';\nexport * from './FormDialog';\nexport * from './FullScreenDialo"
  },
  {
    "path": "src/components/Mui/Dropdown/Dropdown.scss",
    "chars": 271,
    "preview": ".mui-dropdown-button {\n  &.mui-dropdown-button {\n    @include flex(center, space-between);\n    font-size: inherit;\n    t"
  },
  {
    "path": "src/components/Mui/Dropdown/Dropdown.tsx",
    "chars": 1618,
    "preview": "import React, { useMemo, ReactNode, MouseEvent, forwardRef } from 'react';\nimport { Menu, MenuProps } from '../Menu';\nim"
  },
  {
    "path": "src/components/Mui/Dropdown/index.ts",
    "chars": 105,
    "preview": "import './Dropdown.scss';\n\nexport * from './Dropdown';\nexport { Dropdown as default } from './Dropdown';\n"
  },
  {
    "path": "src/components/Mui/EditIcon.tsx",
    "chars": 439,
    "preview": "import React from 'react';\nimport SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';\n\nexport const EditIcon = R"
  },
  {
    "path": "src/components/Mui/IconButton/IconButton.scss",
    "chars": 75,
    "preview": ".mui-icon-button.mui-icon-button {\n  color: var(--text-secondary-color);\n}\n"
  },
  {
    "path": "src/components/Mui/IconButton/IconButton.tsx",
    "chars": 1054,
    "preview": "import React, { ComponentType, ReactElement } from 'react';\nimport { SvgIconProps } from '@material-ui/core/SvgIcon';\nim"
  },
  {
    "path": "src/components/Mui/IconButton/index.ts",
    "chars": 113,
    "preview": "import './IconButton.scss';\n\nexport * from './IconButton';\nexport { IconButton as default } from './IconButton';\n"
  },
  {
    "path": "src/components/Mui/Input/Input.scss",
    "chars": 533,
    "preview": ".mui-input-base.mui-input-base {\n  font-size: inherit;\n  min-height: 40px;\n  padding: 1px 0px 0px 10px;\n\n  input,\n  text"
  },
  {
    "path": "src/components/Mui/Input/Input.tsx",
    "chars": 536,
    "preview": "import React, { useMemo } from 'react';\nimport InputBase, { InputBaseProps } from '@material-ui/core/InputBase';\n\nexport"
  },
  {
    "path": "src/components/Mui/Input/index.ts",
    "chars": 93,
    "preview": "import './Input.scss';\n\nexport * from './Input';\nexport { Input as default } from './Input';\n"
  },
  {
    "path": "src/components/Mui/Menu/Menu.scss",
    "chars": 786,
    "preview": ".mui-menu-paper {\n  &.mui-menu-paper {\n    border-radius: 8px;\n    background-color: var(--paper-background-color);\n    "
  },
  {
    "path": "src/components/Mui/Menu/Menu.tsx",
    "chars": 824,
    "preview": "import React, { useMemo, ReactNode } from 'react';\nimport Popover, { PopoverProps } from '@material-ui/core/Popover';\nim"
  },
  {
    "path": "src/components/Mui/Menu/MenuItem.tsx",
    "chars": 1300,
    "preview": "import React, { forwardRef, useCallback, MouseEvent } from 'react';\nimport MuiMenuItem from '@material-ui/core/MenuItem'"
  },
  {
    "path": "src/components/Mui/Menu/index.ts",
    "chars": 147,
    "preview": "import './Menu.scss';\n\nexport * from './Menu';\nexport * from './MenuItem';\nexport * from './useMuiMenu';\nexport { Menu a"
  },
  {
    "path": "src/components/Mui/Menu/useMuiMenu.ts",
    "chars": 1736,
    "preview": "import {\n  useState,\n  useCallback,\n  useEffect,\n  SyntheticEvent,\n  MouseEvent\n} from 'react';\nimport { MenuProps } fro"
  },
  {
    "path": "src/components/Mui/Tooltip.tsx",
    "chars": 1019,
    "preview": "import React from 'react';\nimport { makeStyles, Theme } from '@material-ui/core/styles';\nimport MuiTooltip, {\n  TooltipP"
  },
  {
    "path": "src/components/Mui/index.ts",
    "chars": 218,
    "preview": "export * from './DeleteIcon';\nexport * from './Dialog';\nexport * from './Dropdown';\nexport * from './EditIcon';\nexport *"
  },
  {
    "path": "src/components/Preferences/AccentColor.tsx",
    "chars": 696,
    "preview": "import React from 'react';\nimport { FullScreenDialog } from '../Mui/Dialog/FullScreenDialog';\nimport { Control } from '."
  },
  {
    "path": "src/components/Preferences/Preferences.scss",
    "chars": 3010,
    "preview": ".preferences {\n  color: var(--text-color);\n\n  input[type='number']::-webkit-inner-spin-button,\n  input[type='number']::-"
  },
  {
    "path": "src/components/Preferences/Preferences.tsx",
    "chars": 6039,
    "preview": "import React, { useState } from 'react';\nimport { useSelector } from 'react-redux';\nimport {\n  Input,\n  ErrorTooltip,\n  "
  },
  {
    "path": "src/components/Preferences/Storage.tsx",
    "chars": 445,
    "preview": "import React from 'react';\nimport { Input, FullScreenDialog } from '../Mui';\n\nexport function Storage() {\n  return (\n   "
  },
  {
    "path": "src/components/Preferences/ThemeSelector.tsx",
    "chars": 685,
    "preview": "import React from 'react';\nimport { FullScreenDialog } from '../Mui/Dialog/FullScreenDialog';\nimport { Control } from '."
  },
  {
    "path": "src/components/Preferences/TitleBarSelector.tsx",
    "chars": 2056,
    "preview": "import React, { useState } from 'react';\nimport { FullScreenDialog, ConfirmDialog } from '../Mui';\nimport { Dropdown, Me"
  },
  {
    "path": "src/components/Preferences/index.ts",
    "chars": 117,
    "preview": "import './Preferences.scss';\n\nexport * from './Preferences';\nexport { Preferences as default } from './Preferences';\n"
  },
  {
    "path": "src/components/PrivateRoute.tsx",
    "chars": 411,
    "preview": "import React from 'react';\nimport { Route, Redirect, RouteProps } from 'react-router-dom';\nimport { useSelector } from '"
  },
  {
    "path": "src/components/Switch/Switch.scss",
    "chars": 1064,
    "preview": "$defaultWidth: 47.5px;\n\n@mixin shadow($x) {\n  box-shadow: $x 0.5px 3px 0px lighten(#000, 50%);\n}\n\n.switch {\n  @include d"
  },
  {
    "path": "src/components/Switch/Switch.tsx",
    "chars": 697,
    "preview": "import React, { useMemo, useCallback, CSSProperties } from 'react';\n\ninterface Props {\n  checked?: boolean;\n  onChange?("
  },
  {
    "path": "src/components/Switch/index.ts",
    "chars": 97,
    "preview": "import './Switch.scss';\n\nexport * from './Switch';\nexport { Switch as default } from './Switch';\n"
  },
  {
    "path": "src/constants/index.ts",
    "chars": 49,
    "preview": "export { default as PATHS } from './paths.json';\n"
  },
  {
    "path": "src/constants/paths.json",
    "chars": 62,
    "preview": "{\n  \"AUTH\": \"/auth\",\n  \"TASKLIST\": \"/tasklist/:taskListId?\"\n}\n"
  },
  {
    "path": "src/date.d.ts",
    "chars": 1257,
    "preview": "type DayCn = '日' | '一' | '二' | '三' | '四' | '五' | '六';\ntype MonthFullName =\n  | 'January'\n  | 'February'\n  | 'March'\n  | "
  },
  {
    "path": "src/hooks/crud-reducer/bindDispatch.ts",
    "chars": 529,
    "preview": "import { Dispatch } from 'react';\nimport { ActionCreators } from './crudAction';\n\nexport type Dispatched<A extends Actio"
  },
  {
    "path": "src/hooks/crud-reducer/crudAction.ts",
    "chars": 4455,
    "preview": "/* eslint-disable @typescript-eslint/ban-types */\n\n// https://medium.com/dailyjs/typescript-create-a-condition-based-sub"
  },
  {
    "path": "src/hooks/crud-reducer/crudReducer.ts",
    "chars": 5623,
    "preview": "/* eslint-disable @typescript-eslint/ban-types */\n\nimport {\n  Key,\n  CRUDActions,\n  CRUDActionTypes,\n  PaginatePayload,\n"
  },
  {
    "path": "src/hooks/crud-reducer/crudSelector.ts",
    "chars": 783,
    "preview": "import { CRUDState } from './crudReducer';\n\nexport interface PaginateState<S extends CRUDState<unknown, any>> {\n  ids: S"
  },
  {
    "path": "src/hooks/crud-reducer/index.ts",
    "chars": 189,
    "preview": "export * from './bindDispatch';\nexport * from './crudAction';\nexport * from './crudReducer';\nexport * from './crudSelect"
  },
  {
    "path": "src/hooks/crud-reducer/useActions.ts",
    "chars": 389,
    "preview": "import { useState } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { ActionCreators } from './crudActio"
  },
  {
    "path": "src/hooks/crud-reducer/useCRUDReducer.ts",
    "chars": 1304,
    "preview": "/* eslint-disable @typescript-eslint/ban-types */\n\nimport { useReducer, useState } from 'react';\nimport {\n  CRUDState,\n "
  },
  {
    "path": "src/hooks/useActions.ts",
    "chars": 922,
    "preview": "import { useMemo, useRef, Dispatch as ReactDispatch } from 'react';\nimport { AnyAction, Dispatch } from 'redux';\nimport "
  },
  {
    "path": "src/hooks/useBoolean.ts",
    "chars": 333,
    "preview": "import { useState, useMemo } from 'react';\n\nexport function useBoolean(initialState = false) {\n  const [flag, setFlag] ="
  },
  {
    "path": "src/hooks/useMouseTrap.ts",
    "chars": 612,
    "preview": "import { useEffect } from 'react';\nimport mousetrap, { MousetrapStatic } from 'mousetrap';\n\ntype Params = Parameters<Mou"
  },
  {
    "path": "src/index.scss",
    "chars": 777,
    "preview": "*,\n*:before,\n*:after {\n  box-sizing: border-box;\n}\n\nhtml,\nbody,\n#root {\n  min-height: 100vh;\n}\n\nhtml {\n  background-colo"
  },
  {
    "path": "src/index.tsx",
    "chars": 1185,
    "preview": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport { Provider } from 'react-redux';\nimport { ConnectedR"
  },
  {
    "path": "src/pages/Auth/Auth.scss",
    "chars": 1740,
    "preview": ".auth {\n  @include flex($flex-direction: column);\n  @include fixed(0, null, 0);\n  @include sq-dimen(100%);\n\n  background"
  },
  {
    "path": "src/pages/Auth/Auth.tsx",
    "chars": 1892,
    "preview": "import React, { useState } from 'react';\nimport { useRxInput, useRxAsync } from 'use-rx-hooks';\nimport { Button } from '"
  },
  {
    "path": "src/pages/Auth/FileUpload.tsx",
    "chars": 2234,
    "preview": "import React, { useState } from 'react';\nimport { useRxAsync } from 'use-rx-hooks';\nimport { useBoolean } from '../../ho"
  },
  {
    "path": "src/pages/Auth/index.ts",
    "chars": 89,
    "preview": "import './Auth.scss';\n\nexport * from './Auth';\nexport { Auth as default } from './Auth';\n"
  },
  {
    "path": "src/pages/TaskList/CompletedTaskList/CompletedTaskList.scss",
    "chars": 1392,
    "preview": "$completed-task-list-header-height: 54px;\n\n.completed-tasks-list {\n  @include dimen(100%, $completed-task-list-header-he"
  },
  {
    "path": "src/pages/TaskList/CompletedTaskList/CompletedTaskList.tsx",
    "chars": 1222,
    "preview": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport { CompletedTask } from '../Task';\nimport { "
  },
  {
    "path": "src/pages/TaskList/CompletedTaskList/index.ts",
    "chars": 141,
    "preview": "import './CompletedTaskList.scss';\n\nexport * from './CompletedTaskList';\nexport { CompletedTaskList as default } from '."
  },
  {
    "path": "src/pages/TaskList/NewTask/NewTask.scss",
    "chars": 722,
    "preview": ".new-task {\n  @include dimen(100%, 56px);\n  @include flex(center);\n  @include padding-x(10px);\n\n  .new-task-button {\n   "
  },
  {
    "path": "src/pages/TaskList/NewTask/NewTask.tsx",
    "chars": 924,
    "preview": "import React from 'react';\nimport { SvgIconProps } from '@material-ui/core/SvgIcon';\nimport { IconButton, useMuiMenu } f"
  },
  {
    "path": "src/pages/TaskList/NewTask/index.ts",
    "chars": 101,
    "preview": "import './NewTask.scss';\n\nexport * from './NewTask';\nexport { NewTask as default } from './NewTask';\n"
  },
  {
    "path": "src/pages/TaskList/Task/CompletedTask.tsx",
    "chars": 747,
    "preview": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport { Task, TaskProps } from './Task';\nimport {"
  },
  {
    "path": "src/pages/TaskList/Task/DatePicker/DatePicker.scss",
    "chars": 1358,
    "preview": ".calender-header {\n  @include flex(center, space-between);\n\n  .month-year {\n    @include typeface('Nunito Sans', 700);\n "
  },
  {
    "path": "src/pages/TaskList/Task/DatePicker/DatePicker.tsx",
    "chars": 3926,
    "preview": "import React, { useState, useCallback, HTMLAttributes, useMemo } from 'react';\nimport { IconButton } from '../../../../c"
  },
  {
    "path": "src/pages/TaskList/Task/DatePicker/index.ts",
    "chars": 113,
    "preview": "import './DatePicker.scss';\n\nexport * from './DatePicker';\nexport { DatePicker as default } from './DatePicker';\n"
  },
  {
    "path": "src/pages/TaskList/Task/DateTimeDialog/DateTimeDialog.scss",
    "chars": 351,
    "preview": ".date-time-dialog-paper.date-time-dialog-paper {\n  @include dimen(100%);\n  color: var(--text-secondary-color);\n  max-hei"
  },
  {
    "path": "src/pages/TaskList/Task/DateTimeDialog/DateTimeDialog.tsx",
    "chars": 1894,
    "preview": "import React, {\n  useState,\n  useEffect,\n  createContext,\n  ReactNode,\n  useContext\n} from 'react';\nimport {\n  ConfirmDi"
  },
  {
    "path": "src/pages/TaskList/Task/DateTimeDialog/index.ts",
    "chars": 129,
    "preview": "import './DateTimeDialog.scss';\n\nexport * from './DateTimeDialog';\nexport { DateTimeDialog as default } from './DateTime"
  },
  {
    "path": "src/pages/TaskList/Task/Task.scss",
    "chars": 3401,
    "preview": "$border: 1px solid var(--border-color);\n$min-task-height: 50px;\n\n@mixin withTopBottomBorder($color: var(--border-color))"
  },
  {
    "path": "src/pages/TaskList/Task/Task.tsx",
    "chars": 1354,
    "preview": "import React, { ReactNode } from 'react';\nimport { useSelector } from 'react-redux';\nimport { Input, InputProps } from '"
  },
  {
    "path": "src/pages/TaskList/Task/TaskInput.tsx",
    "chars": 1292,
    "preview": "import React from 'react';\nimport { Input, InputProps } from '../../../components/Mui';\nimport { Schema$Task } from '../"
  },
  {
    "path": "src/pages/TaskList/Task/TodoTask/TodoTask.scss",
    "chars": 1031,
    "preview": ".todo-task {\n  @include textHighlight();\n  user-select: none;\n\n  &.dragging {\n    @include padding-x((0px, 20px));\n\n    "
  },
  {
    "path": "src/pages/TaskList/Task/TodoTask/TodoTask.tsx",
    "chars": 6951,
    "preview": "import React, {\n  useRef,\n  useMemo,\n  useEffect,\n  MouseEvent,\n  KeyboardEvent\n} from 'react';\nimport { useSelector } f"
  },
  {
    "path": "src/pages/TaskList/Task/TodoTask/TodoTaskMenu.tsx",
    "chars": 2291,
    "preview": "import React, {\n  createContext,\n  useContext,\n  useState,\n  MouseEvent,\n  ReactNode\n} from 'react';\nimport { useSelecto"
  },
  {
    "path": "src/pages/TaskList/Task/TodoTask/index.ts",
    "chars": 105,
    "preview": "import './TodoTask.scss';\n\nexport * from './TodoTask';\nexport { TodoTask as default } from './TodoTask';\n"
  },
  {
    "path": "src/pages/TaskList/Task/TodoTaskDetails/DateTimeButton.tsx",
    "chars": 782,
    "preview": "import React from 'react';\nimport { IconButton } from '../../../../components/Mui';\nimport Button from '@material-ui/cor"
  },
  {
    "path": "src/pages/TaskList/Task/TodoTaskDetails/TodoTaskDetails.scss",
    "chars": 2029,
    "preview": "$height: 36px;\n\n.todo-task-details {\n  .row,\n  .mui-input-base {\n    + .row,\n    + .mui-input-base {\n      margin-top: 8"
  },
  {
    "path": "src/pages/TaskList/Task/TodoTaskDetails/TodoTaskDetails.tsx",
    "chars": 4486,
    "preview": "import React, {\n  KeyboardEvent,\n  useState,\n  createContext,\n  ReactNode,\n  useEffect,\n  useContext\n} from 'react';\nimp"
  },
  {
    "path": "src/pages/TaskList/Task/TodoTaskDetails/index.ts",
    "chars": 133,
    "preview": "import './TodoTaskDetails.scss';\n\nexport * from './TodoTaskDetails';\nexport { TodoTaskDetails as default } from './TodoT"
  },
  {
    "path": "src/pages/TaskList/Task/ToggleCompleted.tsx",
    "chars": 1347,
    "preview": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport { IconButton } from '../../../components/Mu"
  },
  {
    "path": "src/pages/TaskList/Task/index.ts",
    "chars": 150,
    "preview": "import './Task.scss';\n\nexport * from './Task';\nexport * from './TodoTask';\nexport * from './CompletedTask';\nexport { Tas"
  },
  {
    "path": "src/pages/TaskList/TaskList.scss",
    "chars": 612,
    "preview": ".task-list {\n  @include dimen(100%, 100vh);\n  @include flex($flex-direction: column);\n\n  &.disabled {\n    opacity: 0.3;\n"
  },
  {
    "path": "src/pages/TaskList/TaskList.tsx",
    "chars": 1803,
    "preview": "import React, { useEffect } from 'react';\nimport { useSelector } from 'react-redux';\nimport { TaskListHeader } from './T"
  },
  {
    "path": "src/pages/TaskList/TaskListDropdown/TaskListDropdown.scss",
    "chars": 910,
    "preview": "$button-height: 26px;\n$item-height: 40px;\n\n.task-list-header-dropdown-container {\n  @include app-region-no-drag;\n  overf"
  },
  {
    "path": "src/pages/TaskList/TaskListDropdown/TaskListDropdown.tsx",
    "chars": 2306,
    "preview": "import React, { ReactNode, useEffect, useRef } from 'react';\nimport { useSelector } from 'react-redux';\nimport {\n  useMu"
  },
  {
    "path": "src/pages/TaskList/TaskListDropdown/TaskListDropdownItem.tsx",
    "chars": 680,
    "preview": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport { MenuItem, MenuItemProps } from '../../../"
  },
  {
    "path": "src/pages/TaskList/TaskListDropdown/index.ts",
    "chars": 137,
    "preview": "import './TaskListDropdown.scss';\n\nexport * from './TaskListDropdown';\nexport { TaskListDropdown as default } from './Ta"
  },
  {
    "path": "src/pages/TaskList/TaskListHeader/TaskListHeader.scss",
    "chars": 266,
    "preview": ".task-list-header {\n  @include dimen(100%, var(--header-height));\n  @include flex(flex-end, space-between);\n  @include f"
  },
  {
    "path": "src/pages/TaskList/TaskListHeader/TaskListHeader.tsx",
    "chars": 1741,
    "preview": "import React from 'react';\nimport { generatePath } from 'react-router-dom';\nimport { FormDialog, MenuItem } from '../../"
  },
  {
    "path": "src/pages/TaskList/TaskListHeader/index.ts",
    "chars": 129,
    "preview": "import './TaskListHeader.scss';\n\nexport * from './TaskListHeader';\nexport { TaskListHeader as default } from './TaskList"
  },
  {
    "path": "src/pages/TaskList/TaskListMenu.tsx",
    "chars": 4687,
    "preview": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport { Divider } from '@material-ui/core';\nimpor"
  },
  {
    "path": "src/pages/TaskList/TodoTaskList/TodoTaskList.scss",
    "chars": 565,
    "preview": ".todo-tasks-list-by-date {\n  .todo-task {\n    .task-due-date-button {\n      display: none;\n    }\n  }\n\n  .date-label {\n  "
  },
  {
    "path": "src/pages/TaskList/TodoTaskList/TodoTaskList.tsx",
    "chars": 2678,
    "preview": "import React, { useState } from 'react';\nimport { useSelector } from 'react-redux';\nimport { SortableContainer, Sortable"
  },
  {
    "path": "src/pages/TaskList/TodoTaskList/TodoTaskListByDate.tsx",
    "chars": 1442,
    "preview": "import React, { Fragment } from 'react';\nimport { useSelector } from 'react-redux';\nimport { TodoTask } from '../Task';\n"
  },
  {
    "path": "src/pages/TaskList/TodoTaskList/index.ts",
    "chars": 121,
    "preview": "import './TodoTaskList.scss';\n\nexport * from './TodoTaskList';\nexport { TodoTaskList as default } from './TodoTaskList';"
  },
  {
    "path": "src/pages/TaskList/index.ts",
    "chars": 105,
    "preview": "import './TaskList.scss';\n\nexport * from './TaskList';\nexport { TaskList as default } from './TaskList';\n"
  },
  {
    "path": "src/react-app-env.d.ts",
    "chars": 400,
    "preview": "/// <reference types=\"react-scripts\" />\n\ndeclare module '*.scss';\ndeclare module 'react-desktop/windows';\n\ndeclare inter"
  },
  {
    "path": "src/scss/_functions.scss",
    "chars": 986,
    "preview": "@function str-replace($string, $search, $replace: '') {\n  $index: str-index($string, $search);\n\n  @if ($index) {\n    @re"
  },
  {
    "path": "src/scss/_mixins.scss",
    "chars": 634,
    "preview": "@import './mixins/animation';\n@import './mixins/background';\n@import './mixins/border';\n@import './mixins/electron';\n@im"
  },
  {
    "path": "src/scss/_platform.scss",
    "chars": 204,
    "preview": ":root {\n  --header-height: 70px;\n}\n\n[data-title-bar^='native']:not([data-platform^='darwin']) {\n  --header-height: 60px;"
  },
  {
    "path": "src/scss/_theme.scss",
    "chars": 1270,
    "preview": ":root {\n  --main-color: #fff;\n  --main-color-diff: #f1f3f4;\n  --main-color-diff2: #fff;\n  --accent-color: #{getColor(blu"
  },
  {
    "path": "src/scss/_variables.scss",
    "chars": 467,
    "preview": "$padding-x: 20px;\n\n$mui-menu-z-index: 1300; // default\n$app-region-z-index: $mui-menu-z-index + 10;\n\n$base-colors: (\n  r"
  },
  {
    "path": "src/scss/index.scss",
    "chars": 107,
    "preview": "@import './variables';\n@import './functions';\n@import './mixins';\n@import './theme';\n@import './platform';\n"
  },
  {
    "path": "src/scss/mixins/_animation.scss",
    "chars": 155,
    "preview": "@mixin animate($property) {\n  transition-duration: 0.3s;\n  transition-property: $property;\n  transition-timing-function:"
  },
  {
    "path": "src/scss/mixins/_background.scss",
    "chars": 319,
    "preview": "@mixin bg-image(\n  $image: null,\n  $size: 100%,\n  $position: center center,\n  $repeat: no-repeat\n) {\n  @if ($image) {\n  "
  },
  {
    "path": "src/scss/mixins/_border.scss",
    "chars": 275,
    "preview": "@mixin fake-border(\n  $position: bottom,\n  $borderWidth: 1px,\n  $color: #000,\n  $width: 100%\n) {\n  background: linear-gr"
  },
  {
    "path": "src/scss/mixins/_electron.scss",
    "chars": 128,
    "preview": "@mixin app-region-drag {\n  -webkit-app-region: drag;\n}\n\n@mixin app-region-no-drag {\n  -webkit-app-region: no-drag !impor"
  },
  {
    "path": "src/scss/mixins/_flex.scss",
    "chars": 304,
    "preview": "@mixin flex(\n  $align-items: stretch,\n  $justify-content: flex-start,\n  $flex-direction: row,\n  $wrap: nowrap,\n  $is-inl"
  },
  {
    "path": "src/scss/mixins/_font.scss",
    "chars": 171,
    "preview": "@mixin typeface($font: 'Roboto', $font-weight: normal) {\n  font-family: $font, Helvetica, Arial, 微軟正黑體, Microsoft JhengH"
  },
  {
    "path": "src/scss/mixins/_position.scss",
    "chars": 690,
    "preview": "@mixin position(\n  $top: null,\n  $bottom: null,\n  $left: null,\n  $right: null,\n  $position: absolute\n) {\n  position: $po"
  },
  {
    "path": "src/scss/mixins/_size.scss",
    "chars": 1188,
    "preview": "@mixin dimen($width: null, $height: null) {\n  @if ($width) {\n    width: $width;\n  }\n\n  @if ($height) {\n    height: $heig"
  },
  {
    "path": "src/scss/mixins/_textHighlight.scss",
    "chars": 475,
    "preview": "$text-highlight-z-index: 11;\n\n@mixin textHighlight() {\n  @include relative();\n\n  &:after {\n    @include absolute(null, -"
  },
  {
    "path": "src/scss/mixins/_textOverflow.scss",
    "chars": 374,
    "preview": "@mixin text-overflow-ellipsis() {\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n}\n\n@mixin multi-"
  },
  {
    "path": "src/service/auth.ts",
    "chars": 1231,
    "preview": "import { google } from 'googleapis';\n\nconst { oAuth2Storage, tokenStorage } = window;\n\nexport let OAuth2Keys = oAuth2Sto"
  },
  {
    "path": "src/service/index.ts",
    "chars": 77,
    "preview": "export * from './auth';\nexport * from './task';\nexport * from './tasksList';\n"
  },
  {
    "path": "src/service/task.ts",
    "chars": 1577,
    "preview": "import { Observable, defer, of } from 'rxjs';\nimport { mergeMap } from 'rxjs/operators';\nimport { google, tasks_v1 } fro"
  },
  {
    "path": "src/service/tasksList.ts",
    "chars": 515,
    "preview": "import { defer } from 'rxjs';\nimport { map } from 'rxjs/operators';\nimport { google, tasks_v1 } from 'googleapis';\nimpor"
  },
  {
    "path": "src/serviceWorker.ts",
    "chars": 5201,
    "preview": "// This optional code is used to register a service worker.\n// register() is not called by default.\n\n// This lets the ap"
  },
  {
    "path": "src/store/actions/auth.ts",
    "chars": 418,
    "preview": "import { useActions, GetCreatorsAction } from '../../hooks/crud-reducer';\n\nexport function authenticated() {\n  return {\n"
  },
  {
    "path": "src/store/actions/index.ts",
    "chars": 107,
    "preview": "export * from './auth';\nexport * from './task';\nexport * from './taskList';\nexport * from './preferences';\n"
  },
  {
    "path": "src/store/actions/preferences.ts",
    "chars": 501,
    "preview": "import { useActions } from '../../hooks/crud-reducer';\nimport { DeepPartial } from '../../utils/form';\n\nfunction updateP"
  },
  {
    "path": "src/store/actions/task.ts",
    "chars": 3465,
    "preview": "import { tasks_v1 } from 'googleapis';\nimport {\n  useActions,\n  getCRUDActionsCreator,\n  GetCreatorsAction,\n  PaginatePa"
  },
  {
    "path": "src/store/actions/taskList.ts",
    "chars": 1773,
    "preview": "import { tasks_v1 } from 'googleapis';\nimport {\n  getCRUDActionsCreator,\n  useActions,\n  PaginatePayload,\n  GetCreatorsA"
  },
  {
    "path": "src/store/epics/auth.ts",
    "chars": 917,
    "preview": "import { generatePath } from 'react-router-dom';\nimport { ofType, Epic } from 'redux-observable';\nimport { RouterAction,"
  },
  {
    "path": "src/store/epics/index.ts",
    "chars": 298,
    "preview": "import { combineEpics } from 'redux-observable';\nimport authEpics from './auth';\nimport taskEpics from './task';\nimport "
  },
  {
    "path": "src/store/epics/preferences.ts",
    "chars": 2578,
    "preview": "import { Epic, ofType } from 'redux-observable';\nimport { defer, empty, fromEvent, merge, timer, of, concat } from 'rxjs"
  },
  {
    "path": "src/store/epics/task.ts",
    "chars": 9421,
    "preview": "import { ofType, Epic, ActionsObservable } from 'redux-observable';\nimport { RouterAction } from 'connected-react-router"
  },
  {
    "path": "src/store/epics/taskList.ts",
    "chars": 3438,
    "preview": "import { ofType, Epic } from 'redux-observable';\nimport { generatePath } from 'react-router-dom';\nimport { RouterAction,"
  },
  {
    "path": "src/store/index.ts",
    "chars": 1495,
    "preview": "import { createHashHistory } from 'history';\nimport { createStore, applyMiddleware, compose } from 'redux';\nimport { cre"
  },
  {
    "path": "src/store/reducers/auth.ts",
    "chars": 483,
    "preview": "import { AuthActions } from '../actions/auth';\n\ninterface State {\n  loggedIn: boolean;\n}\n\nconst initialState: State = {\n"
  },
  {
    "path": "src/store/reducers/index.ts",
    "chars": 591,
    "preview": "import { combineReducers } from 'redux';\nimport { connectRouter } from 'connected-react-router';\nimport { taskReducer } "
  },
  {
    "path": "src/store/reducers/preferences.ts",
    "chars": 594,
    "preview": "import { PreferenceActions } from '../actions/preferences';\n\ninterface State extends Schema$Preferences {}\n\nconst initia"
  },
  {
    "path": "src/store/reducers/task.ts",
    "chars": 6780,
    "preview": "import { TaskActionTypes, TaskActions, taskActions } from '../actions/task';\nimport { taskSelector } from '../selectors'"
  },
  {
    "path": "src/store/reducers/taskList.ts",
    "chars": 2116,
    "preview": "import {\n  TaskListActionTypes,\n  TaskListActions,\n  taskListActions\n} from '../actions/taskList';\nimport { createCRUDRe"
  },
  {
    "path": "src/store/selectors/index.ts",
    "chars": 83,
    "preview": "export * from './task';\nexport * from './taskList';\nexport * from './preferences';\n"
  },
  {
    "path": "src/store/selectors/preferences.ts",
    "chars": 280,
    "preview": "import { RootState } from '../reducers';\n\nexport const preferencesSelector = (state: RootState) => state.preferences;\n\ne"
  },
  {
    "path": "src/store/selectors/task.ts",
    "chars": 2086,
    "preview": "import { RootState } from '../reducers';\nimport { createSelector } from 'reselect';\nimport { Schema$Task } from '../../t"
  },
  {
    "path": "src/store/selectors/taskList.ts",
    "chars": 1035,
    "preview": "import { createSelector } from 'reselect';\nimport { matchPath } from 'react-router-dom';\nimport { RootState } from '../r"
  },
  {
    "path": "src/theme.ts",
    "chars": 464,
    "preview": "import { createMuiTheme } from '@material-ui/core/styles';\n\nexport const theme = createMuiTheme({\n  typography: {\n    fo"
  },
  {
    "path": "src/typings/index.ts",
    "chars": 512,
    "preview": "import { tasks_v1 } from 'googleapis';\n\nexport interface Schema$Task {\n  uuid: string;\n\n  completed?: string | null;\n\n  "
  },
  {
    "path": "src/utils/date.ts",
    "chars": 6358,
    "preview": "/* eslint-disable */\n/* tslint:disable:only-arrow-functions */\n\n// Date class extension\n\n// reference :\n// http://stacko"
  },
  {
    "path": "src/utils/form/form.ts",
    "chars": 8074,
    "preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport React, { ReactElement, ReactNode } from 'react';\nimport "
  },
  {
    "path": "src/utils/form/index.ts",
    "chars": 119,
    "preview": "import * as validators from './validators';\n\nexport { validators };\nexport * from './form';\nexport * from './typings';\n"
  },
  {
    "path": "src/utils/form/typings.ts",
    "chars": 1869,
    "preview": "type ValueOf<T> = T[keyof T];\n\ntype Cons<H, T> = T extends readonly any[]\n  ? ((h: H, ...t: T) => void) extends (...r: i"
  },
  {
    "path": "src/utils/form/validators.ts",
    "chars": 3097,
    "preview": "export type Validator = (rule: any, value: any) => Promise<void>;\n\nexport const compose = (validators: Array<Validator |"
  },
  {
    "path": "src/utils/nprogress.ts",
    "chars": 278,
    "preview": "import NProgress from 'nprogress';\nimport 'nprogress/nprogress.css';\n\nNProgress.configure({\n  parent: '#root',\n  easing:"
  },
  {
    "path": "src/utils/uuid.ts",
    "chars": 196,
    "preview": "export class UUID {\n  private count: number = 0;\n\n  next() {\n    this.count++;\n    // to string avoid conflict with inde"
  },
  {
    "path": "tsconfig.json",
    "chars": 664,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    "
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the Pong420/google-tasks-desktop GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 185 files (266.3 KB), approximately 75.4k tokens, and a symbol index with 258 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!