[
  {
    "path": ".eslintcache",
    "content": "[{\"/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.\"]"
  },
  {
    "path": ".eslintignore",
    "content": "# Logs\nlogs\n*.log\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n.eslintcache\n\n# Dependency directory\n# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git\nnode_modules\n\n# OSX\n.DS_Store\n\n# flow-typed\nflow-typed/npm/*\n!flow-typed/npm/module_vx.x.x.js\n\n# App packaged\nrelease\nbuild\nelectron/*.js\n\n.idea\nnpm-debug.log.*\n__snapshots__\n\n# Package.json\npackage.json\n.travis.yml\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\nrelease\nelectron/**/**/*.js\nelectron/**/**/*.js.map\n*.tsbuildinfo\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"overrides\": [\n    {\n      \"files\": [\".prettierrc\", \".babelrc\", \".eslintrc\", \".stylelintrc\"],\n      \"options\": {\n        \"parser\": \"json\"\n      }\n    }\n  ],\n  \"singleQuote\": true,\n  \"trailingComma\": \"none\",\n  \"arrowParens\": \"avoid\"\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015-present C. T. Lin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Procfile",
    "content": "react: npm run app:dev\nelectron: node scripts/electron-wait-react.js && yarn electron:dev"
  },
  {
    "path": "README.md",
    "content": "## Google Tasks Desktop\n\n> Unofficial google tasks desktop application. Using React and google tasks api\n\n<div>\n  <img src=\"./screenshot/1.png\" width=\"24%\">\n  <img src=\"./screenshot/2.png\" width=\"24%\">\n  <img src=\"./screenshot/3.png\" width=\"24%\">\n  <img src=\"./screenshot/4.png\" width=\"24%\">\n</div>\n\n#### [Download](https://github.com/Pong420/google-tasks-desktop/releases)\n\n#### :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.\n\n#### Step to enable Google Tasks API.\n\n1. 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 )\n\n2. After the OAuth client created, you could download the `oAuth.json` by clicking this button\n\n<img src=\"./screenshot/guide-1.png\" />\n\nAnd the `oAuth.json` looks like this\n\n```json\n{\n  \"installed\": {\n    \"client_id\": \"...\",\n    \"project_id\": \"...\",\n    \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n    \"token_uri\": \"https://oauth2.googleapis.com/token\",\n    \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\",\n    \"client_secret\": \"...\",\n    \"redirect_uris\": [\"urn:ietf:wg:oauth:2.0:oob\", \"http://localhost\"]\n  }\n}\n```\n\n3. Start and drag the `oAuth.json` into the application.\n\n4. Enable [Google Tasks API](https://console.developers.google.com/apis/library/tasks.googleapis.com)\n\n5. 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.\n\n6. Paste the code into the input filed and click the `Confirm` button.\n\n### Development\n\n```\nyarn dev\n```\n\n### Packaging\n\nTo package apps for the local platform:\n\n```\nyarn package\n```\n\nFirst, refer to the [Multi Platform Build docs](https://www.electron.build/multi-platform-build) for dependencies. Then,\n\n```\nyarn package-all\n```\n\n### TODO\n\n- [x] Support Window & Linux\n- [x] Keyboard shortcuts\n- [x] Dark Theme\n- [x] Add Note\n- [x] Add Date\n- [x] Animation\n- [x] Sync data periodically\n- [x] Move task to another list\n- [x] Improve / check performace\n- [ ] Subtask\n- [ ] Error handling\n\n### Known issue\n\n- Add time / repeat is not supported as API limitation\n- Tasks sorting type (My order / Date) is not synced to the official platform (Web/App)\n- The position of the task which marks as complete to incomplete may be different after refresh\n"
  },
  {
    "path": "common.d.ts",
    "content": "type Theme = 'light' | 'dark';\ntype AccentColor = 'red' | 'blue' | 'amber' | 'green' | 'purple' | 'grey';\ntype TitleBar = 'native' | 'frameless';\n\ninterface Schema$Storage<T> {\n  get(): T;\n  save(value: NonNullable<T>): void;\n}\n\ninterface OAuthKeys {\n  installed: {\n    client_id: string;\n    project_id: string;\n    auth_uri: string;\n    token_uri: string;\n    client_secret: string;\n    redirect_uris: string[];\n  };\n}\n\ninterface SyncConfig {\n  enabled: boolean;\n  reconnection: boolean;\n  inactiveHours: number;\n}\n\ntype Schema$Preferences = {\n  accentColor: AccentColor;\n  maxTasks: number;\n  sync: SyncConfig;\n  theme: Theme;\n  titleBar: TitleBar;\n};\n\ndeclare interface Window {\n  __setTheme(theme?: Theme): void;\n  __setAccentColor(color?: AccentColor): void;\n  __setTitleBar(titleBar?: TitleBar, shouldRelaunch?: boolean): void;\n  platform: NodeJS.Platform;\n  openExternal: Electron.Shell['openExternal'];\n  getCurrentWindow(): Electron.BrowserWindow;\n  oAuth2Storage: Schema$Storage<OAuthKeys | undefined>;\n  tokenStorage: Schema$Storage<any>;\n  preferencesStorage: Schema$Storage<Schema$Preferences>;\n  taskListSortByDateStorage: Schema$Storage<string[]>;\n  logout: () => void;\n  TOKEN_PATH: string;\n  OAUTH2_KEYS_PATH: string;\n  PREFERENCES_PATH: string;\n  TASKLIST_SORT_BY_DATE_PATH: string;\n  STORAGE_DIRECTORY: string;\n  relaunch: () => void;\n}\n"
  },
  {
    "path": "config-overrides.js",
    "content": "const path = require('path');\n\n/** @typedef {import('webpack').Configuration} Configuration */\n\n/**\n * @param {Configuration} config\n * @param {*} env\n * @returns {Configuration}\n */\nmodule.exports = function (config, env) {\n  config.resolve.alias = {\n    ...config.resolve.alias,\n    fs: path.resolve(__dirname, 'mock-fs.js')\n  };\n\n  config.module.rules = config.module.rules.map(r => {\n    if (r.oneOf) {\n      return {\n        ...r,\n        oneOf: r.oneOf.map(r => {\n          if (Array.isArray(r.use)) {\n            return {\n              ...r,\n              use: r.use.map(u =>\n                typeof u === 'object' &&\n                typeof u.options === 'object' &&\n                u.loader.includes('sass-loader')\n                  ? {\n                      ...u,\n                      options: {\n                        ...u.options,\n                        additionalData: `@import 'scss/index.scss';`,\n                        sassOptions: {\n                          includePaths: [path.resolve(__dirname, 'src')]\n                        }\n                      }\n                    }\n                  : u\n              )\n            };\n          }\n          return r;\n        })\n      };\n    }\n    return r;\n  });\n\n  return config;\n};\n"
  },
  {
    "path": "electron/electron.d.ts",
    "content": "declare module 'electron-devtools-installer';\n"
  },
  {
    "path": "electron/main.ts",
    "content": "import path from 'path';\nimport url from 'url';\nimport {\n  app,\n  shell,\n  nativeTheme,\n  BrowserWindow,\n  BrowserWindowConstructorOptions\n} from 'electron';\nimport { MenuBuilder } from './menu';\nimport { initStorage } from './storage';\n\nlet mainWindow: BrowserWindow | null = null;\n\nconst isDevelopment = process.env.NODE_ENV === 'development';\n\nconst { preferencesStorage } = initStorage(app, nativeTheme);\n\napp.allowRendererProcessReuse = true;\n\nasync function createWindow() {\n  if (isDevelopment) {\n    const {\n      default: installExtension,\n      REACT_DEVELOPER_TOOLS,\n      REDUX_DEVTOOLS\n    } = await import('electron-devtools-installer');\n    await installExtension(REACT_DEVELOPER_TOOLS);\n    await installExtension(REDUX_DEVTOOLS);\n  }\n\n  let options: BrowserWindowConstructorOptions = {\n    height: 500,\n    width: 300,\n    show: false,\n    webPreferences: {\n      enableRemoteModule: true,\n      preload: path.join(__dirname, './preload/index.js')\n    }\n  };\n\n  const titleBar: TitleBar = preferencesStorage.get().titleBar;\n\n  if (\n    titleBar === 'frameless' ||\n    process.platform === 'win32' ||\n    process.platform === 'darwin'\n  ) {\n    options = {\n      ...options,\n      frame: false,\n      titleBarStyle: titleBar === 'frameless' ? 'hidden' : 'hiddenInset'\n    };\n  }\n\n  mainWindow = new BrowserWindow(options);\n  mainWindow.setMenuBarVisibility(false);\n\n  const startUrl =\n    process.env.ELECTRON_START_URL ||\n    url.format({\n      pathname: path.join(__dirname, '../build/index.html'),\n      protocol: 'file:',\n      slashes: true\n    });\n\n  mainWindow.loadURL(startUrl);\n\n  mainWindow.webContents.on('did-finish-load', () => {\n    mainWindow && mainWindow.show();\n  });\n\n  mainWindow.webContents.on('new-window', (event, url) => {\n    event.preventDefault();\n    shell.openExternal(url);\n  });\n\n  mainWindow.on('closed', () => {\n    mainWindow = null;\n  });\n\n  const menuBuilder = new MenuBuilder(mainWindow);\n  menuBuilder.buildMenu();\n}\n\napp.on('ready', createWindow);\n\napp.on('window-all-closed', () => {\n  // On OS X it is common for applications and their menu bar\n  // to stay active until the user quits explicitly with Cmd + Q\n  if (process.platform !== 'darwin') {\n    app.quit();\n  }\n});\n\napp.on('activate', () => {\n  if (mainWindow === null) {\n    createWindow();\n  }\n});\n"
  },
  {
    "path": "electron/menu.ts",
    "content": "// borrow from\n// https://github.com/electron-react-boilerplate/examples/blob/master/examples/typescript/app/main.dev.ts\n\nimport {\n  app,\n  Menu,\n  shell,\n  BrowserWindow,\n  MenuItemConstructorOptions\n} from 'electron';\n\nconst help = {\n  label: 'Help',\n  submenu: [\n    {\n      label: 'Github',\n      click() {\n        shell.openExternal('https://github.com/Pong420/google-tasks-desktop');\n      }\n    },\n    {\n      label: 'Search Issues',\n      click() {\n        shell.openExternal(\n          'https://github.com/Pong420/google-tasks-desktop/issues'\n        );\n      }\n    }\n  ]\n};\n\nexport class MenuBuilder {\n  mainWindow: BrowserWindow;\n\n  constructor(mainWindow: BrowserWindow) {\n    this.mainWindow = mainWindow;\n  }\n\n  buildMenu() {\n    if (\n      process.env.NODE_ENV === 'development' ||\n      process.env.DEBUG_PROD === 'true'\n    ) {\n      this.setupDevelopmentEnvironment();\n    }\n\n    const template =\n      process.platform === 'darwin'\n        ? this.buildDarwinTemplate()\n        : this.buildDefaultTemplate();\n\n    const menu = Menu.buildFromTemplate(template);\n    Menu.setApplicationMenu(menu);\n\n    return menu;\n  }\n\n  setupDevelopmentEnvironment() {\n    this.mainWindow.webContents.toggleDevTools();\n    this.mainWindow.webContents.on('context-menu', (e, props) => {\n      const { x, y } = props;\n\n      Menu.buildFromTemplate([\n        {\n          label: 'Inspect element',\n          click: () => {\n            this.mainWindow.webContents.inspectElement(x, y);\n          }\n        }\n      ]).popup({ window: this.mainWindow });\n    });\n  }\n\n  buildDarwinTemplate(): MenuItemConstructorOptions[] {\n    const subMenuAbout = {\n      label: 'Google Tasks',\n      submenu: [\n        {\n          label: 'About Google Tasks',\n          selector: 'orderFrontStandardAboutPanel:'\n        },\n        { type: 'separator' },\n        { label: 'Services', submenu: [] },\n        { type: 'separator' },\n        {\n          label: 'Hide Google Tasks',\n          accelerator: 'Command+H',\n          selector: 'hide:'\n        },\n        {\n          label: 'Hide Others',\n          accelerator: 'Command+Shift+H',\n          selector: 'hideOtherApplications:'\n        },\n        { label: 'Show All', selector: 'unhideAllApplications:' },\n        { type: 'separator' },\n        {\n          label: 'Quit',\n          accelerator: 'Command+Q',\n          click: () => {\n            app.quit();\n          }\n        }\n      ]\n    };\n    const subMenuEdit = {\n      label: 'Edit',\n      submenu: [\n        { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' },\n        { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' },\n        { type: 'separator' },\n        { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' },\n        { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' },\n        { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' },\n        {\n          label: 'Select All',\n          accelerator: 'Command+A',\n          selector: 'selectAll:'\n        }\n      ]\n    };\n    const subMenuWindow = {\n      label: 'Window',\n      submenu: [\n        {\n          label: 'Minimize',\n          accelerator: 'Command+M',\n          selector: 'performMiniaturize:'\n        },\n        { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' },\n        { type: 'separator' },\n        { label: 'Bring All to Front', selector: 'arrangeInFront:' }\n      ]\n    };\n    const subMenuHelp = help;\n    const subMenuView = {\n      label: 'View',\n      submenu: [\n        { role: 'Reload', accelerator: 'CmdOrCtrl+R' },\n        {\n          label: 'Toggle Full Screen',\n          accelerator: 'Ctrl+Command+F',\n          click: () => {\n            this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());\n          }\n        },\n        { type: 'separator' },\n        { role: 'toggledevtools', accelerator: 'Option+CmdOrCtrl+i' }\n      ]\n    };\n    return [\n      subMenuAbout,\n      subMenuEdit,\n      subMenuView,\n      subMenuWindow,\n      subMenuHelp\n    ] as MenuItemConstructorOptions[];\n  }\n\n  buildDefaultTemplate(): MenuItemConstructorOptions[] {\n    const templateDefault = [\n      {\n        label: '&File',\n        submenu: [\n          { label: '&Open', accelerator: 'Ctrl+O' },\n          {\n            label: '&Close',\n            accelerator: 'Ctrl+W',\n            click: () => {\n              this.mainWindow.close();\n            }\n          }\n        ]\n      },\n      {\n        label: '&View',\n        submenu: [\n          {\n            label: '&Reload',\n            accelerator: 'Ctrl+R',\n            click: () => {\n              this.mainWindow.webContents.reload();\n            }\n          },\n          {\n            label: 'Toggle &Full Screen',\n            accelerator: 'F11',\n            click: () => {\n              this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());\n            }\n          },\n          {\n            label: 'Toggle &Developer Tools',\n            accelerator: 'Alt+Ctrl+I',\n            click: () => {\n              this.mainWindow.webContents.toggleDevTools();\n            }\n          }\n        ]\n      },\n      help\n    ];\n\n    return templateDefault;\n  }\n}\n"
  },
  {
    "path": "electron/preload/index.ts",
    "content": "import fs from 'fs';\nimport { remote } from 'electron';\nimport { handleOSTheme } from './theme';\nimport { initStorage } from '../storage';\n\nfunction relaunch() {\n  remote.app.relaunch();\n  remote.app.exit();\n}\n\nconst storage = initStorage(remote.app, remote.nativeTheme);\nconst { preferencesStorage } = storage;\n\nwindow.__setAccentColor = (newColor?: AccentColor) => {\n  const preferences = preferencesStorage.get();\n  const accentColor = newColor || preferences.accentColor;\n\n  document.documentElement.setAttribute('data-accent-color', accentColor);\n\n  if (newColor) {\n    preferencesStorage.save({\n      ...preferences,\n      accentColor\n    });\n  }\n};\n\nwindow.__setTitleBar = (newTitleBar?: TitleBar, shouldRelaunch?: boolean) => {\n  const preferences = preferencesStorage.get();\n  const titleBar = newTitleBar || preferences.titleBar;\n  document.documentElement.setAttribute('data-title-bar', titleBar);\n  if (titleBar) {\n    preferencesStorage.save({\n      ...preferences,\n      titleBar\n    });\n\n    if (shouldRelaunch) {\n      relaunch();\n    }\n  }\n};\n\nObject.assign(window, storage);\n\nwindow.platform = process.platform;\nwindow.getCurrentWindow = remote.getCurrentWindow;\nwindow.openExternal = remote.shell.openExternal;\nwindow.logout = () => fs.unlinkSync(storage.TOKEN_PATH);\nwindow.relaunch = relaunch;\n\nprocess.once('loaded', () => {\n  handleOSTheme();\n});\n"
  },
  {
    "path": "electron/preload/theme.ts",
    "content": "import { remote } from 'electron';\n\nexport const setTheme = (newTheme?: Theme) => {\n  const preferences = window.preferencesStorage.get();\n  let theme = newTheme || preferences.theme;\n\n  document.documentElement.setAttribute('data-theme', theme);\n\n  if (newTheme) {\n    window.preferencesStorage.save({\n      ...preferences,\n      theme\n    });\n  }\n};\n\nwindow.__setTheme = setTheme;\n\nexport function handleOSTheme() {\n  if (process.platform === 'darwin') {\n    const { systemPreferences } = remote;\n    systemPreferences.subscribeNotification(\n      'AppleInterfaceThemeChangedNotification',\n      () => setTheme()\n    );\n  }\n}\n"
  },
  {
    "path": "electron/storage.ts",
    "content": "import fs from 'fs';\nimport path from 'path';\nimport { App, NativeTheme } from 'electron';\n\nfunction FileStorage<T extends {}>(path: string): Schema$Storage<T | undefined>;\nfunction FileStorage<T extends {}>(path: string, defaultValue: T): Schema$Storage<T> // prettier-ignore\n// prettier-ignore\nfunction FileStorage<T extends {}>(path: string, defaultValue?: T): Schema$Storage<T | undefined> { \n  return {\n    get() {\n      try {\n        const val = fs.readFileSync(path, 'utf8');\n        return JSON.parse(val);\n      } catch (error) {}\n\n      return defaultValue;\n    },\n    save(value) {\n      fs.writeFileSync(path, JSON.stringify(value, null, 2));\n    }\n  };\n}\n\nexport function initStorage(app: App, nativeTheme: NativeTheme) {\n  const STORAGE_DIRECTORY = path.join(\n    app.getPath('userData'),\n    'google-tasks-desktop'\n  );\n\n  const TOKEN_PATH = path.join(STORAGE_DIRECTORY, 'token.json');\n\n  const OAUTH2_KEYS_PATH = path.join(STORAGE_DIRECTORY, 'oauth2.json');\n\n  const PREFERENCES_PATH = path.join(STORAGE_DIRECTORY, 'preferences.json');\n\n  const TASKLIST_SORT_BY_DATE_PATH = path.join(\n    STORAGE_DIRECTORY,\n    'tasklist-order.json'\n  );\n\n  if (!fs.existsSync(STORAGE_DIRECTORY)) {\n    fs.mkdirSync(STORAGE_DIRECTORY);\n  }\n\n  const oAuth2Storage = FileStorage<any>(OAUTH2_KEYS_PATH);\n\n  const tokenStorage = FileStorage<any>(TOKEN_PATH);\n\n  const taskListSortByDateStorage = FileStorage<any>(\n    TASKLIST_SORT_BY_DATE_PATH,\n    []\n  );\n\n  const defaultPrefrences: Schema$Preferences = {\n    accentColor: 'blue',\n    maxTasks: 100,\n    theme: nativeTheme.shouldUseDarkColors ? 'dark' : 'light',\n    titleBar: 'native',\n    sync: {\n      enabled: true,\n      reconnection: true,\n      inactiveHours: 12\n    }\n  };\n\n  const preferencesStorage = FileStorage<Schema$Preferences>(\n    PREFERENCES_PATH,\n    defaultPrefrences\n  );\n\n  // for version <= v3.0.2\n  let preferences = preferencesStorage.get();\n  preferencesStorage.save(\n    Object.entries(defaultPrefrences).reduce((results, [key, value]) => {\n      const currentValue: unknown =\n        preferences[key as keyof typeof preferences];\n      return {\n        ...results,\n        [key]: typeof currentValue === 'undefined' ? value : currentValue\n      };\n    }, {} as Schema$Preferences)\n  );\n\n  return {\n    STORAGE_DIRECTORY,\n    TOKEN_PATH,\n    PREFERENCES_PATH,\n    TASKLIST_SORT_BY_DATE_PATH,\n    oAuth2Storage,\n    tokenStorage,\n    taskListSortByDateStorage,\n    preferencesStorage\n  };\n}\n"
  },
  {
    "path": "electron/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"noEmit\": false,\n    \"rootDir\": \"./\",\n    \"incremental\": true\n  },\n  \"exclude\": [\"../node_modules\"],\n  \"include\": [\"./*.ts\", \"./preload/*.ts\", \"../common.d.ts\"]\n}\n"
  },
  {
    "path": "mock-fs.js",
    "content": "// https://github.com/googleapis/google-api-nodejs-client/issues/1775#issuecomment-520572247\n\nmodule.exports = {\n  readFile() {},\n  readFileSync() {}\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"google-tasks-desktop\",\n  \"version\": \"3.1.3\",\n  \"scripts\": {\n    \"dev\": \"nf start\",\n    \"build\": \"yarn app:build && yarn electron:compile\",\n    \"package\": \"rimraf release && yarn build && electron-builder build --publish never\",\n    \"package-all\": \"rimraf release && yarn build && electron-builder build -mwl\",\n    \"lint\": \"eslint 'electron/**/*.ts?(x)' && eslint 'src/**/*.ts?(x)'\",\n    \"app:dev\": \"cross-env BROWSER=false react-app-rewired start\",\n    \"app:build\": \"react-app-rewired build\",\n    \"electron:compile\": \"tsc --project electron/tsconfig.json\",\n    \"electron:dev\": \"cross-env NODE_ENV=development & electron electron/main.js\",\n    \"component\": \"node scripts/component.js\",\n    \"get\": \"node scripts/type.js\",\n    \"redux\": \"node scripts/redux.js\",\n    \"test\": \"react-app-rewired test\",\n    \"eject\": \"react-scripts eject\"\n  },\n  \"homepage\": \".\",\n  \"main\": \"./electron/main.js\",\n  \"build\": {\n    \"productName\": \"Google Tasks\",\n    \"appId\": \"Google Tasks\",\n    \"directories\": {\n      \"buildResources\": \"public\",\n      \"output\": \"release\"\n    },\n    \"extraMetadata\": {\n      \"main\": \"electron/main.js\"\n    },\n    \"files\": [\n      \"build/index.html\",\n      \"build/**/*\",\n      \"electron/**/*.js\",\n      \"package.json\"\n    ],\n    \"extraFiles\": [\n      \"credentials\"\n    ],\n    \"mac\": {\n      \"target\": [\n        \"dmg\",\n        \"pkg\",\n        \"zip\"\n      ],\n      \"darkModeSupport\": true,\n      \"icon\": \"public/icon/icon.png\",\n      \"type\": \"distribution\"\n    },\n    \"dmg\": {\n      \"contents\": [\n        {\n          \"x\": 130,\n          \"y\": 220\n        },\n        {\n          \"x\": 410,\n          \"y\": 220,\n          \"type\": \"link\",\n          \"path\": \"/Applications\"\n        }\n      ]\n    },\n    \"pkg\": {\n      \"license\": \"LICENSE\"\n    },\n    \"win\": {\n      \"target\": [\n        \"nsis\",\n        \"portable\",\n        \"zip\"\n      ],\n      \"icon\": \"public/icon/icon.ico\"\n    },\n    \"nsis\": {\n      \"installerIcon\": \"public/icon/icon.ico\",\n      \"license\": \"LICENSE\",\n      \"warningsAsErrors\": false\n    },\n    \"linux\": {\n      \"target\": [\n        \"AppImage\",\n        \"deb\",\n        \"snap\"\n      ],\n      \"icon\": \"./public/icon/512x512.png\",\n      \"desktop\": {\n        \"Type\": \"Application\",\n        \"Encoding\": \"UTF-8\",\n        \"Name\": \"Google Tasks\",\n        \"Comment\": \"Unofficial google tasks desktop application\",\n        \"Icon\": \"google-tasks-desktop\",\n        \"Terminal\": \"false\",\n        \"StartupWMClass\": \"google-tasks-desktop\"\n      }\n    },\n    \"snap\": {\n      \"grade\": \"stable\"\n    }\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/Pong420/google-tasks-desktop\"\n  },\n  \"author\": {\n    \"name\": \"Pong\",\n    \"email\": \"samfunghp@gmial.com\",\n    \"url\": \"https://pong420.netlify.app\"\n  },\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/Pong420/google-tasks-desktop/issues\"\n  },\n  \"eslintConfig\": {\n    \"extends\": \"react-app\",\n    \"rules\": {\n      \"react/self-closing-comp\": \"warn\",\n      \"import/no-anonymous-default-export\": 0\n    }\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-co3mit\": \"lint-staged\"\n    }\n  },\n  \"lint-staged\": {\n    \"*.{ts,tsx}\": [\n      \"eslint --max-warnings=0\",\n      \"prettier --ignore-path .eslintignore --write\"\n    ],\n    \"{*.json,.{babelrc,eslintrc,prettierrc}}\": [\n      \"prettier --ignore-path .eslintignore --parser json --write\"\n    ],\n    \"*.{css,scss}\": [\n      \"prettier --ignore-path .eslintignore --single-quote --write\"\n    ],\n    \"*.{yml,md}\": [\n      \"prettier --ignore-path .eslintignore --single-quote --write\"\n    ]\n  },\n  \"devDependencies\": {\n    \"@types/history\": \"^4.7.8\",\n    \"@types/lodash\": \"^4.14.168\",\n    \"@types/mousetrap\": \"^1.6.5\",\n    \"@types/node\": \"^14.14.37\",\n    \"@types/nprogress\": \"^0.2.0\",\n    \"@types/react\": \"^17.0.3\",\n    \"@types/react-dom\": \"^17.0.3\",\n    \"@types/react-router-dom\": \"^5.1.7\",\n    \"cross-env\": \"^7.0.3\",\n    \"electron\": \"11.5.0\",\n    \"electron-builder\": \"^22.10.5\",\n    \"electron-devtools-installer\": \"^3.1.1\",\n    \"foreman\": \"^3.0.1\",\n    \"husky\": \"^4.2.3\",\n    \"lint-staged\": \"^10.5.4\",\n    \"react-scripts\": \"4.0.3\",\n    \"prettier\": \"^2.2.1\",\n    \"react-desktop\": \"^0.3.9\",\n    \"rimraf\": \"^3.0.2\",\n    \"sass\": \"^1.32.8\",\n    \"typescript\": \"4.2.3\"\n  },\n  \"dependencies\": {\n    \"@material-ui/core\": \"^4.11.3\",\n    \"@material-ui/icons\": \"^4.11.2\",\n    \"connected-react-router\": \"^6.9.1\",\n    \"customize-cra\": \"^1.0.0\",\n    \"googleapis\": \"^47.0.0\",\n    \"history\": \"^4.10.1\",\n    \"lodash\": \"^4.17.21\",\n    \"mousetrap\": \"^1.6.5\",\n    \"nprogress\": \"^0.2.0\",\n    \"rc-field-form\": \"^1.20.0\",\n    \"react\": \"^17.0.2\",\n    \"react-app-rewired\": \"^2.1.8\",\n    \"react-dom\": \"^17.0.2\",\n    \"react-redux\": \"^7.2.3\",\n    \"react-router-dom\": \"^5.2.0\",\n    \"react-sortable-hoc\": \"^2.0.0\",\n    \"redux\": \"^4.0.5\",\n    \"redux-observable\": \"^1.2.0\",\n    \"reselect\": \"^4.0.0\",\n    \"rxjs\": \"^6.6.6\",\n    \"typeface-nunito-sans\": \"^1.1.13\",\n    \"typeface-roboto\": \"^1.1.13\",\n    \"use-rx-hooks\": \"1.6.2\"\n  },\n  \"devEngines\": {\n    \"node\": \">=7.x\",\n    \"npm\": \">=4.x\",\n    \"yarn\": \">=0.21.3\"\n  },\n  \"browserslist\": [\n    \"last 1 chrome version\"\n  ]\n}\n"
  },
  {
    "path": "public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"shortcut icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, initial-scale=1, shrink-to-fit=no\"\n    />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>Google Tasks</title>\n    <script>\n      __setTheme();\n      __setAccentColor();\n      __setTitleBar();\n      document.documentElement.setAttribute('data-platform', window.platform);\n    </script>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "public/manifest.json",
    "content": "{\n  \"short_name\": \"Google Tasks\",\n  \"name\": \"Create Google Tasks Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "scripts/component.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst pkg = require('../package.json');\n\nconst useTypescript =\n  (pkg['devDependencies'] || {}).typescript ||\n  (pkg['dependencies'] || {}).typescript;\n\nconst componentName = process.argv\n  .slice(2)\n  .find(v => !/-/.test(v))\n  .replace(/^\\w/, function (chr) {\n    return chr.toUpperCase();\n  });\nconst componentOnly = process.argv.find(v => v === '-s');\nconst dir = path.join(\n  __dirname,\n  `../src/components/`,\n  componentOnly ? '' : componentName\n);\n\nconst write = (path, content) => {\n  fs.writeFileSync(\n    path,\n    content.replace(/^ {2}/gm, '').replace(/^ *\\n/, ''),\n    'utf-8'\n  );\n};\n\nif (!fs.existsSync(dir)) {\n  fs.mkdirSync(dir);\n}\n\nconst index = `\n  import './${componentName}.scss';\n  \n  export * from './${componentName}';\n  export { ${componentName} as default } from './${componentName}';\n  `;\n\nconst reactComponent = `\nimport React from 'react';\n  \n  export function ${componentName}() {\n    return (\n      <div className=\"${componentName\n        .split(/(?=[A-Z])/)\n        .map(str => str.toLocaleLowerCase())\n        .join('-')}\"></div>\n    );\n  }\n  `;\n\nconst scss = '';\n\nif (!componentOnly) {\n  write(`${dir}/index.${useTypescript ? 'ts' : 'js'}`, index);\n  write(`${dir}/${componentName}.scss`, scss);\n}\n\nwrite(\n  `${dir}/${componentName}.${useTypescript ? 'tsx' : 'jsx'}`,\n  reactComponent\n);\n"
  },
  {
    "path": "scripts/electron-wait-react.js",
    "content": "// @ts-check\nconst exec = require('child_process').exec;\nconst net = require('net');\n\nlet client = new net.Socket();\nconst port = process.env.PORT ? Number(process.env.PORT) - 100 : 3000;\n\nprocess.env.ELECTRON_START_URL = `http://localhost:${port}`;\n\nlet startedElectron = false;\nconst tryConnection = () =>\n  client.connect({ port }, () => {\n    client.end();\n    if (!startedElectron) {\n      process.off('uncaughtException', uncaughtException);\n      client.destroy();\n      console.log('starting electron');\n      startedElectron = true;\n      exec('npm run electron:dev');\n    }\n  });\n\nexec('npm run electron:compile', tryConnection);\n\nfunction uncaughtException() {\n  client.destroy();\n  client = new net.Socket();\n  setTimeout(tryConnection, 1000);\n}\n\nprocess.on('uncaughtException', uncaughtException);\n"
  },
  {
    "path": "scripts/redux.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst { exec } = require('child_process');\nconst [, , ...args] = process.argv;\n\nconst execPromise = command => {\n  return new Promise((resolve, reject) => {\n    const cmd = exec(command, (error, stdout) => {\n      if (error) {\n        reject(error);\n        return;\n      }\n\n      resolve();\n    });\n\n    cmd.stdout.on('data', data => {\n      console.log(data.trim());\n    });\n  });\n};\n\nconst root = path.join(__dirname, '../src');\nconst templatePath = path.join(__dirname, 'template');\nconst store = path.join(root, 'store');\nconst subdir = ['actions', 'epics', 'reducers'].map(dir =>\n  path.join(store, dir)\n);\n\nif (args[0] === 'init') {\n  const commands = [\n    `yarn add redux`,\n    `yarn add react-redux && yarn add --dev @types/react-redux`,\n    `yarn add rxjs && yarn add redux-observable`\n  ];\n\n  (async () => {\n    for (const cmd of commands) {\n      await execPromise(cmd);\n    }\n  })();\n\n  [store, ...subdir].forEach(dir => {\n    const key = dir.split('/').slice(-1)[0];\n    fs.readFile(\n      path.join(templatePath, 'store', `${key}.tmpl`),\n      (error, content) => {\n        if (!error) {\n          !fs.existsSync(dir) && fs.mkdirSync(dir);\n          fs.writeFileSync(path.join(dir, 'index.ts'), content);\n        }\n      }\n    );\n  });\n\n  fs.readFile(path.join(templatePath, 'useActions.tmpl'), (error, content) => {\n    if (!error) {\n      fs.writeFileSync(path.join(root, 'hooks', 'useActions.ts'), content);\n    }\n  });\n} else if (args[0]) {\n  const filename = args[0];\n  subdir.map(dir => fs.writeFileSync(path.join(dir, `${filename}.ts`), ''));\n}\n"
  },
  {
    "path": "scripts/template/store/actions.tmpl",
    "content": ""
  },
  {
    "path": "scripts/template/store/epics.tmpl",
    "content": "import { combineEpics } from 'redux-observable';\n\nexport default combineEpics(\n\n);\n"
  },
  {
    "path": "scripts/template/store/reducers.tmpl",
    "content": "import { combineReducers } from 'redux';\n\nconst rootReducer = () => combineReducers({\n\n});\n\nexport type RootState = ReturnType<ReturnType<typeof rootReducer>>;\n\nexport default rootReducer;\n"
  },
  {
    "path": "scripts/template/store/store.tmpl",
    "content": "import { createStore, applyMiddleware, compose } from 'redux';\nimport { createEpicMiddleware, Epic } from 'redux-observable';\nimport { BehaviorSubject } from 'rxjs';\nimport { switchMap } from 'rxjs/operators';\nimport rootEpic from './epics';\nimport createRootReducer from './reducers';\n\nconst epic$ = new BehaviorSubject(rootEpic);\nconst hotReloadingEpic: Epic = (...args) =>\n  epic$.pipe(switchMap(epic => epic(...args)));\n\nexport default function configureStore() {\n  const epicMiddleware = createEpicMiddleware();\n  const composeEnhancers =\n    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;\n  const enhancer = composeEnhancers(applyMiddleware(epicMiddleware));\n\n  const store = createStore(createRootReducer(), undefined, enhancer);\n\n  epicMiddleware.run(hotReloadingEpic);\n\n  if (process.env.NODE_ENV !== 'production') {\n    if (module.hot) {\n      module.hot.accept('./reducers', () => {\n        store.replaceReducer(createRootReducer());\n      });\n\n      module.hot.accept('./epics', () => {\n        const nextRootEpic = require('./epics').default;\n        epic$.next(nextRootEpic);\n      });\n    }\n  }\n\n  return store;\n}\n\nexport * from './actions';\nexport * from './reducers';\nexport * from './epics';\n"
  },
  {
    "path": "scripts/template/useActions.tmpl",
    "content": "import { useMemo, useRef, Dispatch as ReactDispatch } from 'react';\nimport { AnyAction, Dispatch } from 'redux';\nimport { useDispatch } from 'react-redux';\n\ninterface ActionCreators {\n  [k: string]: (...args: any[]) => AnyAction;\n}\n\ntype Handler<A extends ActionCreators> = {\n  [X in keyof A]: (...args: Parameters<A[X]>) => void;\n};\n\nexport function withDispatch<A extends ActionCreators>(\n  creators: A,\n  dispatch: Dispatch | ReactDispatch<any>\n) {\n  const handler = {} as Handler<A>;\n  for (const key in creators) {\n    const creator = creators[key];\n    handler[key] = (...args: Parameters<typeof creator>) => {\n      dispatch(creator(...args));\n    };\n  }\n\n  return handler;\n}\n\nexport function useActions<A extends ActionCreators>(creators: A): Handler<A> {\n  const dispatch = useDispatch();\n  const creatorsRef = useRef(creators);\n\n  return useMemo(() => withDispatch(creatorsRef.current, dispatch), [dispatch]);\n}"
  },
  {
    "path": "scripts/type.js",
    "content": "const { exec } = require('child_process');\n\nconst [, , ...args] = process.argv;\n\nconst install = pkg => {\n  return `yarn add ${pkg} && yarn add --dev @types/${pkg}`;\n};\n\nconst execPromise = command => {\n  return new Promise((resolve, reject) => {\n    const cmd = exec(command, (error, stdout) => {\n      if (error) {\n        reject(error);\n        return;\n      }\n\n      resolve();\n    });\n\n    cmd.stdout.on('data', data => {\n      console.log(data.trim());\n    });\n  });\n};\n\nconst promises = args.map(pkg => execPromise(install(pkg)));\n\nPromise.all(promises).then(() => {\n  process.exit(0);\n});\n"
  },
  {
    "path": "src/App.tsx",
    "content": "import React from 'react';\nimport { Route, Switch, Redirect, generatePath } from 'react-router-dom';\nimport { AppRegion } from './components/AppRegion';\nimport { PrivateRoute } from './components/PrivateRoute';\nimport { Auth } from './pages/Auth';\nimport { TaskList } from './pages/TaskList';\nimport { PATHS } from './constants';\n\nconst App = () => {\n  return (\n    <>\n      <AppRegion />\n      <Switch>\n        <Route path={PATHS.AUTH} component={Auth} />\n        <PrivateRoute path={PATHS.TASKLIST} component={TaskList} />\n        <Redirect to={generatePath(PATHS.TASKLIST, {})} />\n      </Switch>\n    </>\n  );\n};\n\nexport default App;\n"
  },
  {
    "path": "src/components/AppRegion/AppRegion.scss",
    "content": ".app-region {\n  @include absolute(0, null, 0);\n  @include dimen(100%, var(--header-height));\n\n  .simple-title-bar {\n    @include sq-dimen(100%);\n    @include app-region-drag;\n    padding: 10px 0px 0px 10px;\n    button {\n      @include app-region-no-drag;\n      z-index: $app-region-z-index;\n    }\n  }\n\n  // drawin\n  .app-region-drag {\n    @include app-region-drag;\n    @include sq-dimen(100%);\n  }\n}\n\n[data-platform^='win32'][data-title-bar^='native'] {\n  .app-region {\n    height: auto;\n    > div {\n      @include relative();\n      z-index: $app-region-z-index;\n    }\n  }\n\n  .windows-title {\n    @include flex(center);\n    svg {\n      @include sq-dimen(14px);\n      margin-top: 1px;\n      margin-right: 5px;\n    }\n  }\n}\n\nbody {\n  > [role='dialog'],\n  > [role='presentation'] {\n    @include app-region-no-drag;\n    z-index: $app-region-z-index !important;\n  }\n}\n"
  },
  {
    "path": "src/components/AppRegion/AppRegion.tsx",
    "content": "import React, { ReactNode } from 'react';\nimport { useSelector } from 'react-redux';\nimport { IconButton } from '../Mui';\nimport { titleBarSelector } from '../../store';\nimport { WindowsTitleBar } from './WindowsTitleBar';\nimport Close from '@material-ui/icons/Close';\n\nconst { close } = window.getCurrentWindow();\n\nexport function AppRegion() {\n  const titleBar = useSelector(titleBarSelector);\n  let content: ReactNode = null;\n\n  if (window.platform === 'darwin') {\n    content = <div className=\"app-region-drag\" />;\n  } else if (titleBar === 'frameless') {\n    content = (\n      <div className=\"simple-title-bar\">\n        {/* should not pass `close` function directly into `onClick` props */}\n        <IconButton icon={Close} onClick={() => close()} />\n      </div>\n    );\n  } else {\n    // native\n    if (window.platform === 'win32') {\n      content = <WindowsTitleBar />;\n    }\n  }\n\n  return <div className=\"app-region\">{content}</div>;\n}\n"
  },
  {
    "path": "src/components/AppRegion/WindowsTitleBar.tsx",
    "content": "import React, { useState, useEffect, Suspense } from 'react';\nimport { useSelector } from 'react-redux';\nimport { themeSelector } from '../../store';\nimport { ReactComponent as Logo } from '../../assets/logo.svg';\n\nconst WindowsTitleBarComp = React.lazy(() =>\n  import('react-desktop/windows').then(({ TitleBar }) => ({\n    default: TitleBar\n  }))\n);\n\nconst {\n  isMaximized,\n  minimize,\n  maximize,\n  unmaximize,\n  close\n} = window.getCurrentWindow();\n\nfunction toggleMaximize() {\n  isMaximized() ? unmaximize() : maximize();\n}\n\nexport function WindowsTitleBar() {\n  const [isFullscreen, setIsFullscreen] = useState(isMaximized());\n  const theme = useSelector(themeSelector);\n\n  useEffect(() => {\n    function onResize() {\n      setIsFullscreen(isMaximized());\n    }\n\n    onResize();\n\n    window.addEventListener('resize', onResize);\n\n    return () => window.removeEventListener('resize', onResize);\n  }, []);\n\n  return (\n    <Suspense fallback={null}>\n      <WindowsTitleBarComp\n        title={\n          <div className=\"windows-title\">\n            <Logo />\n            Google Tasks\n          </div>\n        }\n        controls\n        theme={theme}\n        background=\"var(--main-color)\"\n        isMaximized={isFullscreen}\n        onCloseClick={() => close()}\n        onMinimizeClick={() => minimize()}\n        onMaximizeClick={() => toggleMaximize()}\n        onRestoreDownClick={() => toggleMaximize()}\n      />\n    </Suspense>\n  );\n}\n"
  },
  {
    "path": "src/components/AppRegion/index.ts",
    "content": "import './AppRegion.scss';\n\nexport * from './AppRegion';\nexport { AppRegion as default } from './AppRegion';\n"
  },
  {
    "path": "src/components/KeyboardShortcuts/KeyboardShortcuts.scss",
    "content": ".keyboard-shortcuts {\n  color: var(--text-color);\n\n  .keyboard-shortcuts-label {\n    @include dimen(100%);\n  }\n\n  .keyboard-shortcuts-key {\n    min-width: 100px;\n    text-align: right;\n    padding-left: 10px;\n  }\n}\n"
  },
  {
    "path": "src/components/KeyboardShortcuts/KeyboardShortcuts.tsx",
    "content": "import React from 'react';\nimport { FullScreenDialog, FullScreenDialogProps } from '../Mui/Dialog';\nimport shortcuts from './shortcuts.json';\n\nfunction normalizeKeyName(str: string) {\n  switch (window.platform) {\n    case 'darwin':\n      return str.replace('Alt', '⌥');\n\n    default:\n      return str;\n  }\n}\n\nexport function KeyboardShortcuts(props: FullScreenDialogProps) {\n  return (\n    <FullScreenDialog\n      {...props}\n      className=\"keyboard-shortcuts\"\n      title=\"Keyboard shortcuts\"\n    >\n      <div className=\"keyboard-shortcuts-content\">\n        {Object.entries(shortcuts).map(([type, rows]) => (\n          <FullScreenDialog.Section key={type}>\n            <FullScreenDialog.Title children={type} />\n            {rows.map(({ label, key }, index) => (\n              <FullScreenDialog.Row key={index}>\n                <div className=\"keyboard-shortcuts-label\">{label}</div>\n                <div className=\"keyboard-shortcuts-key\">\n                  {normalizeKeyName(key)}\n                </div>\n              </FullScreenDialog.Row>\n            ))}\n          </FullScreenDialog.Section>\n        ))}\n      </div>\n    </FullScreenDialog>\n  );\n}\n"
  },
  {
    "path": "src/components/KeyboardShortcuts/index.ts",
    "content": "import './KeyboardShortcuts.scss';\n\nexport * from './KeyboardShortcuts';\nexport { KeyboardShortcuts as default } from './KeyboardShortcuts';\n"
  },
  {
    "path": "src/components/KeyboardShortcuts/shortcuts.json",
    "content": "{\n  \"Actions\": [\n    { \"label\": \"Add a task\", \"key\": \"Enter\" },\n    { \"label\": \"Focus on previous/next task\", \"key\": \"Up/Down\" },\n    { \"label\": \"Move task up/down\", \"key\": \"Alt + Up/Down\" },\n    { \"label\": \"Enter details view\", \"key\": \"Shift + Enter\" }\n  ],\n  \"Details view\": [{ \"label\": \"Exit details view\", \"key\": \"Esc\" }]\n}\n"
  },
  {
    "path": "src/components/Mui/DeleteIcon.tsx",
    "content": "import React from 'react';\nimport SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';\n\nexport const DeleteIcon = React.forwardRef<SVGSVGElement, SvgIconProps>(\n  (props, ref) => {\n    return (\n      <SvgIcon {...props} ref={ref}>\n        <path d=\"M15 4V3H9v1H4v2h1v13c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V6h1V4h-5zm2 15H7V6h10v13z\" />\n        <path d=\"M9 8h2v9H9zm4 0h2v9h-2z\" />\n      </SvgIcon>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/Mui/Dialog/ConfirmDialog.tsx",
    "content": "import React, { useMemo, ReactNode } from 'react';\nimport Button from '@material-ui/core/Button';\nimport Dialog, { DialogProps } from '@material-ui/core/Dialog';\nimport mergeWith from 'lodash/mergeWith';\n\nexport interface ConfirmDialogProps extends DialogProps {\n  title?: string;\n  confirmLabel?: string;\n  open: boolean;\n  children?: ReactNode;\n  onClose(): void;\n  onConfirm: () => unknown;\n  autoFocusConfirmButon?: boolean;\n}\n\nconst backdropProps = { classes: { root: 'mui-menu-backdrop' } };\n\nexport function ConfirmDialog({\n  title,\n  confirmLabel = 'Confirm',\n  children,\n  onClose,\n  onConfirm,\n  autoFocusConfirmButon = true,\n  classes,\n  ...props\n}: ConfirmDialogProps) {\n  const mergedClasses = useMemo<ConfirmDialogProps['classes']>(\n    () =>\n      mergeWith(\n        { root: 'mui-dialog', paper: 'mui-dialog-paper' },\n        classes,\n        (a, b) => a + ' ' + b\n      ),\n    [classes]\n  );\n\n  return (\n    <Dialog\n      {...props}\n      onClose={onClose}\n      classes={mergedClasses}\n      BackdropProps={backdropProps}\n    >\n      <div className=\"dialog-scroll-content\">\n        <div className=\"dialog-title\">{title}</div>\n        <div className=\"dialog-content\">{children}</div>\n        <div className=\"dialog-actions\">\n          <Button onClick={onClose}>Cancel</Button>\n          <Button\n            onClick={() => onConfirm() !== false && onClose()}\n            autoFocus={autoFocusConfirmButon}\n          >\n            {confirmLabel}\n          </Button>\n        </div>\n      </div>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "src/components/Mui/Dialog/Dialog.scss",
    "content": ".mui-dialog-paper.mui-dialog-paper {\n  @include dimen(100%);\n  @include margin-x(25px);\n  color: var(--text--color);\n  background-color: var(--main-color-diff2);\n  border-radius: 8px;\n  max-width: 280px;\n  padding: 0;\n\n  [data-theme^='light'] & {\n    box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14),\n      0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2);\n  }\n  [data-theme^='dark'] & {\n    box-shadow: 0px 0px 3px 1px var(--shadow-color);\n  }\n\n  .dialog-scroll-content {\n    max-height: 100%;\n    padding: 15px 20px;\n  }\n\n  .dialog-title {\n    @include typeface('Nunito Sans', 700);\n    color: var(--text-color);\n    margin-bottom: 10px;\n  }\n\n  .dialog-actions {\n    @include flex(center, flex-end);\n    flex: 0 0 auto;\n    margin-top: 10px;\n\n    button {\n      @include typeface('Nunito Sans', 700);\n\n      background: none;\n      text-transform: initial;\n\n      + button {\n        color: var(--accent-dark-color);\n        margin-left: 10px;\n      }\n    }\n  }\n}\n\n.form-dialog-error-message {\n  @include typeface('Nunito Sans', 600);\n  color: var(--error-color);\n  font-size: 13px;\n  margin-top: 3px;\n  padding-left: 3px;\n}\n\n.fullscreen-dialog-paper.fullscreen-dialog-paper {\n  background-color: var(--main-color);\n  color: var(--text-color);\n}\n\n.fullscreen-diaglog-header {\n  @include app-region-drag;\n  @include dimen(100%, 70px);\n  @include flex(center, space-between);\n  @include fake-border($borderWidth: 1px, $color: var(--main-color-diff));\n  @include padding-x(15px 10px);\n  flex: 0 0 auto;\n\n  .mui-icon-button {\n    @include app-region-no-drag;\n  }\n\n  h4 {\n    @include margin-y(0 15px);\n    color: var(--text-color);\n    font-size: 18px;\n    font-weight: 500;\n    margin-bottom: 0;\n  }\n\n  [data-platform^='darwin'] & {\n    height: 70px;\n    padding-bottom: 5px;\n\n    h4 {\n      margin-bottom: 7.5px;\n      align-self: flex-end;\n    }\n  }\n}\n\n.fullscreen-dialog-content {\n  $padding-x: 15px;\n  @include flex();\n  @include sq-dimen(100%);\n  overflow: auto;\n\n  .fullscreen-dialog-inner-content {\n    @include dimen(100%);\n    @include padding-x($padding-x);\n  }\n\n  .fullscreen-dialog-section {\n    .fullscreen-dialog-section-title {\n      @include margin-x(-$padding-x);\n      @include padding-x($padding-x);\n      @include padding-y(12px);\n      background-color: var(--main-color-diff);\n      border-top: 1px solid var(--border-color);\n      border-bottom: 1px solid var(--border-color);\n      font-weight: 500;\n      font-size: 15px;\n    }\n\n    .fullscreen-dialog-row {\n      @include flex(center, space-between);\n      @include padding-y(12px);\n      min-height: 50px;\n\n      + .fullscreen-dialog-row {\n        border-top: 1px solid var(--border-color);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/Mui/Dialog/FormDialog.tsx",
    "content": "import React, { useCallback, useState, useRef, FormEvent } from 'react';\nimport { ConfirmDialog, ConfirmDialogProps } from './ConfirmDialog';\nimport { Input } from '../Input';\n\ninterface Props extends Omit<ConfirmDialogProps, 'onConfirm' | 'confirmLabel'> {\n  defaultValue?: string;\n  errorMsg?: string;\n  onConfirm(payload: string): void;\n}\n\nconst INPUT_NAME = 'NAME';\n\nexport function FormDialog({\n  defaultValue = '',\n  errorMsg,\n  onClose,\n  onConfirm,\n  ...props\n}: Props) {\n  const formRef = useRef<HTMLFormElement>(null);\n  const [error, setError] = useState(false);\n\n  const submitCallback = useCallback(\n    (evt?: FormEvent<HTMLFormElement>) => {\n      evt && evt.preventDefault();\n\n      const formEl = formRef.current;\n      if (formEl) {\n        const formData = new FormData(formEl);\n        const trimedValue = (formData.get(INPUT_NAME) as string).trim();\n\n        if (!trimedValue) {\n          setError(true);\n          return false;\n        } else if (trimedValue !== defaultValue) {\n          onConfirm(trimedValue);\n          onClose();\n        }\n      }\n    },\n    [onClose, onConfirm, defaultValue]\n  );\n\n  const onExitedCallback = useCallback(() => {\n    setError(false);\n  }, []);\n\n  return (\n    <ConfirmDialog\n      {...props}\n      autoFocusConfirmButon={false}\n      confirmLabel=\"Done\"\n      onConfirm={submitCallback}\n      onClose={onClose}\n      onExited={onExitedCallback}\n    >\n      <form onSubmit={submitCallback} ref={formRef}>\n        <Input\n          autoFocus\n          className=\"filled bottom-border\"\n          defaultValue={defaultValue}\n          error={error}\n          name={INPUT_NAME}\n          placeholder=\"Enter name\"\n        />\n        <div className=\"form-dialog-error-message\">{error && errorMsg}</div>\n      </form>\n    </ConfirmDialog>\n  );\n}\n"
  },
  {
    "path": "src/components/Mui/Dialog/FullScreenDialog.tsx",
    "content": "import React, { ReactNode } from 'react';\nimport Dialog, { DialogProps } from '@material-ui/core/Dialog';\nimport { SlideProps } from '@material-ui/core/Slide';\nimport { IconButton } from '../IconButton';\nimport Slide from '@material-ui/core/Slide';\nimport CloseIcon from '@material-ui/icons/Close';\n\nexport interface FullScreenDialogProps extends Omit<DialogProps, 'title'> {\n  title?: string;\n  headerComponents?: ReactNode;\n  onClose(): void;\n}\n\ninterface ContainerProps {\n  children?: ReactNode;\n}\n\nexport const FULLSCREEN_DIALOG_TRANSITION = 300;\n\nconst Transition = React.forwardRef<unknown, SlideProps>((props, ref) => {\n  return <Slide direction=\"left\" ref={ref} {...props} />;\n});\n\nconst backdropProps = {\n  open: false\n};\n\nconst paperClasses = { paper: 'fullscreen-dialog-paper' };\n\nexport function FullScreenDialog({\n  children,\n  title,\n  onClose,\n  headerComponents,\n  ...props\n}: FullScreenDialogProps) {\n  return (\n    <Dialog\n      {...props}\n      fullScreen\n      BackdropProps={backdropProps}\n      classes={paperClasses}\n      onClose={onClose}\n      transitionDuration={FULLSCREEN_DIALOG_TRANSITION}\n      TransitionComponent={Transition}\n    >\n      <div className=\"fullscreen-diaglog-header\">\n        <h4>{title}</h4>\n        <div>\n          {headerComponents}\n          <IconButton tooltip=\"Close task\" icon={CloseIcon} onClick={onClose} />\n        </div>\n      </div>\n      <div className=\"fullscreen-dialog-content\">\n        <div className=\"fullscreen-dialog-inner-content\">{children}</div>\n      </div>\n    </Dialog>\n  );\n}\n\nFullScreenDialog.Section = ({ children }: ContainerProps) => {\n  return <div className=\"fullscreen-dialog-section\">{children}</div>;\n};\n\nFullScreenDialog.Title = ({ children }: ContainerProps) => {\n  return <div className=\"fullscreen-dialog-section-title\">{children}</div>;\n};\n\nFullScreenDialog.Row = ({ children }: ContainerProps) => {\n  return <div className=\"fullscreen-dialog-row\">{children}</div>;\n};\n\nexport default FullScreenDialog;\n"
  },
  {
    "path": "src/components/Mui/Dialog/index.ts",
    "content": "import './Dialog.scss';\n\nexport * from './ConfirmDialog';\nexport * from './FormDialog';\nexport * from './FullScreenDialog';\n"
  },
  {
    "path": "src/components/Mui/Dropdown/Dropdown.scss",
    "content": ".mui-dropdown-button {\n  &.mui-dropdown-button {\n    @include flex(center, space-between);\n    font-size: inherit;\n    text-transform: initial;\n    padding: 0 0 0 10px;\n\n    > span:nth-child(1) {\n      div {\n        @include text-overflow-ellipsis();\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/Mui/Dropdown/Dropdown.tsx",
    "content": "import React, { useMemo, ReactNode, MouseEvent, forwardRef } from 'react';\nimport { Menu, MenuProps } from '../Menu';\nimport Button, { ButtonProps } from '@material-ui/core/Button';\nimport ArrowDropDownIcon from '@material-ui/icons/ArrowDropDownRounded';\nimport mergeWith from 'lodash/mergeWith';\n\nexport interface DropdownProps extends Omit<MenuProps, 'onClick' | 'ref'> {\n  label?: string;\n  onClick(evt: MouseEvent<HTMLButtonElement>): void;\n  children?: ReactNode;\n  buttonProps?: ButtonProps;\n}\n\nexport const Dropdown = forwardRef<HTMLButtonElement, DropdownProps>(\n  ({ buttonProps, children, classes, label, onClick, ...props }, ref) => {\n    const mergedMenuClasses = useMemo<MenuProps['classes']>(\n      () =>\n        mergeWith(\n          { paper: 'dropdown-menu-paper' },\n          classes,\n          (a, b) => a + ' ' + b\n        ),\n      [classes]\n    );\n\n    const { classes: buttonClasses, ...otherButtonProps } = buttonProps || {\n      classes: ''\n    };\n\n    const mergedButtonClasses = useMemo<ButtonProps['classes']>(\n      () =>\n        mergeWith(\n          { root: 'mui-dropdown-button' },\n          buttonClasses,\n          (a, b) => a + ' ' + b\n        ),\n      [buttonClasses]\n    );\n\n    return (\n      <>\n        <Button\n          ref={ref}\n          classes={mergedButtonClasses}\n          onClick={onClick}\n          disableFocusRipple\n          {...otherButtonProps}\n        >\n          <div>{label}</div>\n          <ArrowDropDownIcon fontSize=\"default\" />\n        </Button>\n        <Menu {...props} classes={mergedMenuClasses}>\n          {children}\n        </Menu>\n      </>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/Mui/Dropdown/index.ts",
    "content": "import './Dropdown.scss';\n\nexport * from './Dropdown';\nexport { Dropdown as default } from './Dropdown';\n"
  },
  {
    "path": "src/components/Mui/EditIcon.tsx",
    "content": "import React from 'react';\nimport SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';\n\nexport const EditIcon = React.forwardRef<SVGSVGElement, SvgIconProps>(\n  (props, ref) => {\n    return (\n      <SvgIcon {...props} ref={ref}>\n        <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\" />\n      </SvgIcon>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/Mui/IconButton/IconButton.scss",
    "content": ".mui-icon-button.mui-icon-button {\n  color: var(--text-secondary-color);\n}\n"
  },
  {
    "path": "src/components/Mui/IconButton/IconButton.tsx",
    "content": "import React, { ComponentType, ReactElement } from 'react';\nimport { SvgIconProps } from '@material-ui/core/SvgIcon';\nimport MuiIconButton, { IconButtonProps } from '@material-ui/core/IconButton';\nimport Tooltip from '@material-ui/core/Tooltip';\n\ninterface Props extends Partial<IconButtonProps> {\n  tooltip?: string;\n  icon?: ComponentType<Omit<SvgIconProps, 'ref'>>;\n  iconProps?: SvgIconProps;\n  children?: ReactElement<{}>;\n}\n\nconst PopperProps = {\n  popperOptions: {\n    modifiers: {\n      offset: {\n        fn: (data: any) => {\n          data.offsets.reference.top = data.offsets.reference.top + 75;\n          return data;\n        }\n      }\n    }\n  }\n};\n\nexport function IconButton({\n  className = '',\n  tooltip = '',\n  icon: Icon,\n  iconProps,\n  children,\n  ...props\n}: Props) {\n  return (\n    <MuiIconButton className={`mui-icon-button ${className}`.trim()} {...props}>\n      <Tooltip title={tooltip} PopperProps={PopperProps}>\n        {Icon ? <Icon {...iconProps} /> : children ? children : <div />}\n      </Tooltip>\n    </MuiIconButton>\n  );\n}\n"
  },
  {
    "path": "src/components/Mui/IconButton/index.ts",
    "content": "import './IconButton.scss';\n\nexport * from './IconButton';\nexport { IconButton as default } from './IconButton';\n"
  },
  {
    "path": "src/components/Mui/Input/Input.scss",
    "content": ".mui-input-base.mui-input-base {\n  font-size: inherit;\n  min-height: 40px;\n  padding: 1px 0px 0px 10px;\n\n  input,\n  textarea {\n    color: var(--text-color);\n    outline: none;\n    overflow: hidden;\n\n    &:hover:not(:focus) {\n      cursor: default;\n    }\n  }\n\n  &.filled {\n    @include textHighlight();\n    background-color: var(--task-highlight-background-color);\n  }\n\n  &.bottom-border {\n    border-bottom: 1px solid var(--border-color);\n  }\n\n  &.error {\n    &:after {\n      border-color: var(--error-color) !important;\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/Mui/Input/Input.tsx",
    "content": "import React, { useMemo } from 'react';\nimport InputBase, { InputBaseProps } from '@material-ui/core/InputBase';\n\nexport type InputProps = Omit<InputBaseProps, 'ref'>;\n\nexport function Input({ className = '', ...props }: InputProps) {\n  const mergedClasses = useMemo<InputBaseProps['classes']>(\n    () => ({\n      root: `mui-input-base ${className}`.trim(),\n      focused: 'focused',\n      multiline: 'multiline',\n      error: 'error'\n    }),\n    [className]\n  );\n\n  return <InputBase {...props} fullWidth classes={mergedClasses} />;\n}\n"
  },
  {
    "path": "src/components/Mui/Input/index.ts",
    "content": "import './Input.scss';\n\nexport * from './Input';\nexport { Input as default } from './Input';\n"
  },
  {
    "path": "src/components/Mui/Menu/Menu.scss",
    "content": ".mui-menu-paper {\n  &.mui-menu-paper {\n    border-radius: 8px;\n    background-color: var(--paper-background-color);\n    // prettier-ignore\n    box-shadow: \n      0 0 0 1px  var(--shadow-color),\n      0 2px 4px var(--shadow-color), \n      0 8px 24px var(--shadow-color);\n    color: var(--text-color);\n  }\n\n  .menu-content {\n    @include padding-y(8px);\n  }\n}\n\n.mui-menu-item {\n  &.mui-menu-item {\n    @include padding-y(8px);\n    color: var(--text-color);\n    font-size: inherit;\n    min-height: 0;\n\n    &:hover {\n      background-color: var(--menu-item-hover-color);\n    }\n  }\n\n  > div {\n    @include flex(center, space-between);\n    flex: 1 1 auto;\n    max-width: 100%;\n\n    .text {\n      @include text-overflow-ellipsis();\n      flex: 1 1 auto;\n      padding-right: 25px;\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/Mui/Menu/Menu.tsx",
    "content": "import React, { useMemo, ReactNode } from 'react';\nimport Popover, { PopoverProps } from '@material-ui/core/Popover';\nimport mergeWith from 'lodash/mergeWith';\n\nexport interface MenuProps extends PopoverProps {\n  onClose(): void;\n  footer?: ReactNode;\n}\n\nconst backdropProps = {\n  classes: { root: 'mui-menu-backdrop' },\n  invisible: true\n};\n\nexport const Menu = ({ classes, children, footer, ...props }: MenuProps) => {\n  const mergedClasses = useMemo<PopoverProps['classes']>(\n    () =>\n      mergeWith({ paper: 'mui-menu-paper' }, classes, (a, b) => a + ' ' + b),\n    [classes]\n  );\n\n  return (\n    <Popover classes={mergedClasses} BackdropProps={backdropProps} {...props}>\n      <div className=\"menu-content\">\n        <div className=\"scroll-content\">{children}</div>\n        {footer}\n      </div>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "src/components/Mui/Menu/MenuItem.tsx",
    "content": "import React, { forwardRef, useCallback, MouseEvent } from 'react';\nimport MuiMenuItem from '@material-ui/core/MenuItem';\nimport TickIcon from '@material-ui/icons/Check';\n\ntype DefaultMenuItemProps = Parameters<typeof MuiMenuItem>[0];\n\nexport interface MenuItemProps extends DefaultMenuItemProps {\n  text?: string;\n}\n\ninterface Props {\n  onClose(): void;\n}\n\nconst menuItemClasses = { root: 'mui-menu-item' };\n\nexport const MenuItem = forwardRef<HTMLLIElement, Props & MenuItemProps>(\n  ({ children, text, onClick, onClose, selected, ...props }, ref) => {\n    const onClickCallback = useCallback(\n      (evt: MouseEvent<HTMLLIElement>) => {\n        onClose();\n        onClick && onClick(evt);\n      },\n      [onClose, onClick]\n    );\n\n    return (\n      <MuiMenuItem\n        classes={menuItemClasses}\n        onClick={onClickCallback}\n        ref={ref}\n        {...props}\n      >\n        <div>\n          <div className=\"text\">{text}</div>\n          {selected && <TickIcon />}\n        </div>\n        {children}\n      </MuiMenuItem>\n    );\n  }\n);\n\nexport function useMuiMenuItem({ onClose }: Props) {\n  // eslint-disable-next-line\n  return useCallback(\n    forwardRef<HTMLLIElement, MenuItemProps>((props, ref) => (\n      <MenuItem onClose={onClose} {...props} ref={ref} />\n    )),\n    [onClose]\n  );\n}\n"
  },
  {
    "path": "src/components/Mui/Menu/index.ts",
    "content": "import './Menu.scss';\n\nexport * from './Menu';\nexport * from './MenuItem';\nexport * from './useMuiMenu';\nexport { Menu as default } from './Menu';\n"
  },
  {
    "path": "src/components/Mui/Menu/useMuiMenu.ts",
    "content": "import {\n  useState,\n  useCallback,\n  useEffect,\n  SyntheticEvent,\n  MouseEvent\n} from 'react';\nimport { MenuProps } from '@material-ui/core/Menu';\n\nexport type AnchorEl = HTMLElement | null;\nexport type AnchorPosition = MenuProps['anchorPosition'];\n\nfunction instanceOfAnchorEl(object: any): object is AnchorEl {\n  return object === null || object instanceof HTMLElement;\n}\n\nfunction instanceOfAnchorPosition(object: any): object is AnchorPosition {\n  return typeof object === 'undefined' || (object.top && object.left);\n}\n\nexport function useMuiMenu() {\n  const [anchorEl, setAnchorEl] = useState<AnchorEl>(null);\n  const [anchorPosition, setAnchorPosition] = useState<AnchorPosition>();\n  const onClose = useCallback(() => {\n    setAnchorEl(null);\n    setAnchorPosition(undefined);\n  }, []);\n\n  useEffect(() => {\n    setAnchorEl(anchorEl);\n  }, [anchorEl]);\n\n  useEffect(() => {\n    setAnchorPosition(anchorPosition);\n  }, [anchorPosition]);\n\n  const setAnchorElCallback = useCallback(\n    (props: SyntheticEvent<HTMLElement> | AnchorEl) => {\n      if (instanceOfAnchorEl(props)) {\n        setAnchorEl(props);\n      } else {\n        setAnchorEl(props.currentTarget);\n      }\n    },\n    []\n  );\n\n  const setAnchorPositionCallback = useCallback(\n    (props: MouseEvent<HTMLElement> | AnchorPosition) => {\n      if (instanceOfAnchorPosition(props)) {\n        setAnchorPosition(props);\n      } else {\n        const evt = props;\n        evt && evt.preventDefault();\n        setAnchorPosition({\n          top: evt.pageY,\n          left: evt.pageX\n        });\n      }\n    },\n    []\n  );\n\n  return {\n    anchorEl,\n    anchorPosition,\n    setAnchorEl: setAnchorElCallback,\n    setAnchorPosition: setAnchorPositionCallback,\n    onClose\n  };\n}\n"
  },
  {
    "path": "src/components/Mui/Tooltip.tsx",
    "content": "import React from 'react';\nimport { makeStyles, Theme } from '@material-ui/core/styles';\nimport MuiTooltip, {\n  TooltipProps as MuiTooltipProps\n} from '@material-ui/core/Tooltip';\n\nexport type TooltipProps = Omit<MuiTooltipProps, 'ref'>;\n\nconst fontSize = 14;\nconst PopperProps: TooltipProps['PopperProps'] = { disablePortal: true };\n\nconst useStyles = makeStyles((theme: Theme) => ({\n  tooltip: {\n    fontSize\n  }\n}));\n\nconst useErrorStyles = makeStyles((theme: Theme) => ({\n  arrow: {\n    color: theme.palette.error.main\n  },\n  tooltip: {\n    fontSize,\n    color: theme.palette.error.contrastText,\n    backgroundColor: theme.palette.error.main\n  }\n}));\n\nexport function Tooltip(props: TooltipProps) {\n  const classes = useStyles();\n  return (\n    <MuiTooltip {...props} classes={classes} PopperProps={PopperProps} arrow />\n  );\n}\n\nexport function ErrorTooltip(props: TooltipProps) {\n  const classes = useErrorStyles();\n  return (\n    <MuiTooltip {...props} classes={classes} PopperProps={PopperProps} arrow />\n  );\n}\n"
  },
  {
    "path": "src/components/Mui/index.ts",
    "content": "export * from './DeleteIcon';\nexport * from './Dialog';\nexport * from './Dropdown';\nexport * from './EditIcon';\nexport * from './IconButton';\nexport * from './Input';\nexport * from './Menu';\nexport * from './Tooltip';\n"
  },
  {
    "path": "src/components/Preferences/AccentColor.tsx",
    "content": "import React from 'react';\nimport { FullScreenDialog } from '../Mui/Dialog/FullScreenDialog';\nimport { Control } from '../../utils/form';\n\nconst accentColors: AccentColor[] = [\n  'blue',\n  'purple',\n  'red',\n  'amber',\n  'green',\n  'grey'\n];\n\nexport function AccentColor({ onChange }: Control) {\n  return (\n    <FullScreenDialog.Row>\n      <div className=\"preferences-label\">Accent color</div>\n      <div className=\"preferences-accent-color-selector\">\n        {accentColors.map((color, index) => (\n          <div\n            key={index}\n            className={color}\n            onClick={() => onChange && onChange(color)}\n          />\n        ))}\n      </div>\n    </FullScreenDialog.Row>\n  );\n}\n"
  },
  {
    "path": "src/components/Preferences/Preferences.scss",
    "content": ".preferences {\n  color: var(--text-color);\n\n  input[type='number']::-webkit-inner-spin-button,\n  input[type='number']::-webkit-outer-spin-button {\n    -webkit-appearance: none;\n    margin: 0;\n  }\n\n  .preferences-label {\n    @include flex(center);\n    @include typeface('Nunito Sans', 600);\n    flex: 0 0 auto;\n    padding-right: 10px;\n\n    svg {\n      margin-left: 3px;\n    }\n  }\n\n  .preferences-theme-selector {\n    @include flex(center);\n  }\n\n  .preferences-theme {\n    text-align: center;\n    font-size: 12px;\n    line-height: 1.5em;\n\n    > div {\n      @include dimen(70px, 45px);\n      @include relative();\n\n      border-radius: 5px;\n      border: 2px solid transparent;\n      cursor: pointer;\n      overflow: hidden;\n\n      &:after {\n        $size: 7px;\n        @include absolute(6px, null, 6px);\n        @include sq-dimen($size);\n\n        content: '';\n        background: #ff5f57;\n        border-radius: 50%;\n        box-shadow: $size * 1.5 0 0 #ffbd2e, $size * 3 0 0 #28ca41;\n      }\n\n      &:before {\n        @include absolute(0, null, 0);\n        @include sq-dimen(100%);\n        border-radius: 3px;\n        content: '';\n      }\n    }\n\n    &.light {\n      &:after {\n        content: 'Light';\n      }\n\n      > div {\n        [data-theme^='light'] & {\n          border-color: var(--accent-color);\n        }\n\n        &:before {\n          background-color: #eee;\n        }\n      }\n    }\n\n    &.dark {\n      margin-left: 20px;\n\n      &:after {\n        content: 'Dark';\n      }\n\n      > div {\n        [data-theme^='dark'] & {\n          border-color: var(--accent-color);\n        }\n\n        &:before {\n          background-color: #232323;\n        }\n      }\n    }\n  }\n\n  .preferences-accent-color-selector {\n    @include flex(center);\n\n    > div {\n      @include sq-dimen(20px);\n      @include relative();\n\n      border-radius: 50%;\n      cursor: pointer;\n      overflow: hidden;\n\n      &:nth-child(n + 2) {\n        margin-left: 10px;\n      }\n    }\n\n    @each $name, $colors in $accent-colors {\n      .#{'' + $name} {\n        background-color: map-get($colors, primary);\n        box-shadow: 2px 2px 10px -3px map-get($colors, dark);\n\n        &:after {\n          @include absolute();\n          @include sq-dimen(100%);\n          content: '';\n        }\n\n        [data-accent-color^='#{\"\" + $name}'] & {\n          &:before {\n            @include absolute(0px, 0, 0px, 0);\n            @include sq-dimen(8px);\n\n            content: '';\n            border-radius: 50%;\n            background-color: #fff;\n            margin: auto;\n          }\n        }\n      }\n    }\n  }\n\n  .preferences-hours,\n  .preferences-max-tasks-selector {\n    .mui-input-base {\n      @include padding-y(0);\n      @include padding-x(10px);\n      margin-right: 5px;\n      max-width: 4em;\n      min-height: 30px;\n\n      input {\n        text-align: center;\n      }\n    }\n  }\n\n  .preferences-max-tasks-selector {\n    .mui-input-base {\n      max-width: 6em;\n    }\n  }\n\n  .preferences-hours {\n    .mui-input-base {\n      max-width: 4em;\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/Preferences/Preferences.tsx",
    "content": "import React, { useState } from 'react';\nimport { useSelector } from 'react-redux';\nimport {\n  Input,\n  ErrorTooltip,\n  FullScreenDialog,\n  FullScreenDialogProps\n} from '../Mui';\nimport { Switch } from '../Switch';\nimport { ThemeSelector } from './ThemeSelector';\nimport { AccentColor } from './AccentColor';\nimport { TitleBarSelector } from './TitleBarSelector';\nimport { Storage } from './Storage';\nimport { preferencesSelector, usePreferenceActions } from '../../store';\nimport { createForm, validators } from '../../utils/form';\n\nconst { Form, FormItem, useForm } = createForm<Schema$Preferences>();\n\nconst normalizeNumber = (value: string) => {\n  const num = Number(value);\n  return value === '' ||\n    value === '-0' ||\n    isNaN(num) ||\n    /^0\\d+/.test(value) ||\n    /\\.(0+)?$/.test(value)\n    ? String(value)\n    : num;\n};\n\nexport function Preferences(props: FullScreenDialogProps) {\n  const preferences = useSelector(preferencesSelector);\n  const [form] = useForm();\n  const [errors, setErrors] = useState<string[]>([]);\n  const { updatePreferences } = usePreferenceActions();\n\n  return (\n    <FullScreenDialog {...props} className=\"preferences\" title=\"Preferences\">\n      <Form\n        form={form}\n        initialValues={preferences}\n        onValuesChange={changes => {\n          setTimeout(() => {\n            form\n              .validateFields()\n              .then(() => updatePreferences(changes))\n              .catch(() => {\n                setErrors(\n                  form\n                    .getFieldsError(['maxTasks', ['sync', 'inactiveHours']])\n                    .map(payload => payload.errors[0])\n                );\n              });\n          }, 0);\n        }}\n      >\n        <FullScreenDialog.Section>\n          <FullScreenDialog.Title children=\"Appearances\" />\n\n          <FormItem name=\"theme\" noStyle>\n            <ThemeSelector />\n          </FormItem>\n\n          <FormItem name=\"accentColor\" noStyle>\n            <AccentColor />\n          </FormItem>\n\n          {window.platform === 'darwin' ? null : (\n            <FormItem name=\"titleBar\" noStyle>\n              <TitleBarSelector />\n            </FormItem>\n          )}\n        </FullScreenDialog.Section>\n\n        <FullScreenDialog.Section>\n          <FullScreenDialog.Title children=\"Tasks\" />\n\n          <FullScreenDialog.Row>\n            <div className=\"preferences-label\">Maximum Tasks</div>\n            <div className=\"preferences-max-tasks-selector\">\n              <ErrorTooltip\n                enterDelay={0}\n                placement=\"top\"\n                title={errors[0] || ''}\n                open={!!errors[0]}\n              >\n                <span>\n                  <FormItem\n                    name=\"maxTasks\"\n                    normalize={normalizeNumber}\n                    validators={[\n                      validators.required('Cannot be empty'),\n                      validators.number,\n                      validators.integer('Plase input integer only'),\n                      validators.min(0, 'Cannot less then 1')\n                    ]}\n                    noStyle\n                  >\n                    <Input className=\"filled\" />\n                  </FormItem>\n                </span>\n              </ErrorTooltip>\n            </div>\n          </FullScreenDialog.Row>\n\n          <FullScreenDialog.Row>\n            <div className=\"preferences-label\">Enable synchronization</div>\n            <div className=\"preferences-switch\">\n              <FormItem\n                name={['sync', 'enabled']}\n                valuePropName=\"checked\"\n                noStyle\n              >\n                <Switch />\n              </FormItem>\n            </div>\n          </FullScreenDialog.Row>\n\n          <FormItem deps={[['sync', 'enabled']]} noStyle>\n            {({ sync }) => {\n              if (sync.enabled) {\n                return (\n                  <>\n                    <FullScreenDialog.Row>\n                      <div className=\"preferences-label\">\n                        Sync on reconnection\n                      </div>\n                      <div className=\"preferences-hours\">\n                        <FormItem\n                          name={['sync', 'reconnection']}\n                          valuePropName=\"checked\"\n                          noStyle\n                        >\n                          <Switch />\n                        </FormItem>\n                      </div>\n                    </FullScreenDialog.Row>\n\n                    <FullScreenDialog.Row>\n                      <div className=\"preferences-label\">\n                        Sync after inactive\n                      </div>\n                      <div className=\"preferences-hours\">\n                        <ErrorTooltip\n                          enterDelay={0}\n                          placement=\"top\"\n                          title={errors[1] || ''}\n                          open={!!errors[1]}\n                        >\n                          <span>\n                            <FormItem\n                              name={['sync', 'inactiveHours']}\n                              normalize={normalizeNumber}\n                              validators={[\n                                validators.required('Cannot be empty'),\n                                validators.number,\n                                validators.integer('Plase input integer only'),\n                                validators.min(1, 'Cannot less then 1')\n                              ]}\n                              noStyle\n                            >\n                              <Input className=\"filled\" />\n                            </FormItem>\n                          </span>\n                        </ErrorTooltip>\n                        Hours\n                      </div>\n                    </FullScreenDialog.Row>\n                  </>\n                );\n              }\n              return <div />;\n            }}\n          </FormItem>\n        </FullScreenDialog.Section>\n\n        <Storage />\n      </Form>\n    </FullScreenDialog>\n  );\n}\n"
  },
  {
    "path": "src/components/Preferences/Storage.tsx",
    "content": "import React from 'react';\nimport { Input, FullScreenDialog } from '../Mui';\n\nexport function Storage() {\n  return (\n    <FullScreenDialog.Section>\n      <FullScreenDialog.Title children=\"Storage ( Read-Only )\" />\n      <FullScreenDialog.Row>\n        <div className=\"preferences-label\">Path</div>\n        <Input value={window.STORAGE_DIRECTORY} readOnly className=\"filled\" />\n      </FullScreenDialog.Row>\n    </FullScreenDialog.Section>\n  );\n}\n"
  },
  {
    "path": "src/components/Preferences/ThemeSelector.tsx",
    "content": "import React from 'react';\nimport { FullScreenDialog } from '../Mui/Dialog/FullScreenDialog';\nimport { Control } from '../../utils/form';\n\nexport function ThemeSelector({ value, onChange }: Control<Theme>) {\n  const handleChange = onChange || (() => {});\n  return (\n    <FullScreenDialog.Row>\n      <div className=\"preferences-label\">Theme</div>\n      <div className=\"preferences-theme-selector\">\n        <div className=\"preferences-theme light\">\n          <div onClick={() => handleChange('light')} />\n        </div>\n        <div className=\"preferences-theme dark\">\n          <div onClick={() => handleChange('dark')} />\n        </div>\n      </div>\n    </FullScreenDialog.Row>\n  );\n}\n"
  },
  {
    "path": "src/components/Preferences/TitleBarSelector.tsx",
    "content": "import React, { useState } from 'react';\nimport { FullScreenDialog, ConfirmDialog } from '../Mui';\nimport { Dropdown, MenuItem, useMuiMenu } from '../Mui';\nimport { Control } from '../../utils/form';\n\nconst exlucded: Array<Window['platform']> = ['darwin', 'win32'];\nconst shouldRelaunch = !exlucded.includes(window.platform);\n\nexport function TitleBarSelector({ value, onChange }: Control<TitleBar>) {\n  const { anchorEl, setAnchorEl, onClose } = useMuiMenu();\n  const [change, setChange] = useState<TitleBar | null>(null);\n  const [shouldClose, setShoudClose] = useState(false);\n  const handleChange = onChange || (() => {});\n  const hadnleSelect = shouldRelaunch ? setChange : handleChange;\n\n  return (\n    <FullScreenDialog.Row>\n      <div className=\"preferences-label\">Title Bar</div>\n      <div className=\"preferences-title-bar-selector\">\n        <Dropdown\n          open={!!anchorEl}\n          anchorEl={anchorEl}\n          onClick={setAnchorEl}\n          onClose={onClose}\n          label={value?.replace(/^./, match => match.toUpperCase())}\n        >\n          <MenuItem\n            text=\"Native\"\n            selected={value === 'native'}\n            onClick={() => hadnleSelect('native')}\n            onClose={onClose}\n          />\n          <MenuItem\n            text=\"Frameless\"\n            selected={value === 'frameless'}\n            onClick={() => hadnleSelect('frameless')}\n            onClose={onClose}\n          />\n        </Dropdown>\n        {shouldRelaunch && (\n          <ConfirmDialog\n            title=\"Notice\"\n            open={!!change}\n            onClose={() => setChange(null)}\n            onConfirm={() => {\n              if (change && change !== value) {\n                handleChange(change);\n                setShoudClose(true);\n              }\n            }}\n            onExited={() => shouldClose && window.relaunch()}\n          >\n            This will relaunch your application and the changes to take effect\n            after relaunch\n          </ConfirmDialog>\n        )}\n      </div>\n    </FullScreenDialog.Row>\n  );\n}\n"
  },
  {
    "path": "src/components/Preferences/index.ts",
    "content": "import './Preferences.scss';\n\nexport * from './Preferences';\nexport { Preferences as default } from './Preferences';\n"
  },
  {
    "path": "src/components/PrivateRoute.tsx",
    "content": "import React from 'react';\nimport { Route, Redirect, RouteProps } from 'react-router-dom';\nimport { useSelector } from 'react-redux';\nimport { RootState } from '../store';\nimport { PATHS } from '../constants';\n\nexport function PrivateRoute(props: RouteProps) {\n  const loggedIn = useSelector((state: RootState) => state.auth.loggedIn);\n  return loggedIn ? <Route {...props} /> : <Redirect to={PATHS.AUTH} />;\n}\n"
  },
  {
    "path": "src/components/Switch/Switch.scss",
    "content": "$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 dimen($defaultWidth);\n  @include relative();\n\n  cursor: pointer;\n  user-select: none;\n  -webkit-tap-highlight-color: rgba(255, 255, 255, 0);\n\n  .switch-bar {\n    @include animate(background-color);\n    @include dimen(100%, 0);\n    @include relative();\n    background-color: var(--border-color);\n    border-radius: 50px;\n    padding-bottom: 50%;\n  }\n\n  .switch-icon {\n    @include animate(transform);\n    @include absolute(-0.4px);\n    @include dimen(50%, 100%);\n    padding: round(percentage(2.5 / strip-unit($defaultWidth)));\n    transform: translateX(0);\n\n    &:before {\n      @include sq-dimen(100%);\n      @include shadow(1px);\n      content: '';\n      background-color: #fff;\n      border-radius: 50%;\n      display: block;\n    }\n  }\n\n  &.checked {\n    .switch-bar {\n      background-color: var(--accent-color);\n    }\n    .switch-icon {\n      transform: translateX(100%);\n      &:before {\n        @include shadow(-1px);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/Switch/Switch.tsx",
    "content": "import React, { useMemo, useCallback, CSSProperties } from 'react';\n\ninterface Props {\n  checked?: boolean;\n  onChange?(checked: boolean): void;\n  width?: number;\n}\n\nexport const Switch = React.memo<Props>(\n  ({ checked = false, width, onChange }) => {\n    const style = useMemo<CSSProperties>(() => ({ width }), [width]);\n\n    const onClick = useCallback(() => {\n      onChange && onChange(!checked);\n    }, [onChange, checked]);\n\n    return (\n      <div\n        className={['switch', checked && 'checked'].filter(Boolean).join(' ')}\n        onClick={onClick}\n        style={style}\n      >\n        <div className=\"switch-bar\" />\n        <div className=\"switch-icon\" />\n      </div>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/Switch/index.ts",
    "content": "import './Switch.scss';\n\nexport * from './Switch';\nexport { Switch as default } from './Switch';\n"
  },
  {
    "path": "src/constants/index.ts",
    "content": "export { default as PATHS } from './paths.json';\n"
  },
  {
    "path": "src/constants/paths.json",
    "content": "{\n  \"AUTH\": \"/auth\",\n  \"TASKLIST\": \"/tasklist/:taskListId?\"\n}\n"
  },
  {
    "path": "src/date.d.ts",
    "content": "type DayCn = '日' | '一' | '二' | '三' | '四' | '五' | '六';\ntype MonthFullName =\n  | 'January'\n  | 'February'\n  | 'March'\n  | 'April'\n  | 'May'\n  | 'June'\n  | 'July'\n  | 'August'\n  | 'September'\n  | 'October'\n  | 'November'\n  | 'December';\n\ntype MonthAbbr =\n  | 'Jan'\n  | 'Feb'\n  | 'Mar'\n  | 'Apr'\n  | 'May'\n  | 'Jun'\n  | 'Jul'\n  | 'Aug'\n  | 'Sep'\n  | 'Oct'\n  | 'Nov'\n  | 'Dec';\n\ntype DayFullName =\n  | 'Sunday'\n  | 'Monday'\n  | 'Tuesday'\n  | 'Wednesday'\n  | 'Thursday'\n  | 'Friday'\n  | 'Saturday';\n\ntype DayAbbr = 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thur' | 'Fri' | 'Sat';\n\ntype DaySuffix = 'th' | 'st' | 'nd' | 'rd';\n\ninterface Date {\n  getDayCn(): DayCn;\n\n  addDays: (n: number) => Date;\n\n  addMonths: (n: number) => Date;\n\n  getMonthName(): MonthFullName;\n\n  getMonthAbbr(): MonthAbbr;\n\n  getDayFull(): DayFullName;\n\n  getDayAbbr(): DayAbbr;\n\n  getDayOfYear(): number;\n\n  getDaySuffix(): DaySuffix;\n\n  getWeekOfYear(): number;\n\n  isLeapYear(): boolean;\n\n  getMonthDayCount(): number;\n\n  compare(\n    d: Date\n  ): {\n    sameYear: boolean;\n    sameDate: boolean;\n    sameMonth: boolean;\n    lastMonth: boolean;\n    nextMonth: boolean;\n  };\n\n  isToday(): boolean;\n\n  dayDiff(d?: Date): number;\n\n  toISODateString(): string;\n\n  format(dateFormat: string): string;\n}\n"
  },
  {
    "path": "src/hooks/crud-reducer/bindDispatch.ts",
    "content": "import { Dispatch } from 'react';\nimport { ActionCreators } from './crudAction';\n\nexport type Dispatched<A extends ActionCreators> = {\n  [X in keyof A]: (...args: Parameters<A[X]>) => void;\n};\n\nexport function bindDispatch<A extends ActionCreators>(\n  creators: A,\n  dispatch: Dispatch<any>\n) {\n  const handler = {} as Dispatched<A>;\n  for (const key in creators) {\n    const creator = creators[key];\n    handler[key] = (...args: Parameters<typeof creator>) => {\n      dispatch(creator(...args));\n    };\n  }\n\n  return handler;\n}\n"
  },
  {
    "path": "src/hooks/crud-reducer/crudAction.ts",
    "content": "/* eslint-disable @typescript-eslint/ban-types */\n\n// https://medium.com/dailyjs/typescript-create-a-condition-based-subset-types-9d902cea5b8c\nexport type FilterFlags<Base, Condition> = {\n  [Key in keyof Base]: Base[Key] extends Condition ? Key : never;\n};\n\nexport type AllowedNames<Base, Condition> = FilterFlags<\n  Base,\n  Condition\n>[keyof Base];\n\nexport type Key<I> = AllowedNames<I, string>;\n\nexport interface AnyAction {\n  type: string;\n  [extraProps: string]: any;\n}\n\nexport interface ActionCreators {\n  [k: string]: (...args: any[]) => AnyAction;\n}\n\nexport type GetCreatorsAction<\n  T extends Record<string, (...args: any[]) => any>\n> = ReturnType<T[keyof T]>;\n\nexport type CRUDActionType =\n  | 'LIST'\n  | 'CREATE'\n  | 'UPDATE'\n  | 'DELETE'\n  | 'PAGINATE'\n  | 'PARAMS'\n  | 'RESET';\n\nexport type CRUDActionTypes<Type extends string = any> = {\n  [K in CRUDActionType]?: Type;\n};\n\nexport type CustomActionTypes<\n  M extends Partial<CRUDActionTypes> = CRUDActionTypes\n> = M & Omit<typeof DefaultCRUDActionTypes, keyof M>;\n\nexport type UpdatePayload<I, K extends Key<I>> = Partial<I> &\n  { [T in K]: string };\n\nexport type PaginatePayload<I> =\n  | I[]\n  | {\n      data: I[];\n      total: number;\n      pageNo: number;\n      pageSize?: number;\n    };\n\nexport type List<Type extends string, I> = {\n  type: Type;\n  payload: I[];\n};\n\nexport type Create<Type extends string, I> = {\n  type: Type;\n  payload: I;\n};\n\nexport interface Update<Type extends string, I, K extends Key<I>> {\n  type: Type;\n  payload: UpdatePayload<I, K>;\n}\n\nexport interface Delete<Type extends string, I, K extends Key<I>> {\n  type: Type;\n  payload: { [T in K]: string };\n}\n\nexport interface Paginate<Type extends string, I> {\n  type: Type;\n  payload: PaginatePayload<I>;\n}\n\nexport interface Params<Type extends string> {\n  type: Type;\n  payload: Record<string, any>;\n}\n\nexport interface Reset<Type extends string> {\n  type: Type;\n}\n\nexport type CRUDActionCreators<\n  I,\n  K extends Key<I>,\n  M extends CRUDActionTypes = CRUDActionTypes\n> = {\n  list: (payload: I[]) => List<M['LIST'], I>;\n  create: (payload: I) => Create<M['CREATE'], I>;\n  update: (\n    payload: Update<string, I, K>['payload']\n  ) => Update<M['UPDATE'], I, K>;\n  delete: (payload: { [T in K]: string }) => Delete<M['DELETE'], I, K>;\n  paginate: (payload: PaginatePayload<I>) => Paginate<M['PAGINATE'], I>;\n  params: (payload: Record<string, any>) => Params<M['PARAMS']>;\n  reset: () => Reset<M['RESET']>;\n};\n\nexport type CRUDActions<\n  I,\n  K extends Key<I>,\n  M extends CRUDActionTypes = CRUDActionTypes\n> = GetCreatorsAction<CRUDActionCreators<I, K, M>>;\n\nexport type ExtractAction<\n  CustomActionTypes extends AnyAction,\n  T2 extends CustomActionTypes['type']\n> = CustomActionTypes extends { type: T2 } ? CustomActionTypes : never;\n\nexport function isAction<\n  I,\n  K extends Key<I>,\n  M extends CRUDActionTypes = CRUDActionTypes,\n  BaseType extends keyof M = keyof M\n>(\n  actionTypes: M,\n  action: CRUDActions<I, K, M>,\n  type: BaseType\n): action is ExtractAction<CRUDActions<I, K, M>, M[BaseType]> {\n  return action.type === actionTypes[type];\n}\n\nexport const DefaultCRUDActionTypes = {\n  LIST: 'LIST',\n  CREATE: 'CREATE',\n  UPDATE: 'UPDATE',\n  DELETE: 'DELETE',\n  PAGINATE: 'PAGINATE',\n  PARAMS: 'PARAMS',\n  RESET: 'RESET'\n} as const;\n\nexport function getCRUDActionsCreator<I, K extends Key<I>>() {\n  // prettier-ignore\n  function create(): [CRUDActionCreators<I, K, typeof DefaultCRUDActionTypes>, typeof DefaultCRUDActionTypes]\n  // prettier-ignore\n  function create<M extends CRUDActionTypes = CRUDActionTypes>(actionTypes: M): [CRUDActionCreators<I, K, CustomActionTypes<M>>, CustomActionTypes<M>]\n  // prettier-ignore\n  function create<M extends CRUDActionTypes = CRUDActionTypes>(actionTypes = DefaultCRUDActionTypes as M) {\n    actionTypes = { ...DefaultCRUDActionTypes, ...actionTypes };\n    const creators: CRUDActionCreators<I, K, CustomActionTypes<M>> = {\n      list: payload => ({ type: actionTypes['LIST'], payload }),\n      create: payload => ({ type: actionTypes['CREATE'], payload }),\n      update: payload => ({ type: actionTypes['UPDATE'], payload }),\n      delete: payload => ({ type: actionTypes['DELETE'], payload }),\n      paginate: payload => ({ type: actionTypes['PAGINATE'], payload }),\n      params: payload => ({ type: actionTypes['PARAMS'], payload }),\n      reset: () => ({ type: actionTypes['RESET'] })\n    };\n    return [creators, actionTypes] as const;\n  }\n  return create;\n}\n"
  },
  {
    "path": "src/hooks/crud-reducer/crudReducer.ts",
    "content": "/* eslint-disable @typescript-eslint/ban-types */\n\nimport {\n  Key,\n  CRUDActions,\n  CRUDActionTypes,\n  PaginatePayload,\n  DefaultCRUDActionTypes,\n  isAction,\n  List\n} from './crudAction';\n\nexport interface CRUDState<I, Prefill extends boolean = true> {\n  ids: string[];\n  byIds: Record<string, I>;\n  list: Prefill extends true ? Array<I | null> : I[];\n  pageNo: number;\n  pageSize: number;\n  total: number;\n  params: any;\n}\n\nexport type CRUDReducer<\n  I,\n  K extends Key<I>,\n  Prefill extends boolean = true,\n  M extends CRUDActionTypes = CRUDActionTypes\n> = (\n  state: CRUDState<I, Prefill>,\n  action: CRUDActions<I, K, M>\n) => CRUDState<I, Prefill>;\n\nexport interface CreateCRUDReducerOptions<\n  M extends CRUDActionTypes = CRUDActionTypes\n> {\n  prefill?: boolean;\n  actionTypes?: M;\n  keyGenerator?: (index: number) => string;\n}\n\nexport const defaultKeyGenerator = (() => {\n  let count = 0;\n  return function () {\n    count++;\n    return `mock-${count}`;\n  };\n})();\n\nexport function parsePaginatePayload<I>(payload: PaginatePayload<I>) {\n  return Array.isArray(payload)\n    ? {\n        total: payload.length,\n        data: payload,\n        pageNo: 1\n      }\n    : payload;\n}\n\nexport function createCRUDReducer<\n  I,\n  K extends Key<I>,\n  M extends CRUDActionTypes = CRUDActionTypes\n>(\n  key: K,\n  options: CreateCRUDReducerOptions<M> & { prefill: false }\n): [CRUDState<I, false>, CRUDReducer<I, K, false>];\n\nexport function createCRUDReducer<\n  I,\n  K extends Key<I>,\n  M extends CRUDActionTypes = CRUDActionTypes\n>(\n  key: K,\n  options?: CreateCRUDReducerOptions<M>\n): [CRUDState<I, true>, CRUDReducer<I, K, true>];\n\nexport function createCRUDReducer<\n  I,\n  K extends Key<I>,\n  M extends CRUDActionTypes = CRUDActionTypes\n>(\n  key: K,\n  options?: CreateCRUDReducerOptions<M>\n): [CRUDState<I, boolean>, CRUDReducer<I, K, boolean, M>] {\n  const defaultState: CRUDState<I, boolean> = {\n    byIds: {},\n    ids: [],\n    list: [],\n    pageNo: 1,\n    pageSize: 10,\n    total: 0,\n    params: {}\n  };\n\n  const {\n    prefill = true,\n    keyGenerator = defaultKeyGenerator,\n    actionTypes = DefaultCRUDActionTypes as M\n  } = options || {};\n\n  const reducer: CRUDReducer<I, K, boolean, M> = (\n    state = defaultState,\n    action\n  ) => {\n    if (isAction(actionTypes, action, 'PAGINATE')) {\n      return (() => {\n        const {\n          data,\n          pageNo,\n          total,\n          pageSize = state.pageSize\n        } = parsePaginatePayload(action.payload);\n\n        if (prefill === false) {\n          return reducer(state, { type: actionTypes['LIST'], payload: data });\n        }\n\n        const start = (pageNo - 1) * pageSize;\n\n        const insert = <T1, T2>(arr: T1[], ids: T2[]) => {\n          return [\n            ...arr.slice(0, start),\n            ...ids,\n            ...arr.slice(start + pageSize)\n          ];\n        };\n\n        const { list, ids, byIds } = reducer(defaultState, {\n          type: actionTypes['LIST'],\n          payload: data\n        });\n\n        const length = total - state.ids.length;\n\n        return {\n          ...state,\n          total,\n          pageNo,\n          pageSize,\n          byIds: {\n            ...state.byIds,\n            ...byIds\n          },\n          ids: insert(\n            [\n              ...state.ids,\n              ...Array.from({ length }, (_, index) => keyGenerator(index))\n            ],\n            ids\n          ),\n          list: insert(\n            [...state.list, ...Array.from({ length }, () => null)],\n            list\n          )\n        };\n      })();\n    }\n\n    if (isAction(actionTypes, action, 'LIST')) {\n      return (action as List<'', I>).payload.reduce(\n        (state, payload) =>\n          reducer(state, { type: actionTypes['CREATE'], payload }),\n        defaultState\n      );\n    }\n\n    if (isAction(actionTypes, action, 'CREATE')) {\n      const id: string = action.payload[key] as any;\n      return {\n        ...state,\n        byIds: { ...state.byIds, [id]: action.payload },\n        list: [...state.list, action.payload],\n        ids: [...state.ids, id]\n      };\n    }\n\n    if (isAction(actionTypes, action, 'UPDATE')) {\n      const id = action.payload[key] as string;\n      const updated = { ...state.byIds[id], ...action.payload };\n      const index = state.ids.indexOf(id);\n      return index === -1\n        ? state\n        : {\n            ...state,\n            byIds: { ...state.byIds, [id]: updated },\n            list: [\n              ...state.list.slice(0, index),\n              updated,\n              ...state.list.slice(index + 1)\n            ]\n          };\n    }\n\n    if (isAction(actionTypes, action, 'DELETE')) {\n      const id = action.payload[key];\n      const index = state.ids.indexOf(id);\n      const { [id]: _deleted, ...byIds } = state.byIds;\n      return {\n        ...state,\n        byIds,\n        ids: removeFromArray(state.ids, index),\n        list: removeFromArray(state.list, index)\n      };\n    }\n\n    if (isAction(actionTypes, action, 'PARAMS')) {\n      const { pageNo, pageSize, ...params } = action.payload;\n      const toNum = (value: unknown, num: number) =>\n        typeof value === 'undefined' || isNaN(Number(value))\n          ? num\n          : Number(value);\n\n      return {\n        ...state,\n        pageNo: toNum(pageNo, state.pageNo),\n        pageSize: toNum(pageSize, state.pageSize),\n        params\n      };\n    }\n\n    if (isAction(actionTypes, action, 'RESET')) {\n      return defaultState;\n    }\n\n    return state;\n  };\n\n  return [defaultState, reducer];\n}\n\nexport function removeFromArray<T>(arr: T[], index: number) {\n  return index < 0 ? arr : [...arr.slice(0, index), ...arr.slice(index + 1)];\n}\n"
  },
  {
    "path": "src/hooks/crud-reducer/crudSelector.ts",
    "content": "import { CRUDState } from './crudReducer';\n\nexport interface PaginateState<S extends CRUDState<unknown, any>> {\n  ids: S['ids'];\n  list: S['list'];\n  pageNo: number;\n  pageSize: number;\n  total: number;\n  params: any;\n  hasData: boolean;\n}\n\nexport function paginateSelector<S extends CRUDState<unknown, any>>({\n  list,\n  ids,\n  pageNo,\n  pageSize,\n  params,\n  total\n}: S): PaginateState<S> {\n  const start = (pageNo - 1) * pageSize;\n  const _list = list.slice(start, start + pageSize);\n  const _ids = ids.slice(start, start + pageSize);\n\n  let hasData = !!_list.length;\n  for (const item of _list) {\n    if (item === null) {\n      hasData = false;\n      break;\n    }\n  }\n\n  return {\n    list: _list,\n    ids: _ids,\n    pageNo,\n    pageSize,\n    total,\n    params,\n    hasData\n  };\n}\n"
  },
  {
    "path": "src/hooks/crud-reducer/index.ts",
    "content": "export * from './bindDispatch';\nexport * from './crudAction';\nexport * from './crudReducer';\nexport * from './crudSelector';\nexport * from './useActions';\nexport * from './useCRUDReducer';\n"
  },
  {
    "path": "src/hooks/crud-reducer/useActions.ts",
    "content": "import { useState } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { ActionCreators } from './crudAction';\nimport { Dispatched, bindDispatch } from './bindDispatch';\n\nexport function useActions<A extends ActionCreators>(\n  creators: A\n): Dispatched<A> {\n  const dispatch = useDispatch();\n  const [actions] = useState(bindDispatch(creators, dispatch));\n  return actions;\n}\n"
  },
  {
    "path": "src/hooks/crud-reducer/useCRUDReducer.ts",
    "content": "/* eslint-disable @typescript-eslint/ban-types */\n\nimport { useReducer, useState } from 'react';\nimport {\n  CRUDState,\n  createCRUDReducer,\n  CreateCRUDReducerOptions\n} from './crudReducer';\nimport { Key, getCRUDActionsCreator, CRUDActionCreators } from './crudAction';\nimport { bindDispatch, Dispatched } from './bindDispatch';\n\nexport type UseCRUDReducer<\n  I,\n  K extends Key<I>,\n  Prefill extends boolean = true\n> = () => [CRUDState<I, Prefill>, Dispatched<CRUDActionCreators<I, K>>];\n\nexport function createUseCRUDReducer<I, K extends Key<I>>(\n  key: K,\n  options: CreateCRUDReducerOptions & { prefill: false }\n): UseCRUDReducer<I, K, false>;\n\nexport function createUseCRUDReducer<I, K extends Key<I>>(\n  key: K,\n  options?: CreateCRUDReducerOptions\n): UseCRUDReducer<I, K, true>;\n\nexport function createUseCRUDReducer<I, K extends Key<I>>(\n  key: K,\n  options?: CreateCRUDReducerOptions\n): UseCRUDReducer<I, K, boolean> {\n  const [intialState, reducer] = createCRUDReducer<I, K>(key, options);\n  return function useCRUDReducer() {\n    const [state, dispatch] = useReducer(reducer, intialState);\n    const [actions] = useState(() => {\n      const [actions] = getCRUDActionsCreator<I, K>()();\n      return { dispatch, ...bindDispatch(actions, dispatch) };\n    });\n    return [state, actions];\n  };\n}\n"
  },
  {
    "path": "src/hooks/useActions.ts",
    "content": "import { useMemo, useRef, Dispatch as ReactDispatch } from 'react';\nimport { AnyAction, Dispatch } from 'redux';\nimport { useDispatch } from 'react-redux';\n\ninterface ActionCreators {\n  [k: string]: (...args: any[]) => AnyAction;\n}\n\ntype Handler<A extends ActionCreators> = {\n  [X in keyof A]: (...args: Parameters<A[X]>) => void;\n};\n\nexport function withDispatch<A extends ActionCreators>(\n  creators: A,\n  dispatch: Dispatch | ReactDispatch<any>\n) {\n  const handler = {} as Handler<A>;\n  for (const key in creators) {\n    const creator = creators[key];\n    handler[key] = (...args: Parameters<typeof creator>) => {\n      dispatch(creator(...args));\n    };\n  }\n\n  return handler;\n}\n\nexport function useActions<A extends ActionCreators>(creators: A): Handler<A> {\n  const dispatch = useDispatch();\n  const creatorsRef = useRef(creators);\n\n  return useMemo(() => withDispatch(creatorsRef.current, dispatch), [dispatch]);\n}\n"
  },
  {
    "path": "src/hooks/useBoolean.ts",
    "content": "import { useState, useMemo } from 'react';\n\nexport function useBoolean(initialState = false) {\n  const [flag, setFlag] = useState(initialState);\n  const actions = useMemo(\n    () => [\n      () => setFlag(true),\n      () => setFlag(false),\n      () => setFlag(flag => !flag)\n    ],\n    []\n  );\n  return [flag, ...actions] as const;\n}\n"
  },
  {
    "path": "src/hooks/useMouseTrap.ts",
    "content": "import { useEffect } from 'react';\nimport mousetrap, { MousetrapStatic } from 'mousetrap';\n\ntype Params = Parameters<MousetrapStatic['bind']>;\n\nexport function useMouseTrap(\n  key: Params[0],\n  method: Params[1],\n  preventDefault = true\n) {\n  useEffect(() => {\n    if (key !== '') {\n      const instance = mousetrap(document.body);\n      const handler = (...args: Parameters<Params[1]>) => {\n        method(...args);\n        return preventDefault ? false : true;\n      };\n      instance.bind(key, handler);\n      return () => {\n        instance.unbind(key);\n      };\n    }\n  }, [key, method, preventDefault]);\n}\n"
  },
  {
    "path": "src/index.scss",
    "content": "*,\n*:before,\n*:after {\n  box-sizing: border-box;\n}\n\nhtml,\nbody,\n#root {\n  min-height: 100vh;\n}\n\nhtml {\n  background-color: var(--main-color);\n\n  &:not([data-platform^='darwin']) {\n    @include scrollbar;\n  }\n}\n\nbody {\n  @include typeface();\n  color: var(--text-secondary-color);\n  font-size: 14px;\n  margin: 0;\n  overflow: hidden;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\nimg {\n  vertical-align: middle;\n}\n\na {\n  text-decoration: none;\n  color: inherit;\n}\n\n#nprogress {\n  .bar {\n    background: var(--accent-color);\n    height: 3px;\n  }\n\n  .peg {\n    box-shadow: 0 0 10px var(--accent-color), 0 0 5px var(--accent-color);\n  }\n\n  .spinner-icon {\n    border-top-color: var(--accent-color);\n    border-left-color: var(--accent-color);\n  }\n}\n"
  },
  {
    "path": "src/index.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport { Provider } from 'react-redux';\nimport { ConnectedRouter } from 'connected-react-router/immutable'; // https://github.com/supasate/connected-react-router/issues/312\nimport { MuiThemeProvider } from '@material-ui/core/styles';\nimport { theme } from './theme';\nimport configureStore, { history } from './store';\nimport App from './App';\nimport * as serviceWorker from './serviceWorker';\n\nimport 'typeface-roboto';\nimport 'typeface-nunito-sans';\nimport './utils/date';\nimport './index.scss';\n\nconst store = configureStore();\n\nfunction render() {\n  return ReactDOM.render(\n    <MuiThemeProvider theme={theme}>\n      <Provider store={store}>\n        <ConnectedRouter history={history}>\n          <App />\n        </ConnectedRouter>\n      </Provider>\n    </MuiThemeProvider>,\n    document.getElementById('root')\n  );\n}\n\nrender();\n\n// If you want your app to work offline and load faster, you can change\n// unregister() to register() below. Note this comes with some pitfalls.\n// Learn more about service workers: https://bit.ly/CRA-PWA\nserviceWorker.unregister();\n\nif (module.hot) {\n  module.hot.accept('./App', render);\n}\n"
  },
  {
    "path": "src/pages/Auth/Auth.scss",
    "content": ".auth {\n  @include flex($flex-direction: column);\n  @include fixed(0, null, 0);\n  @include sq-dimen(100%);\n\n  background-color: var(--main-color);\n  color: var(--text-color);\n  padding: $padding-x;\n  text-align: center;\n  z-index: $app-region-z-index - 1;\n\n  .auth-header {\n    margin-top: 65px;\n  }\n\n  .auth-content {\n    @include dimen(100%);\n    @include flex(stretch, center, $flex-direction: column);\n    @include relative();\n    flex: 1 0 auto;\n  }\n\n  .auth-confirm-button {\n    color: var(--accent-color);\n  }\n\n  .auth-go-back {\n    color: #999;\n  }\n\n  form {\n    @include dimen(100%);\n    .mui-input-base {\n      margin: 5px 0 15px;\n    }\n  }\n\n  .file-upload {\n    @include dimen(100%);\n    @include flex($flex-direction: column);\n    margin-top: 50px;\n    font-weight: bold;\n    flex: 1 0 auto;\n\n    .file-upload-header {\n      a {\n        color: var(--accent-color);\n        text-decoration: underline;\n      }\n    }\n\n    .file-upload-content {\n      @include relative();\n\n      border: 2px dashed;\n      border-radius: 5px;\n      color: var(--text-secondary-color);\n      cursor: pointer;\n      flex: 1 0 auto;\n      margin: 10px 0;\n\n      &.dragover {\n        label {\n          opacity: 0.5;\n        }\n      }\n\n      input[type='file'] {\n        @include absolute(0, null, 0);\n        @include sq-dimen(100%);\n\n        opacity: 0;\n        outline: none;\n      }\n\n      label {\n        @include absolute(0, null, 0);\n        @include flex(center, center);\n        @include sq-dimen(100%);\n\n        line-height: 1.5em;\n        padding: 15px;\n      }\n    }\n\n    .file-upload-footer {\n      @include flex(center);\n\n      color: var(--error-color);\n      min-height: 24px;\n\n      svg {\n        margin-right: 5px;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/pages/Auth/Auth.tsx",
    "content": "import React, { useState } from 'react';\nimport { useRxInput, useRxAsync } from 'use-rx-hooks';\nimport { Button } from '@material-ui/core';\nimport { Input } from '../../components/Mui/Input';\nimport { FileUpload } from './FileUpload';\nimport { generateAuthUrl, getToken } from '../../service';\nimport { ReactComponent as LogoSvg } from '../../assets/logo.svg';\nimport { useAuthActions } from '../../store';\n\nexport function Auth() {\n  const [value, inputProps] = useRxInput();\n  const { authenticated } = useAuthActions();\n  const [{ loading }, { fetch }] = useRxAsync(getToken, {\n    defer: true,\n    onSuccess: authenticated\n  });\n  const [installed, setInstanned] = useState(window.oAuth2Storage.get());\n\n  return (\n    <div className=\"auth\">\n      <div className=\"auth-header\">\n        <LogoSvg className=\"logo\" />\n        <h4>Unoffical Google Tasks Client</h4>\n      </div>\n      <div className=\"auth-content\">\n        {installed ? (\n          <form>\n            Paste the code here:\n            <Input\n              {...inputProps}\n              className=\"filled bottom-border\"\n              disabled={loading}\n              autoFocus\n              fullWidth\n            />\n            <div>\n              <div>\n                <Button disabled={loading} onClick={generateAuthUrl}>\n                  Get Code\n                </Button>\n                <Button\n                  disabled={loading}\n                  className=\"auth-confirm-button\"\n                  onClick={() => fetch(value)}\n                >\n                  Confirm\n                </Button>\n              </div>\n              <Button\n                className=\"auth-go-back\"\n                onClick={() => setInstanned(undefined)}\n              >\n                Go Back\n              </Button>\n            </div>\n          </form>\n        ) : (\n          <FileUpload />\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/pages/Auth/FileUpload.tsx",
    "content": "import React, { useState } from 'react';\nimport { useRxAsync } from 'use-rx-hooks';\nimport { useBoolean } from '../../hooks/useBoolean';\nimport ErrorOutline from '@material-ui/icons/ErrorOutline';\n\nfunction readFile(file: File) {\n  return new Promise<OAuthKeys>((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = event => {\n      if (event.target) {\n        try {\n          const content = JSON.parse(event.target.result as string);\n          content.installed && resolve(content);\n        } catch (error) {}\n      }\n      reject('Invalid JSON file');\n    };\n    reader.readAsText(file);\n  });\n}\n\nfunction onSuccess(payload: OAuthKeys) {\n  window.oAuth2Storage.save(payload);\n  window.location.reload();\n}\n\nexport function FileUpload() {\n  const [isDragover, dragover, dragleave] = useBoolean();\n  const [errorMsg, setErrorMsg] = useState('');\n  const [, { fetch }] = useRxAsync(readFile, {\n    defer: true,\n    onSuccess,\n    onFailure: setErrorMsg\n  });\n\n  return (\n    <div className=\"file-upload\">\n      <div className=\"file-upload-header\">\n        <a\n          href=\"https://github.com/Pong420/google-tasks-desktop#project-setup\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          How to get OAuth2 JSON file\n        </a>\n      </div>\n      <div\n        className={['file-upload-content', isDragover && 'dragover']\n          .filter(Boolean)\n          .join(' ')\n          .trim()}\n      >\n        <label htmlFor=\"file-upload-input\">\n          Choose the OAuth2 JSON file or drag it here.\n        </label>\n        <input\n          type=\"file\"\n          name=\"files\"\n          id=\"file-upload-input\"\n          accept=\"application/json\"\n          onDragOver={dragover}\n          onDrop={dragleave}\n          onDragLeave={dragleave}\n          onChange={evt => {\n            const { files } = evt.currentTarget;\n            if (files && files.length && files[0].type === 'application/json') {\n              fetch(files[0]);\n            } else {\n              setErrorMsg('Invalid file or format');\n            }\n          }}\n        />\n      </div>\n      <div className=\"file-upload-footer\">\n        {errorMsg && <ErrorOutline />} {errorMsg}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/pages/Auth/index.ts",
    "content": "import './Auth.scss';\n\nexport * from './Auth';\nexport { Auth as default } from './Auth';\n"
  },
  {
    "path": "src/pages/TaskList/CompletedTaskList/CompletedTaskList.scss",
    "content": "$completed-task-list-header-height: 54px;\n\n.completed-tasks-list {\n  @include dimen(100%, $completed-task-list-header-height);\n  @include relative();\n  flex: 1 0 auto;\n  z-index: $text-highlight-z-index;\n\n  .scroll-content {\n    @include dimen(100%);\n    overflow: auto;\n  }\n}\n\n.completed-tasks-list-inner {\n  @include absolute(null, 0, 0);\n  @include animate(transform);\n  @include flex($flex-direction: column);\n  @include dimen(100%, 100vh);\n  background-color: var(--main-color);\n  max-height: calc(100vh - var(--header-height));\n  padding-bottom: 2px;\n  transform: translateY(calc(100% - #{$completed-task-list-header-height}));\n  z-index: 10;\n\n  .completed-tasks-list-content {\n    visibility: hidden;\n  }\n\n  &.expanded {\n    transform: translateY(0);\n    .completed-tasks-list-content {\n      visibility: visible;\n    }\n  }\n}\n\n.completed-tasks-list-header {\n  @include dimen(100%, $completed-task-list-header-height);\n  @include flex(center, space-between);\n  @include fake-border($borderWidth: 1px, $color: var(--main-color-diff));\n  @include typeface('Nunito Sans', 600);\n  @include padding-x(($padding-x, 10px));\n\n  border-top: 1px solid var(--border-color);\n  cursor: pointer;\n  flex: 1 0 auto;\n}\n\n.completed-tasks-list-content {\n  @include flex();\n  @include sq-dimen(100%);\n  overflow: hidden;\n  padding-bottom: 1px; // avoid scroll shown when there is only one completed task\n}\n"
  },
  {
    "path": "src/pages/TaskList/CompletedTaskList/CompletedTaskList.tsx",
    "content": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport { CompletedTask } from '../Task';\nimport { IconButton } from '../../../components/Mui';\nimport { useBoolean } from '../../../hooks/useBoolean';\nimport { completedTaskIdsSelector } from '../../../store';\nimport ExpandIcon from '@material-ui/icons/ExpandLess';\nimport CollapseIcon from '@material-ui/icons/ExpandMore';\n\nexport function CompletedTaskList() {\n  const tasks = useSelector(completedTaskIdsSelector);\n  const [expanded, , , toggle] = useBoolean();\n\n  if (tasks.length === 0) {\n    return null;\n  }\n\n  return (\n    <div className=\"completed-tasks-list\">\n      <div\n        className={`completed-tasks-list-inner ${\n          expanded ? 'expanded' : ''\n        }`.trim()}\n      >\n        <div className=\"completed-tasks-list-header\" onClick={toggle}>\n          Completed ({tasks.length})\n          <IconButton icon={expanded ? CollapseIcon : ExpandIcon} />\n        </div>\n        <div className=\"completed-tasks-list-content\">\n          <div className=\"scroll-content\">\n            {tasks.map(uuid => (\n              <CompletedTask key={uuid} uuid={uuid} />\n            ))}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/pages/TaskList/CompletedTaskList/index.ts",
    "content": "import './CompletedTaskList.scss';\n\nexport * from './CompletedTaskList';\nexport { CompletedTaskList as default } from './CompletedTaskList';\n"
  },
  {
    "path": "src/pages/TaskList/NewTask/NewTask.scss",
    "content": ".new-task {\n  @include dimen(100%, 56px);\n  @include flex(center);\n  @include padding-x(10px);\n\n  .new-task-button {\n    @include flex(center);\n    border-radius: 50px;\n    cursor: pointer;\n    flex: 1 0 auto;\n    margin-right: 10px;\n    user-select: none;\n\n    &:hover {\n      background-color: var(--main-color-diff);\n    }\n\n    .mui-icon-button {\n      background: none;\n      font-weight: 500;\n      margin-right: 5px;\n    }\n\n    svg {\n      color: var(--accent-color);\n    }\n  }\n}\n\n.task-list-menu-paper {\n  @include dimen(100%);\n  .task-list-menu-title {\n    @include padding-x(16px);\n    color: var(--text-color);\n    font-size: 0.875em !important;\n    height: 30px;\n    line-height: 30px;\n    outline: none;\n  }\n}\n"
  },
  {
    "path": "src/pages/TaskList/NewTask/NewTask.tsx",
    "content": "import React from 'react';\nimport { SvgIconProps } from '@material-ui/core/SvgIcon';\nimport { IconButton, useMuiMenu } from '../../../components/Mui';\nimport { TaskListMenu } from '../TaskListMenu';\nimport { useTaskActions } from '../../../store';\nimport AddIcon from '@material-ui/icons/Add';\nimport MoreIcon from '@material-ui/icons/MoreVert';\n\nconst iconProps: SvgIconProps = { color: 'secondary' };\n\nexport function NewTask() {\n  const { createTask } = useTaskActions();\n  const { anchorEl, setAnchorEl, onClose } = useMuiMenu();\n\n  return (\n    <div className=\"new-task\">\n      <div className=\"new-task-button\" onClick={() => createTask()}>\n        <IconButton icon={AddIcon} iconProps={iconProps} disableTouchRipple />\n        <div>Add a task</div>\n      </div>\n      <IconButton icon={MoreIcon} onClick={setAnchorEl} />\n      <TaskListMenu open={!!anchorEl} anchorEl={anchorEl} onClose={onClose} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/pages/TaskList/NewTask/index.ts",
    "content": "import './NewTask.scss';\n\nexport * from './NewTask';\nexport { NewTask as default } from './NewTask';\n"
  },
  {
    "path": "src/pages/TaskList/Task/CompletedTask.tsx",
    "content": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport { Task, TaskProps } from './Task';\nimport { DeleteIcon, IconButton } from '../../../components/Mui';\nimport { useTaskActions, completedTaskSelector } from '../../../store';\n\ninterface Props extends TaskProps {}\n\nexport function CompletedTask(props: Props) {\n  const { deleteTask } = useTaskActions();\n  const { title } = useSelector(completedTaskSelector(props.uuid)) || {};\n\n  return (\n    <Task\n      {...props}\n      readOnly\n      className=\"completed-task\"\n      value={title}\n      endAdornment={\n        <IconButton\n          tooltip=\"Delete\"\n          icon={DeleteIcon}\n          onClick={() => deleteTask({ uuid: props.uuid })}\n        />\n      }\n    />\n  );\n}\n"
  },
  {
    "path": "src/pages/TaskList/Task/DatePicker/DatePicker.scss",
    "content": ".calender-header {\n  @include flex(center, space-between);\n\n  .month-year {\n    @include typeface('Nunito Sans', 700);\n    color: var(--date-color);\n  }\n}\n\n.calendar-content {\n  @include padding-x(6px);\n  display: grid;\n  grid-template-columns: repeat(7, auto);\n\n  .grid {\n    @include relative();\n    text-align: center;\n\n    &:before {\n      @include dimen(100%, 0);\n      content: '';\n      display: block;\n      padding-bottom: 100%;\n    }\n\n    .grid-content {\n      @include absolute(0, null, 0);\n      @include flex(center, center);\n      @include sq-dimen(100%);\n      @include typeface('Nunito Sans', 500);\n\n      font-size: 12px;\n    }\n  }\n\n  .day {\n    color: var(--day-color);\n    cursor: default;\n  }\n\n  .date {\n    color: var(--date-color);\n    cursor: pointer;\n\n    .grid-content {\n      border-radius: 50%;\n    }\n\n    @mixin backgorundWidthShadow($color) {\n      background-color: $color;\n      [data-theme^='light'] & {\n        box-shadow: 0px 2px 10px -2px #{$color};\n      }\n    }\n\n    &.today {\n      .grid-content {\n        @include backgorundWidthShadow(var(--accent-light-color));\n        color: #333;\n      }\n    }\n\n    &.selected {\n      .grid-content {\n        @include backgorundWidthShadow(var(--accent-dark-color));\n        color: #fff;\n      }\n    }\n\n    &.lastMonth,\n    &.nextMonth {\n      color: var(--day-color);\n    }\n  }\n}\n"
  },
  {
    "path": "src/pages/TaskList/Task/DatePicker/DatePicker.tsx",
    "content": "import React, { useState, useCallback, HTMLAttributes, useMemo } from 'react';\nimport { IconButton } from '../../../../components/Mui/IconButton';\nimport LeftArrowIcon from '@material-ui/icons/KeyboardArrowLeftRounded';\nimport RightArrowIcon from '@material-ui/icons/KeyboardArrowRightRounded';\n\nconst days = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];\n\ntype GridProps = HTMLAttributes<HTMLDivElement>;\n\ninterface DateProps extends Omit<GridProps, 'onClick'> {\n  date: Date;\n  selected?: boolean;\n  onClick(d: Date): void;\n}\n\ninterface Props {\n  value?: Date;\n  onChange?(date: Date): void;\n}\n\nconst Grid = React.memo(({ children, className, ...props }: GridProps) => (\n  <div className={`grid ${className}`.trim()} {...props}>\n    <div className=\"grid-content\">{children}</div>\n  </div>\n));\n\nconst DateGrid = React.memo(\n  ({ date, onClick, selected, className, ...props }: DateProps) => {\n    const onClickCallback = useCallback(() => {\n      onClick(date);\n    }, [date, onClick]);\n\n    return (\n      <Grid\n        className={['date', className, selected && 'selected']\n          .filter(Boolean)\n          .join(' ')\n          .trim()}\n        onClick={onClickCallback}\n        {...props}\n      />\n    );\n  }\n);\n\nexport function DatePicker({ value, onChange }: Props) {\n  const [{ dates, date }, setDisplay] = useState(getDisplayData(value));\n  const [currDate, setCurrDate] = useState<Date>(date);\n\n  const [prevMonth, nextMonth] = useMemo(() => {\n    const handler = (step: number) => () =>\n      setDisplay(({ date }) => getDisplayData(date.addMonths(step)));\n    return [handler(-1), handler(1)];\n  }, []);\n\n  return (\n    <div className=\"date-picker\">\n      <div className=\"calender\">\n        <div className=\"calender-header\">\n          <IconButton icon={LeftArrowIcon} onClick={prevMonth} />\n          <div className=\"month-year\">\n            {date.getMonthName()} {date.getFullYear()}\n          </div>\n          <IconButton icon={RightArrowIcon} onClick={nextMonth} />\n        </div>\n        <div className=\"calendar-content\">\n          {days.map((day, index) => (\n            <Grid className=\"day\" key={index}>\n              {day}\n            </Grid>\n          ))}\n          {dates.map((d, index) => (\n            <DateGrid\n              key={index}\n              date={d}\n              selected={currDate.compare(d).sameDate}\n              className={[\n                ...Object.entries(date.compare(d)).reduce(\n                  (c, [n, v]) => [...c, v && n],\n                  [] as Array<string | boolean>\n                ),\n                d.isToday() && 'today'\n              ]\n                .filter(Boolean)\n                .join(' ')\n                .trim()}\n              onClick={(d: Date) => {\n                setCurrDate(d);\n                onChange && onChange(d);\n              }}\n            >\n              {d.getDate()}\n            </DateGrid>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction getDisplayData(dateObj: Date = new Date()) {\n  const currDate = dateObj.getDate(); // current date\n  const one = dateObj.addDays(-1 * currDate + 1); // first day of current month\n  const index = one.getDay() - 1;\n  const MonthDayCount = one.getMonthDayCount();\n  const curMonth = [];\n  const lastMonth = [];\n  const nextMonth = [];\n  let dates = [];\n\n  for (let a = 0; a < MonthDayCount; a++) {\n    curMonth.push(one.addDays(a));\n  }\n\n  for (let b = 0; b < index; b++) {\n    lastMonth.push(one.addDays((b + 1) * -1));\n  }\n\n  dates = lastMonth.reverse().concat(curMonth);\n\n  const length = dates.length;\n\n  for (let c = length; c < 42; c++) {\n    nextMonth.push(curMonth[curMonth.length - 1].addDays(c - length + 1));\n  }\n\n  dates = dates.concat(nextMonth);\n\n  const cIndex = lastMonth.length + currDate - 1;\n  const wIndex = cIndex - dateObj.getDay() + 1; // index of this week first day in days\n\n  return {\n    dates,\n    week: dates.slice(wIndex, wIndex + 7),\n    date: dateObj\n  };\n}\n"
  },
  {
    "path": "src/pages/TaskList/Task/DatePicker/index.ts",
    "content": "import './DatePicker.scss';\n\nexport * from './DatePicker';\nexport { DatePicker as default } from './DatePicker';\n"
  },
  {
    "path": "src/pages/TaskList/Task/DateTimeDialog/DateTimeDialog.scss",
    "content": ".date-time-dialog-paper.date-time-dialog-paper {\n  @include dimen(100%);\n  color: var(--text-secondary-color);\n  max-height: 420px;\n  max-width: 300px;\n\n  button {\n    color: var(--text-secondary-color);\n    text-transform: inherit;\n  }\n\n  .dialog-scroll-content {\n    padding: 5px 15px 20px 15px;\n  }\n\n  .dialog-actions {\n    margin-top: 20px;\n  }\n}\n"
  },
  {
    "path": "src/pages/TaskList/Task/DateTimeDialog/DateTimeDialog.tsx",
    "content": "import React, {\n  useState,\n  useEffect,\n  createContext,\n  ReactNode,\n  useContext\n} from 'react';\nimport {\n  ConfirmDialog,\n  ConfirmDialogProps\n} from '../../../../components/Mui/Dialog';\nimport { DatePicker } from '../DatePicker';\nimport { useBoolean } from '../../../../hooks/useBoolean';\n\ninterface Props {\n  date?: Date;\n  onConfirm(date: Date): void;\n}\n\ntype Control = Omit<\n  ConfirmDialogProps,\n  'onChange' | 'onConfirm' | 'confirmLabel'\n>;\n\ninterface DateTimeDialogContext {\n  openDateTimeDialog: (props: Props) => void;\n}\n\nconst dialogClasses: ConfirmDialogProps['classes'] = {\n  paper: 'date-time-dialog-paper'\n};\n\nconst Context = createContext({} as DateTimeDialogContext);\n\nexport function useDateTimeDialog() {\n  return useContext(Context);\n}\n\nexport function DateTimeDialogProvider({ children }: { children: ReactNode }) {\n  const [props, setProps] = useState<Props & Partial<Control>>();\n  const [isOpen, open, close] = useBoolean();\n\n  useEffect(() => {\n    props && open();\n  }, [props, open]);\n\n  return (\n    <Context.Provider value={{ openDateTimeDialog: setProps }}>\n      {children}\n      {props && (\n        <DateTimeDialog\n          {...props}\n          open={isOpen}\n          onClose={close}\n          onExited={(...args) => {\n            props.onExited && props.onExited(...args);\n            setProps(undefined);\n          }}\n        />\n      )}\n    </Context.Provider>\n  );\n}\n\nexport const DateTimeDialog = ({\n  date: defaultDate,\n  onConfirm,\n  ...props\n}: Props &\n  Omit<ConfirmDialogProps, 'onChange' | 'onConfirm' | 'confirmLabel'>) => {\n  const [date, setDate] = useState(defaultDate || new Date());\n\n  return (\n    <ConfirmDialog\n      confirmLabel=\"OK\"\n      classes={dialogClasses}\n      onConfirm={() => {\n        onConfirm(date);\n      }}\n      {...props}\n    >\n      <DatePicker value={defaultDate} onChange={setDate} />\n    </ConfirmDialog>\n  );\n};\n"
  },
  {
    "path": "src/pages/TaskList/Task/DateTimeDialog/index.ts",
    "content": "import './DateTimeDialog.scss';\n\nexport * from './DateTimeDialog';\nexport { DateTimeDialog as default } from './DateTimeDialog';\n"
  },
  {
    "path": "src/pages/TaskList/Task/Task.scss",
    "content": "$border: 1px solid var(--border-color);\n$min-task-height: 50px;\n\n@mixin withTopBottomBorder($color: var(--border-color)) {\n  &:before {\n    @include absolute(0, null, 0);\n    @include dimen(100%, calc(100% + 1px));\n\n    border-top: 1px solid $color;\n    border-bottom: 1px solid $color;\n    content: '';\n  }\n}\n\n.task {\n  @include dimen(100%);\n  @include flex(center);\n  @include padding-x(10px);\n  @include relative();\n  @include withTopBottomBorder(transparent);\n  background-color: var(--main-color);\n\n  .task-input-base {\n    @include relative();\n    @include withTopBottomBorder();\n    min-height: $min-task-height;\n    min-width: 0;\n\n    .task-input-base-end-adornment {\n      visibility: hidden;\n    }\n  }\n\n  &:hover,\n  &.focused,\n  &.dragging {\n    background-color: var(--task-highlight-background-color);\n\n    .task-input-base::before {\n      border-color: transparent;\n    }\n  }\n\n  &:hover,\n  &.focused {\n    &:before {\n      border-color: var(--border-color);\n    }\n    &,\n    & + .task:not(:last-child) {\n      .task-input-base::before {\n        border-color: transparent;\n      }\n    }\n\n    // input endAdornment\n    .task-input-base .mui-icon-button {\n      visibility: visible;\n    }\n  }\n\n  &:hover {\n    z-index: 1;\n  }\n\n  &:first-child {\n    .task-input-base:before {\n      border-top-color: transparent;\n    }\n  }\n\n  .toggle-completed {\n    @include relative();\n\n    .mui-icon-button {\n      + .mui-icon-button {\n        @include absolute();\n        visibility: hidden;\n      }\n    }\n\n    .mui-tick-icon {\n      color: var(--accent-color);\n    }\n  }\n\n  .toggle-completed,\n  .task-input-base-end-adornment {\n    @include flex(center);\n    align-self: stretch;\n    max-height: 68px;\n    margin-top: 1px;\n  }\n\n  .task-input-content {\n    @include dimen(100%);\n    @include flex(flex-start, center, column);\n    @include padding-y(12px); // padding-y for multiple line\n    @include relative();\n    align-self: center;\n    overflow: hidden;\n\n    .mui-input-base {\n      $line-height: 20px;\n      @include sq-dimen(auto);\n      font-size: 14px;\n      line-height: $line-height;\n      min-height: 0;\n      padding: 0;\n      width: 100%;\n\n      textarea {\n        min-height: $line-height;\n      }\n    }\n  }\n\n  .task-notes {\n    @include dimen(100%);\n    @include multi-line-ellipsis($line-height: 16px);\n    @include margin-y(2px);\n    color: var(--text-secondary-color);\n    font-size: 12px;\n    overflow-wrap: break-word;\n  }\n\n  .task-due-date-button {\n    @include flex(center, center);\n\n    align-self: flex-start;\n    background-color: var(--task-highlight-background-color);\n    border: 1px solid var(--main-color-diff);\n    border-radius: 3px;\n    color: var(--text-secondary-color);\n    cursor: pointer;\n    font-size: 12px;\n    line-height: 16px;\n    margin-top: 8px;\n    padding: 6px 7px 5px;\n\n    svg {\n      color: var(--accent-color);\n      font-size: 16px;\n      margin-right: 6px;\n    }\n\n    &:after {\n      content: attr(data-date);\n    }\n\n    &[data-date^='Today'] {\n      &:after {\n        color: var(--accent-color);\n        font-weight: 500;\n      }\n    }\n\n    &[data-date^='Yesterday'],\n    &[data-date*='ago'] {\n      svg {\n        color: var(--error-color);\n      }\n    }\n\n    &:hover {\n      background-color: var(--main-color);\n      box-shadow: 0px 5px 10px var(--shadow-color);\n    }\n  }\n}\n\n.completed-task {\n  textarea {\n    text-decoration: line-through;\n  }\n}\n"
  },
  {
    "path": "src/pages/TaskList/Task/Task.tsx",
    "content": "import React, { ReactNode } from 'react';\nimport { useSelector } from 'react-redux';\nimport { Input, InputProps } from '../../../components/Mui';\nimport { taskSelector } from '../../../store';\nimport { ToggleCompleted } from './ToggleCompleted';\nimport { TaskInput, TaskInputProps } from './TaskInput';\n\nexport interface TaskProps\n  extends InputProps,\n    Pick<TaskInputProps, 'onDueDateBtnClick'> {\n  className?: string;\n  uuid: string;\n  isEmpty?: boolean;\n  endAdornment?: ReactNode;\n}\n\nexport const Task = React.forwardRef<HTMLDivElement, TaskProps>(\n  (\n    {\n      className,\n      uuid,\n      isEmpty,\n      endAdornment,\n      onDueDateBtnClick,\n      ...inputProps\n    },\n    ref\n  ) => {\n    const { due, notes } = useSelector(taskSelector(uuid)) || {};\n\n    return (\n      <div\n        data-uuid={uuid}\n        className={['task', className].filter(Boolean).join(' ').trim()}\n        ref={ref}\n      >\n        <ToggleCompleted isEmpty={!!isEmpty} uuid={uuid} />\n        <Input\n          {...inputProps}\n          fullWidth\n          className=\"task-input-base\"\n          inputProps={{ due, notes, onDueDateBtnClick }}\n          inputComponent={TaskInput as InputProps['inputComponent']}\n          endAdornment={\n            <div className=\"task-input-base-end-adornment\">{endAdornment}</div>\n          }\n        />\n      </div>\n    );\n  }\n);\n"
  },
  {
    "path": "src/pages/TaskList/Task/TaskInput.tsx",
    "content": "import React from 'react';\nimport { Input, InputProps } from '../../../components/Mui';\nimport { Schema$Task } from '../../../typings';\nimport EventAvailableIcon from '@material-ui/icons/EventAvailable';\n\nexport interface TaskInputProps extends Pick<Schema$Task, 'due' | 'notes'> {\n  onDueDateBtnClick?(): void;\n}\n\ntype Props = TaskInputProps & InputProps;\n\nexport function TaskInput({\n  due,\n  notes,\n  onDueDateBtnClick,\n  ...inputProps\n}: Props) {\n  return (\n    <div className=\"task-input-content\">\n      <Input {...inputProps} multiline />\n      {notes && <div className=\"task-notes\">{notes}</div>}\n      {due && (\n        <div\n          className=\"task-due-date-button\"\n          onClick={onDueDateBtnClick}\n          data-date={dateFormat(new Date(due))}\n        >\n          <EventAvailableIcon />\n        </div>\n      )}\n    </div>\n  );\n}\n\nfunction dateFormat(d: Date) {\n  const now = new Date();\n  const dayDiff = Math.floor((+now - +d) / 1000 / 60 / 60 / 24);\n\n  if (dayDiff === 0) {\n    return 'Today';\n  }\n\n  if (dayDiff === -1) {\n    return 'Tomorrow';\n  }\n\n  if (dayDiff < -1) {\n    return d.format('D, j M');\n  }\n\n  if (dayDiff === 1) {\n    return 'Yesterday';\n  }\n\n  if (dayDiff < 7) {\n    return `${dayDiff} days ago`;\n  }\n\n  return `${Math.floor(dayDiff / 7)} weeks ago`;\n}\n"
  },
  {
    "path": "src/pages/TaskList/Task/TodoTask/TodoTask.scss",
    "content": ".todo-task {\n  @include textHighlight();\n  user-select: none;\n\n  &.dragging {\n    @include padding-x((0px, 20px));\n\n    border-radius: 12.5px;\n    border: 1px solid var(--border-color);\n    box-shadow: 0px 7px 10px -5px var(--shadow-color);\n    left: auto !important;\n    right: 5px !important;\n    width: calc(100% - 60px) !important;\n    z-index: 1000;\n\n    .task-input-base-end-adornment {\n      display: none;\n    }\n\n    .toggle-completed {\n      margin-left: 0;\n    }\n\n    // override textHighlight\n    &:before,\n    &:after {\n      visibility: hidden;\n    }\n  }\n\n  &.highlight-bottom-border {\n    &:after {\n      @include dimen(100%);\n      border-color: var(--accent-color);\n      transition: 0s;\n    }\n\n    .task-input-base {\n      border-color: transparent;\n    }\n  }\n\n  &:last-child {\n    border-bottom: 1px solid var(--border-color);\n  }\n\n  .toggle-completed {\n    &:hover {\n      > button:nth-child(1) {\n        visibility: hidden;\n      }\n      > button:nth-child(2) {\n        visibility: visible;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/pages/TaskList/Task/TodoTask/TodoTask.tsx",
    "content": "import React, {\n  useRef,\n  useMemo,\n  useEffect,\n  MouseEvent,\n  KeyboardEvent\n} from 'react';\nimport { useSelector } from 'react-redux';\nimport { Task, TaskProps } from '../Task';\nimport { useTodoTaskDetails, EditTaskButton } from '../TodoTaskDetails';\nimport { useDateTimeDialog } from '../DateTimeDialog';\nimport { useTodoTaskMenu } from './TodoTaskMenu';\nimport {\n  focusedSelector,\n  useTaskActions,\n  todoTaskSelector,\n  getDateLabel\n} from '../../../../store';\nimport { useMouseTrap } from '../../../../hooks/useMouseTrap';\nimport { Schema$Task } from '../../../../typings';\n\nexport interface TodoTaskProps extends TaskProps {\n  index: number;\n  inherit?: (keyof Schema$Task)[];\n  prevDue?: string | null;\n  sortByDate?: boolean;\n  prevIndex?: number;\n  nextIndex?: number;\n}\n\nexport const TodoTask = React.memo(\n  ({\n    uuid,\n    index,\n    className,\n    inherit,\n    prevDue,\n    sortByDate,\n    prevIndex = index - 1,\n    nextIndex = index + 1,\n    ...props\n  }: TodoTaskProps) => {\n    const ref = useRef<HTMLDivElement>(null);\n    const {\n      createTask,\n      deleteTask,\n      update: updateTask,\n      moveTask,\n      setFocus\n    } = useTaskActions();\n\n    const focused = useSelector(focusedSelector(uuid));\n\n    const { title, due } = useSelector(todoTaskSelector(uuid)) || {};\n\n    const { onDelete, moveTaskUp, moveTaskDown, ...handler } = useMemo(() => {\n      return {\n        moveTaskUp: () => moveTask({ uuid, from: index, to: index - 1 }),\n        moveTaskDown: () => moveTask({ uuid, from: index, to: index + 1 }),\n        onDelete: () => deleteTask({ uuid }),\n        // prevent focused task trigger `onBlur` event\n        onMouseDown: (event: MouseEvent<HTMLElement>) => {\n          !(event.target instanceof HTMLTextAreaElement) &&\n            event.preventDefault();\n        },\n        onClick: (event: MouseEvent<HTMLElement>) => {\n          event.currentTarget\n            .querySelector<HTMLTextAreaElement>('textarea')!\n            .focus();\n        },\n        onBlur: () => {\n          // reduce unnecessary `FOCUS_TASK` action\n          setTimeout(() => {\n            const el =\n              document.activeElement?.parentElement?.parentElement\n                ?.parentElement?.parentElement;\n            (!el || !el.classList.contains('task')) && setFocus(null);\n          }, 0);\n        },\n        onKeyDown: (event: KeyboardEvent<HTMLTextAreaElement>) => {\n          const input = event.currentTarget;\n\n          if (event.key === 'Enter') {\n            event.preventDefault();\n            createTask({\n              prevTask: uuid,\n              inherit: inherit && { uuid, keys: inherit }\n            });\n          }\n\n          if (event.key === 'Backspace' && !input.value.trim()) {\n            event.preventDefault();\n            deleteTask({ uuid, prevTaskIndex: index - 1 });\n          }\n\n          if (event.key === 'Escape') {\n            input.blur();\n          }\n\n          if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {\n            const { selectionStart, selectionEnd, value } = input;\n            const notHightlighted = selectionStart === selectionEnd;\n            const shouldFocusPrev =\n              event.key === 'ArrowUp' && selectionStart === 0;\n            const shouldFocusNext =\n              event.key === 'ArrowDown' && selectionStart === value.length;\n\n            const to = event.key === 'ArrowUp' ? prevIndex : nextIndex;\n\n            if (notHightlighted && (shouldFocusPrev || shouldFocusNext)) {\n              event.preventDefault();\n              setFocus(to);\n            }\n          }\n        }\n      };\n    }, [\n      uuid,\n      index,\n      createTask,\n      deleteTask,\n      moveTask,\n      setFocus,\n      inherit,\n      prevIndex,\n      nextIndex\n    ]);\n\n    const { updateDue, moveDownByDate, moveUpByDate } = useMemo(() => {\n      // const now = new Date();\n      const date = due ? new Date(due) : null;\n      const updateDue = (date?: Date) => {\n        date && updateTask({ uuid, due: date.toISODateString() });\n      };\n\n      const moveDownByDate = () => {\n        const now = new Date();\n        const label = getDateLabel(due, now);\n        let newDate: Date | null = null;\n        if (label === 'Past') newDate = now;\n        else if (label !== 'No date') newDate = date!.addDays(1);\n\n        newDate && updateDue(newDate);\n      };\n\n      const moveUpByDate = () => {\n        const now = new Date();\n        const label = getDateLabel(due, now);\n        let newDate: Date | null = null;\n        if (label === 'No date') newDate = prevDue ? new Date(prevDue) : now;\n        else if (label !== 'Past' && label !== 'Today')\n          newDate = date!.addDays(-1);\n\n        newDate && updateDue(newDate);\n      };\n\n      return { updateDue, moveUpByDate, moveDownByDate };\n    }, [uuid, due, prevDue, updateTask]);\n\n    const { openDateTimeDialog: _openDateTimeDialog } = useDateTimeDialog();\n    const openDateTimeDialog = () =>\n      _openDateTimeDialog({\n        date: due ? new Date(due) : undefined,\n        onConfirm: updateDue\n      });\n\n    const { openTodoTaskDetails: _openTodoTaskDetails } = useTodoTaskDetails();\n    const openTodoTaskDetails = () =>\n      _openTodoTaskDetails({ openDateTimeDialog, uuid });\n\n    const { openTodoTaskMenu: _openTodoTaskMenu } = useTodoTaskMenu();\n    const openTodoTaskMenu = (event: MouseEvent<HTMLElement>) =>\n      _openTodoTaskMenu({\n        event,\n        uuid,\n        onDelete,\n        openDateTimeDialog,\n        moveToAnotherList: () =>\n          _openTodoTaskDetails({\n            taskListDropdownOpened: true,\n            openDateTimeDialog,\n            uuid\n          })\n      });\n\n    useEffect(() => {\n      const el = ref.current;\n      const input = el && el.querySelector<HTMLTextAreaElement>('textarea');\n      if (input && focused) {\n        const { length } = input.value;\n        input.focus();\n        // make sure cursor place at end of textarea\n        input.setSelectionRange(length, length);\n      }\n    }, [focused]);\n\n    useMouseTrap(focused ? 'shift+enter' : '', openTodoTaskDetails);\n    useMouseTrap(\n      focused ? 'option+up' : '',\n      sortByDate ? moveUpByDate : moveTaskUp\n    );\n    useMouseTrap(\n      focused ? 'option+down' : '',\n      sortByDate ? moveDownByDate : moveTaskDown\n    );\n\n    return (\n      <>\n        <Task\n          {...props}\n          {...handler}\n          ref={ref}\n          uuid={uuid}\n          value={title}\n          isEmpty={!(title && title.trim())}\n          onContextMenu={openTodoTaskMenu}\n          onDueDateBtnClick={openDateTimeDialog}\n          onFocus={() => !focused && setFocus(uuid)}\n          onChange={event =>\n            updateTask({ uuid, title: event.currentTarget.value })\n          }\n          className={['todo-task', focused ? 'focused' : '', className]\n            .join(' ')\n            .trim()}\n          endAdornment={<EditTaskButton onClick={openTodoTaskDetails} />}\n        />\n      </>\n    );\n  }\n);\n"
  },
  {
    "path": "src/pages/TaskList/Task/TodoTask/TodoTaskMenu.tsx",
    "content": "import React, {\n  createContext,\n  useContext,\n  useState,\n  MouseEvent,\n  ReactNode\n} from 'react';\nimport { useSelector } from 'react-redux';\nimport {\n  useMuiMenuItem,\n  Menu,\n  MenuProps,\n  useMuiMenu\n} from '../../../../components/Mui';\nimport { todoTaskSelector } from '../../../../store';\n\ninterface Props {\n  uuid: string;\n  onDelete?: () => void;\n  openDateTimeDialog?: () => void;\n  moveToAnotherList?: () => void;\n  firstTask?: boolean;\n}\n\ninterface TodoTaskMenuContext {\n  openTodoTaskMenu: (props: Props & { event: MouseEvent<HTMLElement> }) => void;\n}\n\ntype Control = Omit<MenuProps, 'ref'>;\n\nconst classes: MenuProps['classes'] = { paper: 'todo-task-menu-paper' };\n\nconst Context = createContext({} as TodoTaskMenuContext);\n\nexport function useTodoTaskMenu() {\n  return useContext(Context);\n}\n\nexport function TodoTaskMenuProvider({ children }: { children: ReactNode }) {\n  const [props, setProps] = useState<Props & Partial<Control>>();\n  const { anchorPosition, setAnchorPosition, onClose } = useMuiMenu();\n\n  return (\n    <Context.Provider\n      value={{\n        openTodoTaskMenu: ({ event, ...props }) => {\n          setProps(props);\n          setAnchorPosition(event);\n        }\n      }}\n    >\n      {children}\n      {props && (\n        <TodoTaskMenu\n          {...props}\n          keepMounted={false}\n          anchorPosition={anchorPosition}\n          anchorReference=\"anchorPosition\"\n          open={!!anchorPosition}\n          onClose={() => {\n            onClose();\n            setProps(undefined);\n          }}\n        />\n      )}\n    </Context.Provider>\n  );\n}\n\nexport const TodoTaskMenu = ({\n  uuid,\n  onClose,\n  onDelete,\n  openDateTimeDialog,\n  moveToAnotherList,\n  firstTask,\n  ...props\n}: Props & Control) => {\n  const MenuItem = useMuiMenuItem({ onClose });\n  const { due } = useSelector(todoTaskSelector(uuid)) || {};\n\n  return (\n    <Menu {...props} classes={classes} onClose={onClose}>\n      <MenuItem text=\"Delete\" onClick={onDelete} />\n      <MenuItem\n        text={`${due ? 'Change' : 'Add'} date/time`}\n        onClick={openDateTimeDialog}\n      />\n      <MenuItem text=\"Add a subtask\" disabled />\n      {!firstTask && <MenuItem text=\"Indent\" disabled />}\n      <MenuItem text=\"Move to another list\" onClick={moveToAnotherList} />\n    </Menu>\n  );\n};\n"
  },
  {
    "path": "src/pages/TaskList/Task/TodoTask/index.ts",
    "content": "import './TodoTask.scss';\n\nexport * from './TodoTask';\nexport { TodoTask as default } from './TodoTask';\n"
  },
  {
    "path": "src/pages/TaskList/Task/TodoTaskDetails/DateTimeButton.tsx",
    "content": "import React from 'react';\nimport { IconButton } from '../../../../components/Mui';\nimport Button from '@material-ui/core/Button';\nimport CloseIcon from '@material-ui/icons/Close';\n\ninterface Props {\n  date?: Date;\n  onClick?: () => void;\n  onRemove?: () => void;\n}\n\nexport const DateTimeButton = ({ date, onClick, onRemove }: Props) => {\n  if (!date) {\n    return <Button onClick={onClick}>Add date/time</Button>;\n  }\n\n  return (\n    <div className=\"task-deatails-due-date-button\">\n      <div className=\"task-deatails-due-date-clickable\" onClick={onClick} />\n      <div>\n        <div className=\"date\">{date.format('D, j M')}</div>\n      </div>\n      <IconButton\n        tooltip=\"Remove date and time\"\n        icon={CloseIcon}\n        onClick={onRemove}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/pages/TaskList/Task/TodoTaskDetails/TodoTaskDetails.scss",
    "content": "$height: 36px;\n\n.todo-task-details {\n  .row,\n  .mui-input-base {\n    + .row,\n    + .mui-input-base {\n      margin-top: 8px;\n    }\n  }\n\n  .mui-input-base {\n    @include padding-y(0.5em);\n    line-height: 1.42em;\n\n    &.todo-task-details-title-field {\n      @include typeface('Nunito Sans', 600);\n      font-size: 16px;\n    }\n\n    &.todo-task-details-notes-field {\n      &,\n      textarea::placeholder {\n        font-size: 14px;\n      }\n    }\n  }\n\n  .row {\n    @include flex(center, stretch);\n    color: var(--text-secondary-color);\n    min-height: 46px;\n\n    .mui-dropdown-button {\n      @include typeface('Nunito Sans', 600);\n      background: var(--task-highlight-background-color);\n      color: var(--text-color);\n      min-height: $height;\n      line-height: $height;\n    }\n\n    > svg {\n      margin-right: 10px;\n    }\n\n    &.row-date,\n    &.row-subtask {\n      button {\n        @include typeface('Nunito Sans', 700);\n        text-transform: initial;\n        color: inherit;\n      }\n    }\n  }\n\n  .task-deatails-due-date-button {\n    @include dimen(100%);\n    @include flex(center, space-between);\n    @include padding-x(15px 0);\n    @include relative();\n\n    align-self: stretch;\n    border: 1px solid var(--border-color);\n    border-radius: 3px;\n    user-select: none;\n\n    .date {\n      color: var(--accent-dark-color);\n      font-weight: 500;\n    }\n\n    button {\n      background: none;\n    }\n\n    .task-deatails-due-date-clickable {\n      @include absolute(0, null, 0);\n      @include sq-dimen(100%);\n      cursor: pointer;\n    }\n  }\n}\n\n.details-task-list-dropdown-paper {\n  @include dimen(calc(100% - 60px));\n\n  .menu-content {\n    @include padding-y(0);\n  }\n\n  .scroll-content {\n    max-height: $height * 5;\n  }\n\n  .mui-menu-item {\n    &.mui-menu-item {\n      height: $height;\n\n      > div {\n        @include relative();\n        flex-direction: row-reverse;\n\n        .text {\n          @include padding-x(36px 0);\n\n          + svg {\n            @include absolute(null, null, 0);\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/pages/TaskList/Task/TodoTaskDetails/TodoTaskDetails.tsx",
    "content": "import React, {\n  KeyboardEvent,\n  useState,\n  createContext,\n  ReactNode,\n  useEffect,\n  useContext\n} from 'react';\nimport { useSelector } from 'react-redux';\nimport {\n  DeleteIcon,\n  EditIcon,\n  FullScreenDialog,\n  FullScreenDialogProps,\n  IconButton,\n  Input\n} from '../../../../components/Mui';\nimport { TaskListDropdown } from '../../TaskListDropdown';\nimport { DateTimeButton } from './DateTimeButton';\nimport { Schema$Task, Schema$TaskList } from '../../../../typings';\nimport { useBoolean } from '../../../../hooks/useBoolean';\nimport {\n  todoTaskSelector,\n  useTaskActions,\n  currentTaskListsSelector\n} from '../../../../store';\nimport FormatListBulletedIcon from '@material-ui/icons/FormatListBulleted';\nimport EventAvailableIcon from '@material-ui/icons/EventAvailable';\n\ninterface Props extends Pick<Schema$Task, 'uuid'> {\n  taskListDropdownOpened?: boolean;\n  openDateTimeDialog: () => void;\n}\n\ninterface TodoTaskDetailsContext {\n  openTodoTaskDetails: (props: Props) => void;\n}\n\nconst preventStartNewLine = (evt: KeyboardEvent<HTMLDivElement>) =>\n  evt.which === 13 && evt.preventDefault();\n\nconst dropdownButtonProps = {\n  fullWidth: true\n};\n\nexport const Context = createContext({} as TodoTaskDetailsContext);\n\nexport function useTodoTaskDetails() {\n  return useContext(Context);\n}\n\nexport function TodoTaskDetailsProvider({ children }: { children: ReactNode }) {\n  const [props, setProps] = useState<Props & Partial<FullScreenDialogProps>>();\n  const [isOpen, open, close] = useBoolean();\n\n  useEffect(() => {\n    props && open();\n  }, [props, open]);\n\n  return (\n    <Context.Provider value={{ openTodoTaskDetails: setProps }}>\n      {children}\n      {props && (\n        <TodoTaskDetails\n          {...props}\n          open={isOpen}\n          onClose={close}\n          onExited={(...args) => {\n            props.onExited && props.onExited(...args);\n            setProps(undefined);\n          }}\n        />\n      )}\n    </Context.Provider>\n  );\n}\n\nexport const EditTaskButton = ({ onClick }: { onClick(): void }) => {\n  return (\n    <IconButton\n      className=\"edit-task-button\"\n      tooltip=\"Edit details\"\n      icon={EditIcon}\n      onClick={onClick}\n    />\n  );\n};\n\nexport function TodoTaskDetails({\n  uuid,\n  open,\n  onClose,\n  openDateTimeDialog,\n  taskListDropdownOpened,\n  ...props\n}: Props & FullScreenDialogProps) {\n  const [shouldBeDeleted, deleteOnExited] = useBoolean();\n  const [moveTo, setMoveTo] = useState<Schema$TaskList>();\n  const {\n    update: updateTask,\n    deleteTask,\n    moveToAnotherList\n  } = useTaskActions();\n  const { title, notes, due } = useSelector(todoTaskSelector(uuid)) || {};\n  const currentTaskList = useSelector(currentTaskListsSelector);\n\n  return (\n    <FullScreenDialog\n      {...props}\n      className=\"todo-task-details\"\n      open={!shouldBeDeleted && open}\n      onClose={onClose}\n      headerComponents={\n        <IconButton\n          tooltip=\"Delete\"\n          icon={DeleteIcon}\n          onClick={deleteOnExited}\n        />\n      }\n      onExited={() => {\n        if (shouldBeDeleted) {\n          deleteTask({ uuid });\n        } else if (moveTo && moveTo.id !== currentTaskList.id) {\n          moveToAnotherList({ tasklistId: moveTo.id, uuid });\n        }\n      }}\n    >\n      <Input\n        multiline\n        autoFocus\n        className=\"filled todo-task-details-title-field\"\n        placeholder=\"Enter title\"\n        onKeyPress={preventStartNewLine}\n        value={title}\n        onChange={event =>\n          updateTask({ uuid, title: event.currentTarget.value })\n        }\n      />\n\n      <Input\n        multiline\n        rows={3}\n        rowsMax={Infinity}\n        value={notes}\n        onChange={event =>\n          updateTask({ uuid, notes: event.currentTarget.value })\n        }\n        className=\"filled todo-task-details-notes-field\"\n        placeholder=\"Add details\"\n      />\n\n      <div className=\"row row-task-list\">\n        <FormatListBulletedIcon />\n        <TaskListDropdown\n          buttonProps={dropdownButtonProps}\n          defaultOpen={taskListDropdownOpened}\n          onSelect={setMoveTo}\n          taskList={moveTo}\n          paperClassName=\"details-task-list-dropdown-paper\"\n        />\n      </div>\n\n      <div className=\"row row-date\">\n        <EventAvailableIcon />\n        <DateTimeButton\n          date={due ? new Date(due) : undefined}\n          onClick={openDateTimeDialog}\n          onRemove={() => updateTask({ uuid, due: null })}\n        />\n      </div>\n    </FullScreenDialog>\n  );\n}\n"
  },
  {
    "path": "src/pages/TaskList/Task/TodoTaskDetails/index.ts",
    "content": "import './TodoTaskDetails.scss';\n\nexport * from './TodoTaskDetails';\nexport { TodoTaskDetails as default } from './TodoTaskDetails';\n"
  },
  {
    "path": "src/pages/TaskList/Task/ToggleCompleted.tsx",
    "content": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport { IconButton } from '../../../components/Mui';\nimport { useTaskActions, taskSelector } from '../../../store';\nimport { Schema$Task } from '../../../typings';\nimport CircleIcon from '@material-ui/icons/RadioButtonUnchecked';\nimport TickIcon from '@material-ui/icons/Check';\n\ninterface Props extends Pick<Schema$Task, 'uuid'> {\n  isEmpty: boolean;\n}\n\nconst MarkCompleteButton = React.memo(() => (\n  <IconButton tooltip=\"Mark incomplete\">\n    <CircleIcon />\n  </IconButton>\n));\n\nconst MarkInCompleteButton = React.memo(() => (\n  <IconButton tooltip=\"Mark complete\">\n    <TickIcon className=\"mui-tick-icon\" />\n  </IconButton>\n));\n\nexport function ToggleCompleted({ uuid, isEmpty }: Props) {\n  const { update: updateTask, deleteTask } = useTaskActions();\n  const { status, hidden } = useSelector(taskSelector(uuid)) || {};\n  const isCompleted = hidden || status === 'completed';\n\n  return (\n    <div\n      className=\"toggle-completed\"\n      onClick={() =>\n        isEmpty\n          ? deleteTask({ uuid })\n          : updateTask({\n              uuid,\n              hidden: !isCompleted,\n              status: isCompleted ? 'needsAction' : 'completed'\n            })\n      }\n    >\n      {!isCompleted && <MarkCompleteButton />}\n      <MarkInCompleteButton />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/pages/TaskList/Task/index.ts",
    "content": "import './Task.scss';\n\nexport * from './Task';\nexport * from './TodoTask';\nexport * from './CompletedTask';\nexport { Task as default } from './Task';\n"
  },
  {
    "path": "src/pages/TaskList/TaskList.scss",
    "content": ".task-list {\n  @include dimen(100%, 100vh);\n  @include flex($flex-direction: column);\n\n  &.disabled {\n    opacity: 0.3;\n\n    &:after {\n      @include fixed(0, null, 0);\n      @include sq-dimen(100%);\n      content: '';\n      user-select: none;\n      z-index: 100000;\n    }\n  }\n\n  .task-list-header,\n  .new-task {\n    flex: 0 0 auto;\n  }\n}\n\n.task-list-content {\n  @include sq-dimen(100%);\n  @include flex($flex-direction: column);\n  max-height: calc(100vh - var(--header-height));\n  overflow: hidden;\n  z-index: 1;\n\n  > .scroll-content {\n    @include sq-dimen(100%);\n    flex: 1 1 auto;\n    overflow: auto;\n  }\n}\n"
  },
  {
    "path": "src/pages/TaskList/TaskList.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { useSelector } from 'react-redux';\nimport { TaskListHeader } from './TaskListHeader';\nimport { TodoTaskList } from './TodoTaskList';\nimport { NewTask } from './NewTask';\nimport { CompletedTaskList } from './CompletedTaskList';\nimport { TodoTaskDetailsProvider } from './Task/TodoTaskDetails';\nimport { DateTimeDialogProvider } from './Task/DateTimeDialog';\nimport { TodoTaskMenuProvider } from './Task/TodoTask/TodoTaskMenu';\nimport {\n  useTaskListActions,\n  useTaskActions,\n  RootState,\n  currentTaskListsSelector\n} from '../../store';\n\nexport function TaskList() {\n  const taskListActions = useTaskListActions();\n  const taskActions = useTaskActions();\n  const currentTasklist = useSelector(currentTaskListsSelector);\n\n  const taskListId = currentTasklist && currentTasklist.id;\n  const disabled = useSelector(\n    (state: RootState) => state.task.loading || state.taskList.loading\n  );\n\n  useEffect(() => {\n    taskListActions.getTaskLists();\n  }, [taskListActions]);\n\n  useEffect(() => {\n    if (taskListId) {\n      taskActions.getTasks({ tasklist: taskListId });\n    }\n  }, [taskActions, taskListId]);\n\n  return (\n    <div className={[`task-list`, disabled ? 'disabled' : ''].join(' ').trim()}>\n      <TaskListHeader onConfirm={taskListActions.newTaskList} />\n      <TodoTaskDetailsProvider>\n        <DateTimeDialogProvider>\n          <TodoTaskMenuProvider>\n            <div className=\"task-list-content\">\n              <NewTask />\n              <div className=\"scroll-content\">\n                <TodoTaskList taskListId={taskListId} />\n              </div>\n              <CompletedTaskList key={taskListId} />\n            </div>\n          </TodoTaskMenuProvider>\n        </DateTimeDialogProvider>\n      </TodoTaskDetailsProvider>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/pages/TaskList/TaskListDropdown/TaskListDropdown.scss",
    "content": "$button-height: 26px;\n$item-height: 40px;\n\n.task-list-header-dropdown-container {\n  @include app-region-no-drag;\n  overflow: hidden;\n\n  .task-list-header-dropdown-label {\n    color: #80868b;\n    font-size: 10px;\n    letter-spacing: 1.5px;\n    margin-bottom: 2px;\n    text-transform: uppercase;\n    text-align: right;\n    padding-right: 6px;\n  }\n\n  .mui-dropdown-button {\n    @include dimen(100%, $button-height);\n    @include typeface('Nunito Sans', 600);\n    color: var(--text-color);\n    font-size: 15px;\n\n    &:not(:hover) {\n      background-color: transparent;\n    }\n  }\n}\n\n.dropdown-menu-paper.task-list-header-dropdown-paper {\n  @include dimen(auto);\n  margin-top: $button-height;\n\n  .scroll-content {\n    max-height: $item-height * 5;\n    min-width: 180px;\n    overflow: auto;\n  }\n\n  .mui-menu-item {\n    @include typeface('Nunito Sans', 500);\n    letter-spacing: 0.2px;\n    height: $item-height;\n  }\n}\n"
  },
  {
    "path": "src/pages/TaskList/TaskListDropdown/TaskListDropdown.tsx",
    "content": "import React, { ReactNode, useEffect, useRef } from 'react';\nimport { useSelector } from 'react-redux';\nimport {\n  useMuiMenu,\n  Dropdown,\n  DropdownProps,\n  FULLSCREEN_DIALOG_TRANSITION\n} from '../../../components/Mui';\nimport { TaskListDropdownItem } from './TaskListDropdownItem';\nimport { taskListIdsSelector, currentTaskListsSelector } from '../../../store';\nimport { Schema$TaskList } from '../../../typings';\n\nexport interface TaskListDropdownProps\n  extends Omit<Partial<DropdownProps>, 'onSelect'> {\n  defaultOpen?: boolean;\n  paperClassName?: string;\n  footer?(onClose: () => void): ReactNode;\n  onSelect(taskList: Schema$TaskList): void;\n  taskList?: Schema$TaskList;\n}\n\nexport function TaskListDropdown({\n  children,\n  onSelect,\n  defaultOpen,\n  footer,\n  paperClassName,\n  PaperProps,\n  taskList: controlled,\n  ...props\n}: TaskListDropdownProps) {\n  const { anchorEl, setAnchorEl, onClose } = useMuiMenu();\n  const ids = useSelector(taskListIdsSelector);\n  const currentTaskList = useSelector(currentTaskListsSelector);\n  const taskList = controlled || currentTaskList;\n  const dropdownRef = useRef<HTMLButtonElement>(null);\n\n  useEffect(() => {\n    const el = dropdownRef.current;\n    if (defaultOpen && el) {\n      setTimeout(() => setAnchorEl(el), FULLSCREEN_DIALOG_TRANSITION / 2);\n    }\n  }, [setAnchorEl, defaultOpen]);\n\n  return (\n    <Dropdown\n      {...props}\n      ref={dropdownRef}\n      PaperProps={{ className: paperClassName }}\n      label={(taskList && taskList.title) || 'Loading...'}\n      open={!!anchorEl}\n      anchorEl={anchorEl}\n      onClick={setAnchorEl}\n      onClose={onClose}\n      onEnter={el => {\n        const scroller = el.querySelector<HTMLDivElement>('.scroll-content');\n        const item = el.querySelector<SVGElement>('svg')!.parentElement!\n          .offsetParent as HTMLElement;\n        if (scroller && item) {\n          scroller.scrollTop = item.offsetTop - 10 - item.offsetHeight * 2; // 10px padding;\n        }\n      }}\n      footer={footer && footer(onClose)}\n    >\n      {ids.map(id => {\n        return (\n          <TaskListDropdownItem\n            id={id}\n            key={id}\n            onClick={onSelect}\n            onClose={onClose}\n            selected={taskList && taskList.id === id}\n          />\n        );\n      })}\n    </Dropdown>\n  );\n}\n"
  },
  {
    "path": "src/pages/TaskList/TaskListDropdown/TaskListDropdownItem.tsx",
    "content": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport { MenuItem, MenuItemProps } from '../../../components/Mui';\nimport { Schema$TaskList } from '../../../typings';\nimport { taskListsSelector } from '../../../store';\n\ninterface Props extends Omit<MenuItemProps, 'onClick'> {\n  id: string;\n  onClose(): void;\n  onClick(taskList: Schema$TaskList): void;\n}\n\nexport function TaskListDropdownItem({\n  id,\n  onClick,\n  onClose,\n  ...props\n}: Props) {\n  const taskList = useSelector(taskListsSelector(id))!;\n\n  return (\n    <MenuItem\n      {...props}\n      text={taskList.title || ''}\n      onClose={onClose}\n      onClick={() => onClick(taskList)}\n    />\n  );\n}\n"
  },
  {
    "path": "src/pages/TaskList/TaskListDropdown/index.ts",
    "content": "import './TaskListDropdown.scss';\n\nexport * from './TaskListDropdown';\nexport { TaskListDropdown as default } from './TaskListDropdown';\n"
  },
  {
    "path": "src/pages/TaskList/TaskListHeader/TaskListHeader.scss",
    "content": ".task-list-header {\n  @include dimen(100%, var(--header-height));\n  @include flex(flex-end, space-between);\n  @include fake-border($borderWidth: 1px, $color: var(--main-color-diff));\n  @include padding-x($padding-x);\n  @include relative();\n\n  padding-bottom: 8px;\n}\n"
  },
  {
    "path": "src/pages/TaskList/TaskListHeader/TaskListHeader.tsx",
    "content": "import React from 'react';\nimport { generatePath } from 'react-router-dom';\nimport { FormDialog, MenuItem } from '../../../components/Mui';\nimport { TaskListDropdown } from '../TaskListDropdown';\nimport { history, currentTaskListsSelector } from '../../../store';\nimport { PATHS } from '../../../constants';\nimport { useBoolean } from '../../../hooks/useBoolean';\nimport { Divider } from '@material-ui/core';\nimport { useSelector } from 'react-redux';\n\ninterface Props {\n  onConfirm: (payload: string) => void;\n}\n\nexport function TaskListHeader({ onConfirm }: Props) {\n  const [dialogOpened, openDialog, closeDialog] = useBoolean();\n  const taskList = useSelector(currentTaskListsSelector);\n\n  return (\n    <>\n      <div className=\"task-list-header\">\n        <div className=\"status\" /> {/* TODO: */}\n        <div className=\"task-list-header-dropdown-container\">\n          <div className=\"task-list-header-dropdown-label\">\n            <span>TASKS</span>\n          </div>\n          <TaskListDropdown\n            paperClassName=\"task-list-header-dropdown-paper\"\n            taskList={taskList}\n            onSelect={({ id }) =>\n              history.push(generatePath(PATHS.TASKLIST, { taskListId: id }))\n            }\n            footer={onClose => (\n              <>\n                <Divider />\n                <MenuItem\n                  text=\"Create new list\"\n                  onClose={onClose}\n                  onClick={openDialog}\n                />\n              </>\n            )}\n          />\n        </div>\n      </div>\n      <FormDialog\n        title=\"Create new list\"\n        errorMsg=\"Task list name cannot be empty\"\n        open={dialogOpened}\n        onClose={closeDialog}\n        onConfirm={onConfirm}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "src/pages/TaskList/TaskListHeader/index.ts",
    "content": "import './TaskListHeader.scss';\n\nexport * from './TaskListHeader';\nexport { TaskListHeader as default } from './TaskListHeader';\n"
  },
  {
    "path": "src/pages/TaskList/TaskListMenu.tsx",
    "content": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport { Divider } from '@material-ui/core';\nimport {\n  Menu,\n  MenuProps,\n  useMuiMenuItem,\n  FormDialog,\n  ConfirmDialog\n} from '../../components/Mui';\nimport { Preferences } from '../../components/Preferences';\nimport { KeyboardShortcuts } from '../../components/KeyboardShortcuts';\nimport {\n  useTaskActions,\n  isMasterTaskListSelector,\n  RootState,\n  useTaskListActions,\n  currentTaskListsSelector,\n  isSortByDateSelector,\n  useAuthActions\n} from '../../store';\nimport { useBoolean } from '../../hooks/useBoolean';\n\ninterface Props extends Omit<MenuProps, 'ref'> {}\n\nconst menuClasses = { paper: 'task-list-menu-paper' };\n\nfunction selector(state: RootState) {\n  const completedTasks = state.task.completed.ids.length;\n  return {\n    isMasterTaskList: isMasterTaskListSelector(state),\n    totalTasks: completedTasks + state.task.todo.ids.length,\n    completedTasks,\n    currentTaskList: currentTaskListsSelector(state)\n  };\n}\n\nexport function TaskListMenu({ onClose, ...props }: Props) {\n  const MenuItem = useMuiMenuItem({ onClose });\n  const taskListActions = useTaskListActions();\n  const { deleteAllCompletedTasks } = useTaskActions();\n  const {\n    isMasterTaskList,\n    totalTasks,\n    completedTasks,\n    currentTaskList\n  } = useSelector(selector);\n\n  const { title: currentTaskListTitle, id: currentTaskListId } =\n    currentTaskList || {};\n\n  const [\n    deleteCompletedTaskDialogOpend,\n    openDeleteCompletedTaskDialog,\n    closeDeleteCompletedTaskDialog\n  ] = useBoolean();\n\n  const [\n    deleteTaskListDialogOpend,\n    openDeleteTaskListDialog,\n    closeDeleteTaskListDialog\n  ] = useBoolean();\n\n  const [\n    renameTaskDialogOpend,\n    openRenameTaskDialog,\n    closeRenameTaskDialog\n  ] = useBoolean();\n\n  const [\n    keyboardShortcutsOpened,\n    openKeyboardShortcuts,\n    closeKeyboardShortcuts\n  ] = useBoolean();\n\n  const [preferencesOpened, openPreferences, closePrefences] = useBoolean();\n\n  const isSoryByDate = useSelector(\n    isSortByDateSelector(currentTaskListId || '')\n  );\n  const sortTaskListBy = (orderType: 'date' | 'order') => {\n    currentTaskListId &&\n      taskListActions.sortTaskListBy({ id: currentTaskListId, orderType });\n  };\n\n  const { logout } = useAuthActions();\n\n  return (\n    <>\n      <Menu {...props} onClose={onClose} classes={menuClasses}>\n        <div className=\"task-list-menu-title\">Sort by</div>\n        <MenuItem\n          text=\"My order\"\n          selected={!isSoryByDate}\n          onClick={() => sortTaskListBy('order')}\n        />\n        <MenuItem\n          text=\"Date\"\n          selected={isSoryByDate}\n          onClick={() => sortTaskListBy('date')}\n        />\n        <Divider />\n        <MenuItem text=\"Rename list\" onClick={openRenameTaskDialog} />\n        <MenuItem\n          text=\"Delete list\"\n          disabled={isMasterTaskList}\n          onClick={\n            totalTasks === 0\n              ? taskListActions.deleteCurrTaskList\n              : openDeleteTaskListDialog\n          }\n        />\n        <MenuItem\n          text=\"Delete all completed tasks\"\n          onClick={openDeleteCompletedTaskDialog}\n          disabled={completedTasks === 0}\n        />\n        <Divider />\n        <MenuItem text=\"Keyboard shortcuts\" onClick={openKeyboardShortcuts} />\n        <MenuItem text=\"Preferences\" onClick={openPreferences} />\n        <MenuItem text=\"Logout\" onClick={logout} />\n      </Menu>\n      <FormDialog\n        title=\"Rename list\"\n        errorMsg=\"Task list name cannot be empty\"\n        defaultValue={currentTaskListTitle || ''}\n        open={renameTaskDialogOpend}\n        onClose={closeRenameTaskDialog}\n        onConfirm={title =>\n          currentTaskListId &&\n          taskListActions.update({ id: currentTaskListId, title })\n        }\n      />\n      <ConfirmDialog\n        title=\"Delete this list?\"\n        confirmLabel=\"Delete\"\n        open={deleteTaskListDialogOpend}\n        onClose={closeDeleteTaskListDialog}\n        onConfirm={taskListActions.deleteCurrTaskList}\n      >\n        Deleting this list will also delete {totalTasks} task.\n      </ConfirmDialog>\n      <ConfirmDialog\n        title=\"Delete all completed tasks?\"\n        confirmLabel=\"Delete\"\n        open={deleteCompletedTaskDialogOpend}\n        onClose={closeDeleteCompletedTaskDialog}\n        onConfirm={deleteAllCompletedTasks}\n      >\n        {completedTasks} completed task will be permanently removed unless it\n        repeats.\n      </ConfirmDialog>\n\n      <Preferences open={preferencesOpened} onClose={closePrefences} />\n\n      <KeyboardShortcuts\n        open={keyboardShortcutsOpened}\n        onClose={closeKeyboardShortcuts}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "src/pages/TaskList/TodoTaskList/TodoTaskList.scss",
    "content": ".todo-tasks-list-by-date {\n  .todo-task {\n    .task-due-date-button {\n      display: none;\n    }\n  }\n\n  .date-label {\n    @include dimen(100%, 40px);\n    @include flex(center);\n    @include padding-x(20px);\n\n    color: var(--text-color);\n\n    &:after {\n      @include typeface('Nunito Sans', 700);\n      content: attr(data-label);\n    }\n\n    &[data-label^='Past'] {\n      color: var(--error-color);\n    }\n\n    &[data-label^='Today'] {\n      color: var(--accent-color);\n    }\n\n    & + .task .task-input-base:before {\n      border-top-color: transparent;\n    }\n  }\n}\n"
  },
  {
    "path": "src/pages/TaskList/TodoTaskList/TodoTaskList.tsx",
    "content": "import React, { useState } from 'react';\nimport { useSelector } from 'react-redux';\nimport { SortableContainer, SortableElement } from 'react-sortable-hoc';\nimport { TodoTask, TodoTaskProps } from '../Task';\nimport { TodoTaskListByDate } from './TodoTaskListByDate';\nimport {\n  useTaskActions,\n  todoTaskIdsSelector,\n  isSortByDateSelector\n} from '../../../store';\nimport { useBoolean } from '../../../hooks/useBoolean';\n\ninterface InsertAfter {\n  insertAfter?: number;\n}\n\ninterface SortableListProps extends InsertAfter {\n  dragging?: boolean;\n  todoTasks: Array<TodoTaskProps['uuid']>;\n}\n\nconst SortableItem = SortableElement(\n  ({ sortIndex, ...props }: TodoTaskProps & { sortIndex: number }) => (\n    <TodoTask {...props} index={sortIndex} />\n  )\n);\n\nconst SortableList = SortableContainer(\n  ({ dragging, todoTasks, insertAfter }: SortableListProps) => {\n    return (\n      <div\n        className=\"todo-tasks-list\"\n        style={{ pointerEvents: dragging ? 'none' : 'auto' }}\n      >\n        {todoTasks.map((uuid, index) => (\n          <SortableItem\n            className={\n              dragging && insertAfter === index ? 'highlight-bottom-border' : ''\n            }\n            key={uuid}\n            uuid={uuid}\n            sortIndex={index}\n            index={index}\n          />\n        ))}\n      </div>\n    );\n  }\n);\n\nfunction TodoTaskListByOrder() {\n  const tasks = useSelector(todoTaskIdsSelector);\n  const [dragging, dragStart, dragEnd] = useBoolean();\n  const [insertAfter, setInsertAfter] = useState<number>();\n  const { moveTask } = useTaskActions();\n\n  return (\n    <SortableList\n      lockAxis=\"y\"\n      helperClass=\"dragging\"\n      distance={10}\n      dragging={dragging}\n      insertAfter={insertAfter}\n      todoTasks={tasks}\n      onSortMove={dragStart}\n      onSortOver={({ newIndex, oldIndex, index }) => {\n        let insertAfter;\n        const move = newIndex - oldIndex > 0 ? 'down' : 'up';\n\n        if (move === 'down') {\n          insertAfter = newIndex > index ? oldIndex + 1 : oldIndex;\n        } else if (move === 'up') {\n          insertAfter = newIndex > index ? newIndex : newIndex - 1;\n        }\n\n        setInsertAfter(insertAfter);\n      }}\n      onSortEnd={({ newIndex, oldIndex }) => {\n        if (newIndex !== oldIndex) {\n          moveTask({ uuid: tasks[oldIndex], from: oldIndex, to: newIndex });\n        }\n        dragEnd();\n      }}\n    />\n  );\n}\n\nexport function TodoTaskList({ taskListId = '' }: { taskListId?: string }) {\n  const sortByDate = useSelector(isSortByDateSelector(taskListId));\n  return (\n    <div className=\"todo-tasks-list\">\n      {sortByDate ? <TodoTaskListByDate /> : <TodoTaskListByOrder />}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/pages/TaskList/TodoTaskList/TodoTaskListByDate.tsx",
    "content": "import React, { Fragment } from 'react';\nimport { useSelector } from 'react-redux';\nimport { TodoTask } from '../Task';\nimport {\n  todoTasksIdsByDateSelector,\n  todoTaskIdsSelector\n} from '../../../store';\nimport { Schema$Task } from '../../../typings';\n\nconst inherit: (keyof Schema$Task)[] = ['due'];\n\nexport function TodoTaskListByDate() {\n  const { order, tasks } = useSelector(todoTasksIdsByDateSelector);\n  const todo = useSelector(todoTaskIdsSelector);\n  let prevDue: string | null | undefined;\n\n  return (\n    <div className=\"todo-tasks-list-by-date\">\n      {tasks.map(([date, ids]) => (\n        <Fragment key={date}>\n          <div className=\"date-label\" data-label={date} />\n          {ids.map(({ uuid, due }) => {\n            const _prevDue = prevDue;\n            const idx = order.indexOf(uuid);\n            const [prev, index, next] = [\n              order[idx - 1],\n              uuid,\n              order[idx + 1]\n            ].map(uuid => todo.indexOf(uuid));\n            prevDue = due;\n\n            return (\n              <TodoTask\n                key={uuid}\n                uuid={uuid}\n                index={index}\n                prevIndex={prev}\n                nextIndex={next === -1 ? index : next}\n                inherit={date !== 'Past' ? inherit : undefined}\n                prevDue={_prevDue}\n                sortByDate\n              />\n            );\n          })}\n        </Fragment>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/pages/TaskList/TodoTaskList/index.ts",
    "content": "import './TodoTaskList.scss';\n\nexport * from './TodoTaskList';\nexport { TodoTaskList as default } from './TodoTaskList';\n"
  },
  {
    "path": "src/pages/TaskList/index.ts",
    "content": "import './TaskList.scss';\n\nexport * from './TaskList';\nexport { TaskList as default } from './TaskList';\n"
  },
  {
    "path": "src/react-app-env.d.ts",
    "content": "/// <reference types=\"react-scripts\" />\n\ndeclare module '*.scss';\ndeclare module 'react-desktop/windows';\n\ndeclare interface Window {\n  __REDUX_DEVTOOLS_EXTENSION__: any;\n  __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any;\n}\n\ndeclare namespace NodeJS {\n  interface Module {\n    hot?: { accept: (path?: string, callback?: () => void) => void };\n  }\n}\n\ndeclare const process: any;\ndeclare const require: any;\n"
  },
  {
    "path": "src/scss/_functions.scss",
    "content": "@function str-replace($string, $search, $replace: '') {\n  $index: str-index($string, $search);\n\n  @if ($index) {\n    @return str-slice($string, 1, $index - 1) + $replace +\n      str-replace(\n        str-slice($string, $index + str-length($search)),\n        $search,\n        $replace\n      );\n  }\n\n  @return $string;\n}\n\n@function to-string($value) {\n  @return inspect($value);\n}\n\n@function list-to-string($list, $separator: ',') {\n  $string: '';\n  @if (type-of($list) != list) {\n    @error 'Hey please give me a list instead!';\n  }\n  @each $item in $list {\n    @if (index($list, $item) ==1) {\n      $string: nth($list, 1);\n    } @else {\n      $string: #{$string}#{$separator}#{$item};\n    }\n  }\n\n  @return $string;\n}\n\n@function strip-unit($number) {\n  @if type-of($number) == 'number' and not unitless($number) {\n    @return $number / ($number * 0 + 1);\n  }\n\n  @return $number;\n}\n\n@function getColor($color, $tone: primary) {\n  @return map-get(map-get($accent-colors, $color), $tone);\n}\n"
  },
  {
    "path": "src/scss/_mixins.scss",
    "content": "@import './mixins/animation';\n@import './mixins/background';\n@import './mixins/border';\n@import './mixins/electron';\n@import './mixins/flex';\n@import './mixins/font';\n@import './mixins/position';\n@import './mixins/size';\n@import './mixins/textHighlight';\n@import './mixins/textOverflow';\n\n@mixin scrollbar {\n  ::-webkit-scrollbar {\n    width: 0.8rem;\n  }\n\n  ::-webkit-scrollbar-thumb {\n    background-clip: padding-box;\n    background-color: rgba(128, 128, 128, 0.4);\n    border: 2px solid transparent;\n    border-radius: 0.8rem;\n    box-shadow: inset -1px -1px 0 rgba(0, 0, 0, 0.05),\n      inset 1px 1px 0 rgba(0, 0, 0, 0.05);\n  }\n}\n"
  },
  {
    "path": "src/scss/_platform.scss",
    "content": ":root {\n  --header-height: 70px;\n}\n\n[data-title-bar^='native']:not([data-platform^='darwin']) {\n  --header-height: 60px;\n}\n\n[data-platform^='win32'][data-title-bar^='native'] {\n  --header-height: 90px;\n}\n"
  },
  {
    "path": "src/scss/_theme.scss",
    "content": ":root {\n  --main-color: #fff;\n  --main-color-diff: #f1f3f4;\n  --main-color-diff2: #fff;\n  --accent-color: #{getColor(blue)};\n  --accent-light-color: #{getColor(blue, light)};\n  --accent-dark-color: #{getColor(blue, dark)};\n  --text-color: #202124;\n  --text-secondary-color: #5f6368;\n  --border-color: #e0e0e0;\n  --task-highlight-background-color: #f8f9fa;\n  --day-color: #9e9e9e;\n  --date-color: #000;\n  --error-color: #da3125;\n  --shadow-color: #e6e6e6;\n  --paper-background-color: #fff;\n  --menu-item-hover-color: #{darken(#fff, 6%)};\n}\n\n$darkMain: #222;\n\n[data-theme^='dark'] {\n  --main-color: #{$darkMain};\n  --main-color-diff: #{lighten($darkMain, 5%)};\n  --main-color-diff2: #{lighten($darkMain, 10%)};\n  --text-color: #dfdedb;\n  --text-secondary-color: #a09c97;\n  --border-color: #313030;\n  --task-highlight-background-color: #333;\n  --day-color: #9e9e9e;\n  --date-color: #fff;\n  --error-color: #f44336;\n  --shadow-color: #161616;\n  --paper-background-color: var(--main-color-diff);\n  --menu-item-hover-color: #444;\n}\n\n@each $name, $colors in $accent-colors {\n  [data-accent-color^='#{\"\" + $name}'] {\n    --accent-color: #{map-get($colors, primary)};\n    --accent-light-color: #{map-get($colors, light)};\n    --accent-dark-color: #{map-get($colors, dark)};\n  }\n}\n"
  },
  {
    "path": "src/scss/_variables.scss",
    "content": "$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  red: #ef5350,\n  blue: #4285f4,\n  amber: #fb0,\n  green: #66bb6a,\n  purple: #ab47bc,\n  grey: #bdbdbd\n);\n\n$accent-colors: ();\n\n@each $name, $color in $base-colors {\n  $accent-colors: map-merge(\n    $accent-colors,\n    (\n      $name: (\n        primary: $color,\n        light: lighten($color, 30%),\n        dark: darken($color, 5%)\n      )\n    )\n  );\n}\n"
  },
  {
    "path": "src/scss/index.scss",
    "content": "@import './variables';\n@import './functions';\n@import './mixins';\n@import './theme';\n@import './platform';\n"
  },
  {
    "path": "src/scss/mixins/_animation.scss",
    "content": "@mixin animate($property) {\n  transition-duration: 0.3s;\n  transition-property: $property;\n  transition-timing-function: ease;\n  will-change: $property;\n}\n"
  },
  {
    "path": "src/scss/mixins/_background.scss",
    "content": "@mixin bg-image(\n  $image: null,\n  $size: 100%,\n  $position: center center,\n  $repeat: no-repeat\n) {\n  @if ($image) {\n    background-image: $image;\n  }\n\n  @if ($position) {\n    background-position: $position;\n  }\n\n  @if ($repeat) {\n    background-repeat: $repeat;\n  }\n\n  @if ($size) {\n    background-size: $size;\n  }\n}\n"
  },
  {
    "path": "src/scss/mixins/_border.scss",
    "content": "@mixin fake-border(\n  $position: bottom,\n  $borderWidth: 1px,\n  $color: #000,\n  $width: 100%\n) {\n  background: linear-gradient(to left, $color 0, $color 100%);\n  background-size: $width $borderWidth;\n  background-repeat: no-repeat;\n  background-position: center $position;\n}\n"
  },
  {
    "path": "src/scss/mixins/_electron.scss",
    "content": "@mixin app-region-drag {\n  -webkit-app-region: drag;\n}\n\n@mixin app-region-no-drag {\n  -webkit-app-region: no-drag !important;\n}\n"
  },
  {
    "path": "src/scss/mixins/_flex.scss",
    "content": "@mixin flex(\n  $align-items: stretch,\n  $justify-content: flex-start,\n  $flex-direction: row,\n  $wrap: nowrap,\n  $is-inline: false\n) {\n  align-items: $align-items;\n  display: if($is-inline, inline-flex, flex);\n  flex-direction: $flex-direction;\n  flex-wrap: $wrap;\n  justify-content: $justify-content;\n}\n"
  },
  {
    "path": "src/scss/mixins/_font.scss",
    "content": "@mixin typeface($font: 'Roboto', $font-weight: normal) {\n  font-family: $font, Helvetica, Arial, 微軟正黑體, Microsoft JhengHei,\n    sans-serif;\n  font-weight: $font-weight;\n}\n"
  },
  {
    "path": "src/scss/mixins/_position.scss",
    "content": "@mixin position(\n  $top: null,\n  $bottom: null,\n  $left: null,\n  $right: null,\n  $position: absolute\n) {\n  position: $position;\n\n  @if ($top) {\n    top: $top;\n  }\n\n  @if ($bottom) {\n    bottom: $bottom;\n  }\n\n  @if ($left) {\n    left: $left;\n  }\n\n  @if ($right) {\n    right: $right;\n  }\n}\n\n@mixin absolute($top: null, $bottom: null, $left: null, $right: null) {\n  @include position($top, $bottom, $left, $right, absolute);\n}\n\n@mixin relative($top: null, $bottom: null, $left: null, $right: null) {\n  @include position($top, $bottom, $left, $right, relative);\n}\n\n@mixin fixed($top: null, $bottom: null, $left: null, $right: null) {\n  @include position($top, $bottom, $left, $right, fixed);\n}\n"
  },
  {
    "path": "src/scss/mixins/_size.scss",
    "content": "@mixin dimen($width: null, $height: null) {\n  @if ($width) {\n    width: $width;\n  }\n\n  @if ($height) {\n    height: $height;\n  }\n}\n\n@mixin sq-dimen($length: null) {\n  @if ($length) {\n    @include dimen($length, $length);\n  }\n}\n\n@mixin margin-x($margin-sizes) {\n  @if (length($margin-sizes) == 1) {\n    margin-left: $margin-sizes;\n    margin-right: $margin-sizes;\n  } @else {\n    margin-left: nth($margin-sizes, 1);\n    margin-right: nth($margin-sizes, 2);\n  }\n}\n\n@mixin margin-y($margin-sizes) {\n  @if (length($margin-sizes) == 1) {\n    margin-top: $margin-sizes;\n    margin-bottom: $margin-sizes;\n  } @else {\n    margin-top: nth($margin-sizes, 1);\n    margin-bottom: nth($margin-sizes, 2);\n  }\n}\n\n@mixin padding-x($padding-sizes) {\n  @if (length($padding-sizes) == 1) {\n    padding-left: $padding-sizes;\n    padding-right: $padding-sizes;\n  } @else {\n    padding-left: nth($padding-sizes, 1);\n    padding-right: nth($padding-sizes, 2);\n  }\n}\n\n@mixin padding-y($padding-sizes) {\n  @if (length($padding-sizes) == 1) {\n    padding-top: $padding-sizes;\n    padding-bottom: $padding-sizes;\n  } @else {\n    padding-top: nth($padding-sizes, 1);\n    padding-bottom: nth($padding-sizes, 2);\n  }\n}\n"
  },
  {
    "path": "src/scss/mixins/_textHighlight.scss",
    "content": "$text-highlight-z-index: 11;\n\n@mixin textHighlight() {\n  @include relative();\n\n  &:after {\n    @include absolute(null, -1px, 0, 0);\n\n    border-bottom: 2px solid transparent;\n    content: '';\n    margin: auto;\n    z-index: $text-highlight-z-index;\n  }\n\n  &.focused {\n    &:after {\n      animation: expand 0.4s ease 1;\n      border-color: var(--accent-color);\n      will-change: width;\n    }\n  }\n}\n\n@keyframes expand {\n  0% {\n    width: 0;\n  }\n  100% {\n    width: 100%;\n  }\n}\n"
  },
  {
    "path": "src/scss/mixins/_textOverflow.scss",
    "content": "@mixin text-overflow-ellipsis() {\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n}\n\n@mixin multi-line-ellipsis($line-height: 1.2em, $line-clamp: 2) {\n  line-height: $line-height;\n  max-height: calc(#{$line-height * $line-clamp} - 0.1em);\n  display: -webkit-box;\n  overflow: hidden;\n  -webkit-line-clamp: $line-clamp;\n  -webkit-box-orient: vertical;\n}\n"
  },
  {
    "path": "src/service/auth.ts",
    "content": "import { google } from 'googleapis';\n\nconst { oAuth2Storage, tokenStorage } = window;\n\nexport let OAuth2Keys = oAuth2Storage.get();\nexport let oAuth2Client = OAuth2Keys\n  ? new google.auth.OAuth2(\n      OAuth2Keys.installed.client_id,\n      OAuth2Keys.installed.client_secret,\n      OAuth2Keys.installed.redirect_uris[0]\n    )\n  : undefined;\n\nconst SCOPES = ['https://www.googleapis.com/auth/tasks'];\n\nauthenticate();\n\nexport const { tasks: tasksAPI, tasklists: taskListAPI } = google.tasks({\n  version: 'v1',\n  auth: oAuth2Client\n});\n\nexport function generateAuthUrl() {\n  if (oAuth2Client) {\n    const authorizeUrl = oAuth2Client.generateAuthUrl({\n      access_type: 'offline',\n      scope: SCOPES\n    });\n    window.openExternal(authorizeUrl);\n  }\n}\n\nexport function authenticate() {\n  const token = tokenStorage.get();\n  if (oAuth2Client && token) {\n    oAuth2Client.setCredentials(token);\n  }\n}\n\nexport async function getToken(code: string) {\n  if (oAuth2Client) {\n    try {\n      const { tokens } = await oAuth2Client.getToken(code);\n      oAuth2Client.setCredentials(tokens);\n      tokenStorage.save(tokens);\n      return tokens;\n    } catch (err) {\n      return Promise.reject(err);\n    }\n  }\n\n  return Promise.reject();\n}\n"
  },
  {
    "path": "src/service/index.ts",
    "content": "export * from './auth';\nexport * from './task';\nexport * from './tasksList';\n"
  },
  {
    "path": "src/service/task.ts",
    "content": "import { Observable, defer, of } from 'rxjs';\nimport { mergeMap } from 'rxjs/operators';\nimport { google, tasks_v1 } from 'googleapis';\nimport { oAuth2Client } from './auth';\nimport { Schema$Task } from '../typings';\nimport { UUID } from '../utils/uuid';\nimport { PaginatePayload } from '../hooks/crud-reducer';\n\nconst { tasks } = google.tasks({\n  version: 'v1',\n  auth: oAuth2Client\n});\n\nexport const taskUUID = new UUID();\n\n// https://developers.google.com/tasks/performance#partial\nconst fields: Record<keyof Omit<Schema$Task, 'uuid'>, unknown> = {\n  completed: '',\n  hidden: '',\n  id: '',\n  notes: '',\n  position: '',\n  status: '',\n  title: '',\n  due: ''\n};\n\nexport function getAllTasks(\n  params: tasks_v1.Params$Resource$Tasks$List,\n  prevTasks: Schema$Task[] = []\n): Observable<PaginatePayload<Schema$Task>> {\n  const max = Number(params.maxResults || 100);\n  const maxResults = String(Math.min(max, 100));\n\n  return defer(() =>\n    tasks.list({\n      ...params,\n      showCompleted: true,\n      showHidden: true,\n      maxResults,\n      fields: `nextPageToken,items(${Object.keys(fields).join(',')})`\n    })\n  ).pipe(\n    mergeMap(response => {\n      const { nextPageToken, items = [] } = response.data;\n      const data = prevTasks.concat(\n        items\n          .sort((a, b) => Number(a.position) - Number(b.position))\n          .map<Schema$Task>(d => ({ ...d, uuid: taskUUID.next() }))\n      );\n\n      if (nextPageToken && data.length < max) {\n        return getAllTasks({ ...params, pageToken: nextPageToken }, data);\n      }\n\n      return of(data);\n    })\n  );\n}\n"
  },
  {
    "path": "src/service/tasksList.ts",
    "content": "import { defer } from 'rxjs';\nimport { map } from 'rxjs/operators';\nimport { google, tasks_v1 } from 'googleapis';\nimport { oAuth2Client } from './auth';\nimport { Schema$TaskList } from '../typings';\n\nexport const { tasklists } = google.tasks({\n  version: 'v1',\n  auth: oAuth2Client\n});\n\nexport function getAllTasklist(\n  params?: tasks_v1.Params$Resource$Tasklists$List\n) {\n  return defer(() => tasklists.list(params)).pipe(\n    map(res => {\n      return (res.data.items || []) as Schema$TaskList[];\n    })\n  );\n}\n"
  },
  {
    "path": "src/serviceWorker.ts",
    "content": "// This optional code is used to register a service worker.\n// register() is not called by default.\n\n// This lets the app load faster on subsequent visits in production, and gives\n// it offline capabilities. However, it also means that developers (and users)\n// will only see deployed updates on subsequent visits to a page, after all the\n// existing tabs open on the page have been closed, since previously cached\n// resources are updated in the background.\n\n// To learn more about the benefits of this model and instructions on how to\n// opt-in, read https://bit.ly/CRA-PWA\n\nconst isLocalhost = Boolean(\n  window.location.hostname === 'localhost' ||\n    // [::1] is the IPv6 localhost address.\n    window.location.hostname === '[::1]' ||\n    // 127.0.0.1/8 is considered localhost for IPv4.\n    window.location.hostname.match(\n      /^127(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/\n    )\n);\n\ntype Config = {\n  onSuccess?: (registration: ServiceWorkerRegistration) => void;\n  onUpdate?: (registration: ServiceWorkerRegistration) => void;\n};\n\nexport function register(config?: Config) {\n  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {\n    // The URL constructor is available in all browsers that support SW.\n    const publicUrl = new URL(\n      (process as { env: { [key: string]: string } }).env.PUBLIC_URL,\n      window.location.href\n    );\n    if (publicUrl.origin !== window.location.origin) {\n      // Our service worker won't work if PUBLIC_URL is on a different origin\n      // from what our page is served on. This might happen if a CDN is used to\n      // serve assets; see https://github.com/facebook/create-react-app/issues/2374\n      return;\n    }\n\n    window.addEventListener('load', () => {\n      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;\n\n      if (isLocalhost) {\n        // This is running on localhost. Let's check if a service worker still exists or not.\n        checkValidServiceWorker(swUrl, config);\n\n        // Add some additional logging to localhost, pointing developers to the\n        // service worker/PWA documentation.\n        navigator.serviceWorker.ready.then(() => {\n          console.log(\n            'This web app is being served cache-first by a service ' +\n              'worker. To learn more, visit https://bit.ly/CRA-PWA'\n          );\n        });\n      } else {\n        // Is not localhost. Just register service worker\n        registerValidSW(swUrl, config);\n      }\n    });\n  }\n}\n\nfunction registerValidSW(swUrl: string, config?: Config) {\n  navigator.serviceWorker\n    .register(swUrl)\n    .then(registration => {\n      registration.onupdatefound = () => {\n        const installingWorker = registration.installing;\n        if (installingWorker == null) {\n          return;\n        }\n        installingWorker.onstatechange = () => {\n          if (installingWorker.state === 'installed') {\n            if (navigator.serviceWorker.controller) {\n              // At this point, the updated precached content has been fetched,\n              // but the previous service worker will still serve the older\n              // content until all client tabs are closed.\n              console.log(\n                'New content is available and will be used when all ' +\n                  'tabs for this page are closed. See https://bit.ly/CRA-PWA.'\n              );\n\n              // Execute callback\n              if (config && config.onUpdate) {\n                config.onUpdate(registration);\n              }\n            } else {\n              // At this point, everything has been precached.\n              // It's the perfect time to display a\n              // \"Content is cached for offline use.\" message.\n              console.log('Content is cached for offline use.');\n\n              // Execute callback\n              if (config && config.onSuccess) {\n                config.onSuccess(registration);\n              }\n            }\n          }\n        };\n      };\n    })\n    .catch(error => {\n      console.error('Error during service worker registration:', error);\n    });\n}\n\nfunction checkValidServiceWorker(swUrl: string, config?: Config) {\n  // Check if the service worker can be found. If it can't reload the page.\n  fetch(swUrl)\n    .then(response => {\n      // Ensure service worker exists, and that we really are getting a JS file.\n      const contentType = response.headers.get('content-type');\n      if (\n        response.status === 404 ||\n        (contentType != null && contentType.indexOf('javascript') === -1)\n      ) {\n        // No service worker found. Probably a different app. Reload the page.\n        navigator.serviceWorker.ready.then(registration => {\n          registration.unregister().then(() => {\n            window.location.reload();\n          });\n        });\n      } else {\n        // Service worker found. Proceed as normal.\n        registerValidSW(swUrl, config);\n      }\n    })\n    .catch(() => {\n      console.log(\n        'No internet connection found. App is running in offline mode.'\n      );\n    });\n}\n\nexport function unregister() {\n  if ('serviceWorker' in navigator) {\n    navigator.serviceWorker.ready.then(registration => {\n      registration.unregister();\n    });\n  }\n}\n"
  },
  {
    "path": "src/store/actions/auth.ts",
    "content": "import { useActions, GetCreatorsAction } from '../../hooks/crud-reducer';\n\nexport function authenticated() {\n  return {\n    type: 'AUTHENTICATED' as const\n  };\n}\n\nexport function logout() {\n  return {\n    type: 'LOGOUT' as const\n  };\n}\n\nconst actions = { authenticated, logout };\n\nexport type AuthActions = GetCreatorsAction<typeof actions>;\n\nexport const useAuthActions = () => useActions({ authenticated, logout });\n"
  },
  {
    "path": "src/store/actions/index.ts",
    "content": "export * from './auth';\nexport * from './task';\nexport * from './taskList';\nexport * from './preferences';\n"
  },
  {
    "path": "src/store/actions/preferences.ts",
    "content": "import { useActions } from '../../hooks/crud-reducer';\nimport { DeepPartial } from '../../utils/form';\n\nfunction updatePreferences(payload: DeepPartial<Schema$Preferences>) {\n  return {\n    type: 'UPDATE_PREFERENCES' as const,\n    payload\n  };\n}\n\nexport type UpdatePreferences = ReturnType<typeof updatePreferences>;\nexport type PreferenceActions = UpdatePreferences;\n\nexport const preferenceActions = {\n  updatePreferences\n};\n\nexport const usePreferenceActions = () => useActions(preferenceActions);\n"
  },
  {
    "path": "src/store/actions/task.ts",
    "content": "import { tasks_v1 } from 'googleapis';\nimport {\n  useActions,\n  getCRUDActionsCreator,\n  GetCreatorsAction,\n  PaginatePayload,\n  UpdatePayload\n} from '../../hooks/crud-reducer';\nimport { Schema$Task } from '../../typings';\nimport { taskUUID } from '../../service';\n\ninterface Payload$CreateTask extends Partial<Schema$Task> {\n  prevTask?: string;\n  inherit?: { uuid: string; keys: (keyof Schema$Task)[] };\n}\n\ninterface Payload$MoveTask {\n  to: number;\n  from: number;\n  uuid: string;\n}\n\nexport interface Payload$MoveToAnotherList {\n  tasklistId: string;\n  uuid: string;\n}\n\nconst [actions, actionTypes] = getCRUDActionsCreator<Schema$Task, 'uuid'>()({\n  UPDATE: 'UPDATE_TASK',\n  PAGINATE: 'PAGINATE_TASK'\n} as const);\n\nexport const TaskActionTypes = {\n  ...actionTypes,\n  GET: 'GET_TASKS',\n  CREATE: 'CREATE_TASK',\n  CREATE_SUCCESS: 'CREATE_TASK_SUCCESS',\n  DELETE: 'DELETE_TASK',\n  FOCUS: 'FOCUS_TASK',\n  UPDATE_SUCCESS: 'UPDATE_TASK_SUCCESS',\n  MOVE_TASK: 'MOVE_TASK',\n  MOVE_TASK_SUCCESS: 'MOVE_TASK_SUCCESS',\n  DELETE_ALL_COMPLETED_TASKS: 'DELETE_ALL_COMPLETED_TASKS',\n  DELETE_ALL_COMPLETED_TASKS_SUCCESS: 'DELETE_ALL_COMPLETED_TASKS_SUCCESS',\n  SYNC: 'SYNC_TASKS',\n  MOVE_TO_ANOTHER_LIST: 'MOVE_TO_ANOTHER_LIST'\n} as const;\n\nexport function getTasks(payload: tasks_v1.Params$Resource$Tasks$List) {\n  return {\n    type: TaskActionTypes.GET,\n    payload\n  };\n}\n\nexport function createTask(payload: Payload$CreateTask = {}) {\n  return {\n    type: TaskActionTypes.CREATE,\n    payload: {\n      uuid: taskUUID.next(),\n      ...payload\n    }\n  };\n}\n\nexport function deleteTask(payload: { uuid: string; prevTaskIndex?: number }) {\n  return {\n    type: TaskActionTypes.DELETE,\n    payload\n  };\n}\n\nexport function setFocus(payload?: string | number | null) {\n  return {\n    type: TaskActionTypes.FOCUS,\n    payload\n  };\n}\n\nexport function createTaskSuccess(payload: Schema$Task) {\n  return {\n    // ...actions.update(payload),\n    type: TaskActionTypes.CREATE_SUCCESS,\n    payload\n  };\n}\n\nexport function updateTaskSuccess(payload: UpdatePayload<Schema$Task, 'uuid'>) {\n  return {\n    type: TaskActionTypes.UPDATE_SUCCESS,\n    payload\n  };\n}\n\nexport function moveTask(payload: Payload$MoveTask) {\n  return {\n    type: TaskActionTypes.MOVE_TASK,\n    payload\n  };\n}\n\nexport function moveTaskSuccess() {\n  return {\n    type: TaskActionTypes.MOVE_TASK_SUCCESS\n  };\n}\n\nexport function deleteAllCompletedTasks() {\n  return {\n    type: TaskActionTypes.DELETE_ALL_COMPLETED_TASKS\n  };\n}\n\nexport function deleteAllCompletedTasksSuccess() {\n  return {\n    type: TaskActionTypes.DELETE_ALL_COMPLETED_TASKS_SUCCESS\n  };\n}\n\nexport function syncTasks(payload: PaginatePayload<Schema$Task>) {\n  return { type: TaskActionTypes.SYNC, payload };\n}\n\nexport function moveToAnotherList(payload: Payload$MoveToAnotherList) {\n  return { type: TaskActionTypes.MOVE_TO_ANOTHER_LIST, payload };\n}\n\nexport const taskActions = {\n  ...actions,\n  getTasks,\n  createTask,\n  deleteTask,\n  setFocus,\n  moveTask,\n  deleteAllCompletedTasks,\n  syncTasks,\n  moveToAnotherList\n};\n\nexport type TaskActions =\n  | GetCreatorsAction<typeof taskActions>\n  | ReturnType<typeof createTaskSuccess>\n  | ReturnType<typeof updateTaskSuccess>\n  | ReturnType<typeof moveTaskSuccess>\n  | ReturnType<typeof deleteAllCompletedTasks>\n  | ReturnType<typeof deleteAllCompletedTasksSuccess>\n  | ReturnType<typeof syncTasks>\n  | ReturnType<typeof moveToAnotherList>;\n\nexport const useTaskActions = () => useActions(taskActions);\n"
  },
  {
    "path": "src/store/actions/taskList.ts",
    "content": "import { tasks_v1 } from 'googleapis';\nimport {\n  getCRUDActionsCreator,\n  useActions,\n  PaginatePayload,\n  GetCreatorsAction\n} from '../../hooks/crud-reducer';\nimport { Schema$TaskList } from '../../typings';\n\nconst [actions, actionTypes] = getCRUDActionsCreator<Schema$TaskList, 'id'>()({\n  CREATE: 'CREATE_TASK_LIST',\n  DELETE: 'DELETE_TASK_LIST',\n  UPDATE: 'UPDATE_TASK_LIST',\n  PAGINATE: 'PAGINATE_TASK_LIST'\n} as const);\n\nexport const TaskListActionTypes = {\n  ...actionTypes,\n  GET: 'GET_TASKLISTS',\n  NEW: 'NEW_TASK_LIST',\n  DELETE_CURRENT_TASKLIST: 'DELETE_CURRENT_TASKLIST',\n  SORT_BY: 'SORT_TASKLIST_BY',\n  DISABLE: 'DISABLE_TASKLIST',\n  SYNC: 'SYNC_TASKLIST'\n} as const;\n\nexport function getTaskLists(\n  payload?: tasks_v1.Params$Resource$Tasklists$List\n) {\n  return { type: TaskListActionTypes.GET, payload };\n}\n\nexport function newTaskList(payload: string) {\n  return { type: TaskListActionTypes.NEW, payload };\n}\n\nexport function deleteCurrTaskList() {\n  return { type: TaskListActionTypes.DELETE_CURRENT_TASKLIST };\n}\n\nexport function sortTaskListBy(payload: {\n  id: string;\n  orderType: 'order' | 'date';\n}) {\n  return { type: TaskListActionTypes.SORT_BY, payload };\n}\n\nexport function syncTaskList(payload: PaginatePayload<Schema$TaskList>) {\n  return { type: TaskListActionTypes.SYNC, payload };\n}\n\nexport const taskListActions = {\n  ...actions,\n  getTaskLists,\n  newTaskList,\n  deleteCurrTaskList,\n  sortTaskListBy\n};\n\nexport type TaskListActions =\n  | GetCreatorsAction<typeof taskListActions>\n  | ReturnType<typeof getTaskLists>\n  | ReturnType<typeof newTaskList>\n  | ReturnType<typeof deleteCurrTaskList>\n  | ReturnType<typeof sortTaskListBy>\n  | ReturnType<typeof syncTaskList>;\n\nexport const useTaskListActions = () => useActions(taskListActions);\n"
  },
  {
    "path": "src/store/epics/auth.ts",
    "content": "import { generatePath } from 'react-router-dom';\nimport { ofType, Epic } from 'redux-observable';\nimport { RouterAction, replace } from 'connected-react-router';\nimport { of } from 'rxjs';\nimport { switchMap } from 'rxjs/operators';\nimport { AuthActions } from '../actions/auth';\nimport { authenticate } from '../../service';\nimport { RootState } from '../reducers';\nimport { PATHS } from '../../constants';\n\ntype Actions = AuthActions | RouterAction;\ntype AuthEpic = Epic<Actions, Actions, RootState>;\n\nconst authEpic: AuthEpic = action$ =>\n  action$.pipe(\n    ofType('AUTHENTICATED'),\n    switchMap(() => {\n      authenticate();\n      return of(replace(generatePath(PATHS.TASKLIST, {})));\n    })\n  );\n\nconst logoutEpic: AuthEpic = action$ =>\n  action$.pipe(\n    ofType('LOGOUT'),\n    switchMap(() => {\n      window.logout();\n      return of(replace(PATHS.AUTH));\n    })\n  );\n\nexport default [authEpic, logoutEpic];\n"
  },
  {
    "path": "src/store/epics/index.ts",
    "content": "import { combineEpics } from 'redux-observable';\nimport authEpics from './auth';\nimport taskEpics from './task';\nimport taskListEpics from './taskList';\nimport syncDataEpic from './preferences';\n\nexport default combineEpics(\n  ...authEpics,\n  ...taskEpics,\n  ...taskListEpics,\n  ...syncDataEpic\n);\n"
  },
  {
    "path": "src/store/epics/preferences.ts",
    "content": "import { Epic, ofType } from 'redux-observable';\nimport { defer, empty, fromEvent, merge, timer, of, concat } from 'rxjs';\nimport { switchMap, map, takeUntil, filter, delay } from 'rxjs/operators';\nimport {\n  TaskListActions,\n  TaskListActionTypes,\n  syncTaskList\n} from '../actions/taskList';\nimport { TaskActions, TaskActionTypes, syncTasks } from '../actions/task';\nimport { RootState } from '../reducers';\nimport { getAllTasklist, getAllTasks } from '../../service';\nimport { currentTaskListsSelector } from '../selectors';\nimport { PreferenceActions } from '../actions';\n\ntype Actions = TaskListActions | TaskActions | PreferenceActions;\ntype PreferencesEpic = Epic<Actions, Actions, RootState>;\n\nexport const syncDataEpic: PreferencesEpic = (action$, state$) => {\n  const userActions$ = action$.pipe(\n    filter(\n      ({ type }) =>\n        type !== TaskListActionTypes.SYNC && type !== TaskActionTypes.SYNC\n    )\n  );\n\n  const inactive$ = userActions$.pipe(\n    switchMap(() => {\n      const { inactiveHours } = state$.value.preferences.sync;\n      const ms = inactiveHours * 60 * 60 * 1000;\n      return timer(Math.max(ms, 60 * 1000));\n    })\n  );\n\n  const reconnect$ = fromEvent(window, 'online').pipe(\n    map(() => (state$.value.preferences.sync.reconnection ? empty() : of([])))\n  );\n\n  return merge(reconnect$, inactive$).pipe(\n    switchMap(() => {\n      const { enabled } = state$.value.preferences.sync;\n      return enabled\n        ? concat(\n            defer(() => getAllTasklist()).pipe(map(syncTaskList)),\n            defer(() => {\n              const taskList = currentTaskListsSelector(state$.value);\n              return taskList\n                ? getAllTasks({ tasklist: taskList.id })\n                : Promise.reject(new Error('Sync tasks failed'));\n            }).pipe(\n              delay(100),\n              map(syncTasks)\n              //\n            )\n          ).pipe(takeUntil(userActions$))\n        : empty();\n    })\n  );\n};\n\nexport const savePreferencesEpic: PreferencesEpic = (action$, state$) => {\n  return action$.pipe(\n    ofType<Actions, PreferenceActions>('UPDATE_PREFERENCES'),\n    switchMap(({ payload: changes }) => {\n      window.preferencesStorage.save(state$.value.preferences);\n\n      if (changes.theme) {\n        window.__setTheme(changes.theme);\n      }\n\n      if (changes.accentColor) {\n        window.__setAccentColor(changes.accentColor);\n      }\n\n      if (changes.titleBar) {\n        window.__setTitleBar(changes.titleBar);\n      }\n\n      return empty();\n    })\n  );\n};\n\nexport default [syncDataEpic, savePreferencesEpic];\n"
  },
  {
    "path": "src/store/epics/task.ts",
    "content": "import { ofType, Epic, ActionsObservable } from 'redux-observable';\nimport { RouterAction } from 'connected-react-router';\nimport { Observable, empty, defer, of, forkJoin, from, concat } from 'rxjs';\nimport {\n  switchMap,\n  mergeMap,\n  map,\n  filter,\n  catchError,\n  groupBy,\n  debounceTime,\n  takeUntil,\n  shareReplay,\n  take,\n  tap,\n  concatMap,\n  retry\n} from 'rxjs/operators';\nimport {\n  TaskActions,\n  createTaskSuccess,\n  updateTaskSuccess,\n  moveTaskSuccess,\n  deleteAllCompletedTasksSuccess,\n  taskActions\n} from '../actions/task';\nimport { RootState } from '../reducers';\nimport { taskSelector, currentTaskListsSelector } from '../selectors';\nimport { tasksAPI, getAllTasks } from '../../service';\nimport { ExtractAction, Schema$Task } from '../../typings';\nimport { NProgress } from '../../utils/nprogress';\n\ntype Actions = TaskActions | RouterAction;\ntype TaskEpic = Epic<Actions, Actions, RootState>;\n\nconst waitForTaskCreated$ = (\n  action$: ActionsObservable<Actions>,\n  uuid: string\n): Observable<Schema$Task> =>\n  action$.pipe(\n    ofType<Actions, ExtractAction<Actions, 'CREATE_TASK_SUCCESS'>>(\n      'CREATE_TASK_SUCCESS'\n    ),\n    filter(action => action.payload.uuid === uuid),\n    map(action => action.payload),\n    take(1)\n  );\n\nconst deleteTask$ = (\n  action$: ActionsObservable<Actions>,\n  uuid: string\n): Observable<ExtractAction<Actions, 'DELETE_TASK'>> =>\n  action$.pipe(\n    ofType<Actions, ExtractAction<Actions, 'DELETE_TASK'>>('DELETE_TASK'),\n    filter(action => action.payload.uuid === uuid),\n    take(1)\n  );\n\nconst nprogressEpic: TaskEpic = action$ =>\n  action$.pipe(\n    ofType('PAGINATE_TASK'),\n    switchMap(() => {\n      NProgress.done();\n      return empty();\n    })\n  );\n\nconst getTasksEpic: TaskEpic = (action$, state$) =>\n  action$.pipe(\n    ofType<Actions, ExtractAction<Actions, 'GET_TASKS'>>('GET_TASKS'),\n    switchMap(action => {\n      NProgress.start();\n      const max = state$.value.preferences.maxTasks;\n      return getAllTasks({\n        ...action.payload,\n        maxResults: String(max)\n      }).pipe(\n        map(payload => taskActions.paginate(payload)),\n        tap(() => NProgress.done())\n      );\n    })\n  );\n\nconst createTaskEpic: TaskEpic = (action$, state$) =>\n  action$.pipe(\n    ofType<Actions, ExtractAction<Actions, 'CREATE_TASK'>>('CREATE_TASK'),\n    mergeMap(action => {\n      const { prevTask: prevTaskUUID, uuid } = action.payload;\n      const tasklist = currentTaskListsSelector(state$.value);\n      const prevTask = prevTaskUUID\n        ? taskSelector(prevTaskUUID)(state$.value)\n        : undefined;\n      const prevTask$ =\n        !prevTask || prevTask.id\n          ? of(prevTask)\n          : waitForTaskCreated$(action$, prevTask.uuid);\n\n      const { uuid: ignore, ...requestBody } =\n        taskSelector(uuid)(state$.value) || {};\n\n      const createTask$ = prevTask$.pipe(\n        switchMap(prevTask =>\n          defer(() =>\n            tasksAPI.insert({\n              previous: prevTask && prevTask.id!,\n              tasklist: tasklist && tasklist.id!,\n              requestBody\n            })\n          )\n        ),\n        map(res => res.data),\n        shareReplay(1)\n      );\n\n      return createTask$.pipe(\n        map(task => createTaskSuccess({ ...task, uuid })),\n        takeUntil(\n          deleteTask$(action$, uuid).pipe(\n            tap(() => {\n              createTask$.subscribe(task =>\n                tasksAPI.delete({\n                  task: task.id!,\n                  tasklist: tasklist && tasklist.id\n                })\n              );\n            })\n          )\n        )\n      );\n    })\n  );\n\nconst updateTaskEpic: TaskEpic = (action$, state$) =>\n  action$.pipe(\n    ofType<Actions, ExtractAction<Actions, 'UPDATE_TASK'>>('UPDATE_TASK'),\n    groupBy(action => action.payload.uuid),\n    mergeMap(group$ =>\n      group$.pipe(\n        debounceTime(250),\n        switchMap(action => {\n          const { uuid, ...changes } = action.payload;\n          const tasklist = currentTaskListsSelector(state$.value)!;\n          const task = taskSelector(uuid)(state$.value);\n          const task$ =\n            task && task.id ? of(task) : waitForTaskCreated$(action$, uuid);\n\n          const patchUpdate$ = task$.pipe(\n            switchMap(task =>\n              defer(() =>\n                tasksAPI.patch({\n                  task: task.id!,\n                  tasklist: tasklist.id!,\n                  requestBody: changes\n                })\n              ).pipe(\n                map(res => res.data),\n                catchError(() => empty())\n              )\n            ),\n            shareReplay(1)\n          );\n\n          return patchUpdate$.pipe(\n            map(task => updateTaskSuccess({ ...task, ...changes, uuid })),\n            takeUntil(\n              deleteTask$(action$, group$.key).pipe(\n                tap(() => {\n                  // patch update will create a new task if task is not exits\n                  patchUpdate$.subscribe(task =>\n                    tasksAPI.delete({\n                      task: task.id!,\n                      tasklist: tasklist && tasklist.id\n                    })\n                  );\n                })\n              )\n            )\n          );\n        })\n      )\n    )\n  );\n\nconst deleteTaskEpic: TaskEpic = (action$, state$) =>\n  action$.pipe(\n    ofType<Actions, ExtractAction<Actions, 'DELETE_TASK'>>('DELETE_TASK'),\n    mergeMap(action => {\n      const { uuid } = action.payload;\n      const task = state$.value.task.deleted[uuid];\n      const tasklist = currentTaskListsSelector(state$.value);\n\n      return (task && task.id\n        ? of(task)\n        : waitForTaskCreated$(action$, uuid)\n      ).pipe(\n        switchMap(task =>\n          defer(() =>\n            tasksAPI.delete({\n              task: task.id!,\n              tasklist: tasklist && tasklist.id\n            })\n          ).pipe(mergeMap(() => empty()))\n        )\n      );\n    })\n  );\n\nconst moveTaskEpic: TaskEpic = (action$, state$) => {\n  return action$.pipe(\n    ofType<Actions, ExtractAction<Actions, 'MOVE_TASK'>>('MOVE_TASK'),\n    groupBy(action => action.payload.uuid),\n    mergeMap(group$ =>\n      group$.pipe(\n        debounceTime(500),\n        switchMap((action: ExtractAction<Actions, 'MOVE_TASK'>) => {\n          const tasklist = currentTaskListsSelector(state$.value);\n          const index = action.payload.to;\n          const todo = state$.value.task.todo.ids;\n          const payload = [todo[index - 1], todo[index]].map(uuid => {\n            const task = uuid && taskSelector(uuid)(state$.value);\n            if (task) {\n              return task.id\n                ? of(task)\n                : waitForTaskCreated$(action$, task.uuid);\n            }\n            return of(undefined);\n          }) as [any, any];\n\n          return forkJoin<Schema$Task | undefined>(...payload).pipe(\n            mergeMap(([prevTask, currTask]) => {\n              const taskId = currTask && currTask.id;\n              const prevId = prevTask && prevTask.id;\n\n              // currTask may be delete before successful loaded\n              if (taskId) {\n                return defer(() =>\n                  tasksAPI.move({\n                    task: taskId,\n                    previous: prevId || undefined,\n                    tasklist: tasklist && tasklist.id\n                  })\n                ).pipe(\n                  //\n                  map(moveTaskSuccess)\n                );\n              }\n\n              return empty();\n            })\n          );\n        })\n      )\n    )\n  );\n};\n\nconst deleteCompletedTasksEpic: TaskEpic = (action$, state$) =>\n  action$.pipe(\n    ofType<Actions, ExtractAction<Actions, 'DELETE_ALL_COMPLETED_TASKS'>>(\n      'DELETE_ALL_COMPLETED_TASKS'\n    ),\n    switchMap(() => {\n      const tasklist = currentTaskListsSelector(state$.value);\n      const completedTasks = state$.value.task.completed.list;\n      if (tasklist) {\n        return from(completedTasks).pipe(\n          concatMap(task =>\n            defer(() =>\n              tasksAPI.delete({ task: task.id!, tasklist: tasklist.id })\n            ).pipe(\n              retry(1),\n              catchError(() => of([]))\n            )\n          ),\n          map(() => deleteAllCompletedTasksSuccess())\n        );\n      }\n      return empty();\n    })\n  );\n\nconst moveToAnotherListEpic: TaskEpic = (action$, state$) =>\n  action$.pipe(\n    ofType<Actions, ExtractAction<Actions, 'MOVE_TO_ANOTHER_LIST'>>(\n      'MOVE_TO_ANOTHER_LIST'\n    ),\n    switchMap(action => {\n      const tasklist = currentTaskListsSelector(state$.value);\n      const task = state$.value.task.deleted[action.payload.uuid];\n      if (tasklist && task && task.id) {\n        const { uuid, ...requestBody } = task;\n\n        const newTask$ = defer(() =>\n          tasksAPI.insert({\n            tasklist: action.payload.tasklistId,\n            requestBody\n          })\n        );\n\n        const deleteTask$ = defer(() =>\n          tasksAPI.delete({\n            task: task.id!,\n            tasklist: tasklist.id\n          })\n        );\n\n        concat(newTask$, deleteTask$).subscribe();\n      } else {\n        console.warn(\n          'Cannot move task to anthor list',\n          !!tasklist,\n          !!task,\n          !!task.id\n        );\n      }\n      return empty();\n    })\n  );\n\nexport default [\n  nprogressEpic,\n  getTasksEpic,\n  createTaskEpic,\n  updateTaskEpic,\n  deleteTaskEpic,\n  moveTaskEpic,\n  deleteCompletedTasksEpic,\n  moveToAnotherListEpic\n];\n"
  },
  {
    "path": "src/store/epics/taskList.ts",
    "content": "import { ofType, Epic } from 'redux-observable';\nimport { generatePath } from 'react-router-dom';\nimport { RouterAction, push, replace } from 'connected-react-router';\nimport { of, defer, empty, merge } from 'rxjs';\nimport { switchMap, mergeMap, map, tap } from 'rxjs/operators';\nimport { taskListActions, TaskListActions } from '../actions/taskList';\nimport { RootState } from '../reducers';\nimport { tasklists, getAllTasklist, taskListAPI } from '../../service';\nimport { PATHS } from '../../constants';\nimport { ExtractAction, Schema$TaskList } from '../../typings';\nimport { currentTaskListsSelector } from '../selectors';\nimport NProgress from '../../utils/nprogress';\n\ntype Actions = TaskListActions | RouterAction;\ntype TaskEpic = Epic<Actions, Actions, RootState>;\n\nconst gotoMasterTasklist = () => of(replace(generatePath(PATHS.TASKLIST, {})));\n\nconst getTaskListsEpic: TaskEpic = action$ =>\n  action$.pipe(\n    ofType<Actions, ExtractAction<Actions, 'GET_TASKLISTS'>>('GET_TASKLISTS'),\n    tap(() => NProgress.start()),\n    switchMap(action =>\n      getAllTasklist(action.payload).pipe(\n        map(payload => taskListActions.paginate(payload))\n      )\n    )\n  );\n\nconst newTaskListEpic: TaskEpic = action$ =>\n  action$.pipe(\n    ofType<Actions, ExtractAction<Actions, 'NEW_TASK_LIST'>>('NEW_TASK_LIST'),\n    tap(() => NProgress.start()),\n    switchMap(action =>\n      defer(() =>\n        tasklists.insert({ requestBody: { title: action.payload } })\n      ).pipe(\n        map(res => res.data as Schema$TaskList),\n        mergeMap(tasklist =>\n          of(\n            taskListActions.create(tasklist),\n            push(\n              generatePath(PATHS.TASKLIST, {\n                taskListId: tasklist.id\n              })\n            )\n          )\n        )\n      )\n    )\n  );\n\nconst deleteCurrentTaskListEpic: TaskEpic = (action$, state$) =>\n  action$.pipe(\n    ofType('DELETE_CURRENT_TASKLIST'),\n    tap(() => NProgress.start()),\n    switchMap(() => {\n      const current = currentTaskListsSelector(state$.value);\n      return current\n        ? defer(() => taskListAPI.delete({ tasklist: current.id })).pipe(\n            mergeMap(() =>\n              merge(\n                of(taskListActions.delete({ id: current.id })),\n                gotoMasterTasklist()\n              )\n            )\n          )\n        : empty();\n    })\n  );\n\nconst updateTaskListEpic: TaskEpic = action$ =>\n  action$.pipe(\n    ofType<Actions, ExtractAction<Actions, 'UPDATE_TASK_LIST'>>(\n      'UPDATE_TASK_LIST'\n    ),\n    switchMap(action => {\n      const { id } = action.payload;\n      return defer(() =>\n        id\n          ? taskListAPI.update({\n              tasklist: id,\n              requestBody: action.payload\n            })\n          : Promise.resolve()\n      ).pipe(mergeMap(() => empty()));\n    })\n  );\n\nconst sortTaskListEpic: TaskEpic = (action$, state$) =>\n  action$.pipe(\n    ofType('DELETE_TASK_LIST', 'SORT_TASKLIST_BY'),\n    switchMap(() => {\n      window.taskListSortByDateStorage.save(state$.value.taskList.sortByDate);\n      return empty();\n    })\n  );\n\nconst redirectEpic: TaskEpic = (action$, state$) =>\n  action$.pipe(\n    ofType('PAGINATE_TASK_LIST', 'SYNC_TASKLIST'),\n    switchMap(() =>\n      currentTaskListsSelector(state$.value) ? empty() : gotoMasterTasklist()\n    )\n  );\n\nexport default [\n  getTaskListsEpic,\n  newTaskListEpic,\n  deleteCurrentTaskListEpic,\n  updateTaskListEpic,\n  sortTaskListEpic,\n  redirectEpic\n];\n"
  },
  {
    "path": "src/store/index.ts",
    "content": "import { createHashHistory } from 'history';\nimport { createStore, applyMiddleware, compose } from 'redux';\nimport { createEpicMiddleware, Epic } from 'redux-observable';\nimport { routerMiddleware } from 'connected-react-router';\nimport { BehaviorSubject } from 'rxjs';\nimport { switchMap } from 'rxjs/operators';\nimport rootEpic from './epics';\nimport createRootReducer from './reducers';\n\nexport const history = createHashHistory();\n\nconst epic$ = new BehaviorSubject(rootEpic);\nconst hotReloadingEpic: Epic<any> = (action$, state$, dependencies) =>\n  epic$.pipe(switchMap(epic => epic(action$, state$, dependencies)));\n\nexport default function configureStore() {\n  const epicMiddleware = createEpicMiddleware();\n  const composeEnhancers =\n    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;\n  const enhancer = composeEnhancers(\n    applyMiddleware(routerMiddleware(history), epicMiddleware)\n  );\n\n  const store = createStore(createRootReducer(history), undefined, enhancer);\n\n  epicMiddleware.run(hotReloadingEpic);\n\n  if (process.env.NODE_ENV !== 'production') {\n    if (module.hot) {\n      module.hot.accept('./reducers', () => {\n        store.replaceReducer(createRootReducer(history));\n      });\n\n      module.hot.accept('./epics', () => {\n        const nextRootEpic = require('./epics').default;\n        epic$.next(nextRootEpic);\n      });\n    }\n  }\n\n  return store;\n}\n\nexport * from './actions';\nexport * from './reducers';\nexport * from './epics';\nexport * from './selectors';\n"
  },
  {
    "path": "src/store/reducers/auth.ts",
    "content": "import { AuthActions } from '../actions/auth';\n\ninterface State {\n  loggedIn: boolean;\n}\n\nconst initialState: State = {\n  loggedIn: !!window.tokenStorage.get()\n};\n\nexport default function (state = initialState, action: AuthActions): State {\n  switch (action.type) {\n    case 'AUTHENTICATED':\n      return {\n        ...state,\n        loggedIn: true\n      };\n\n    case 'LOGOUT':\n      return {\n        ...state,\n        loggedIn: false\n      };\n\n    default:\n      return state;\n  }\n}\n"
  },
  {
    "path": "src/store/reducers/index.ts",
    "content": "import { combineReducers } from 'redux';\nimport { connectRouter } from 'connected-react-router';\nimport { taskReducer } from './task';\nimport { taskListReducer } from './taskList';\nimport auth from './auth';\nimport { preferencesReducer } from './preferences';\n\nconst rootReducer = (history: Parameters<typeof connectRouter>[0]) =>\n  combineReducers({\n    router: connectRouter(history),\n    taskList: taskListReducer,\n    task: taskReducer,\n    preferences: preferencesReducer,\n    auth\n  });\n\nexport type RootState = ReturnType<ReturnType<typeof rootReducer>>;\n\nexport default rootReducer;\n"
  },
  {
    "path": "src/store/reducers/preferences.ts",
    "content": "import { PreferenceActions } from '../actions/preferences';\n\ninterface State extends Schema$Preferences {}\n\nconst initialState: State = {\n  ...window.preferencesStorage.get(),\n  ...(window.platform === 'darwin' ? { titleBar: 'native' } : {})\n};\n\nexport function preferencesReducer(\n  state = initialState,\n  action: PreferenceActions\n): State {\n  switch (action.type) {\n    case 'UPDATE_PREFERENCES':\n      return {\n        ...state,\n        ...action.payload,\n        sync: {\n          ...state.sync,\n          ...action.payload.sync\n        }\n      };\n\n    default:\n      return state;\n  }\n}\n"
  },
  {
    "path": "src/store/reducers/task.ts",
    "content": "import { TaskActionTypes, TaskActions, taskActions } from '../actions/task';\nimport { taskSelector } from '../selectors';\nimport {\n  createCRUDReducer,\n  parsePaginatePayload\n} from '../../hooks/crud-reducer';\nimport { Schema$Task } from '../../typings';\n\ninterface State {\n  loading?: boolean;\n  todo: typeof taskState;\n  completed: typeof taskState;\n  focused: string | null;\n  deleted: { [x: string]: Schema$Task };\n}\n\nconst [taskState, reducer] = createCRUDReducer<Schema$Task, 'uuid'>('uuid', {\n  prefill: false,\n  actionTypes: TaskActionTypes\n});\n\nconst initialState: State = {\n  loading: true,\n  todo: taskState,\n  completed: taskState,\n  focused: null,\n  deleted: {}\n};\n\nexport function taskReducer(\n  state = initialState,\n  action: TaskActions\n): typeof initialState {\n  switch (action.type) {\n    case 'GET_TASKS':\n      return {\n        ...state,\n        loading: true\n      };\n\n    case 'SYNC_TASKS':\n    case 'PAGINATE_TASK':\n      return (() => {\n        const todo: Schema$Task[] = [];\n        const completed: Schema$Task[] = [];\n        const payload = parsePaginatePayload(action.payload);\n        for (const task of payload.data) {\n          task.hidden || task.status === 'completed'\n            ? completed.push(task)\n            : todo.push(task);\n        }\n        return {\n          ...state,\n          loading: false,\n          todo: reducer(taskState, taskActions.paginate(todo)),\n          completed: reducer(taskState, taskActions.paginate(completed))\n        };\n      })();\n\n    case 'CREATE_TASK':\n      return (() => {\n        const { prevTask, uuid, inherit, ...task } = action.payload;\n        const index = prevTask ? state.todo.ids.indexOf(prevTask) + 1 : 0;\n        const inheritFrom = inherit && state.todo.byIds[inherit.uuid];\n        const newTask = {\n          uuid,\n          ...task,\n          ...(inheritFrom &&\n            inherit!.keys.reduce(\n              (t, k) => ({ ...t, [k]: inheritFrom[k] }),\n              {} as Partial<Schema$Task>\n            ))\n        };\n        return {\n          ...state,\n          focused: uuid,\n          todo: {\n            ...state.todo,\n            ids: [\n              ...state.todo.ids.slice(0, index),\n              uuid,\n              ...state.todo.ids.slice(index)\n            ],\n            list: [\n              ...state.todo.list.slice(0, index),\n              newTask,\n              ...state.todo.list.slice(index)\n            ],\n            byIds: {\n              ...state.todo.byIds,\n              [uuid]: newTask\n            }\n          }\n        };\n      })();\n\n    case 'UPDATE_TASK':\n      return (() => {\n        const { uuid } = action.payload;\n        const isTodoTask = state.todo.ids.includes(uuid);\n        const deleteTask = taskActions.deleteTask({ uuid });\n        const updateTask = taskActions.update(action.payload);\n        if (action.payload.status === 'completed') {\n          const { id, hidden, ...task } = state.todo.byIds[uuid];\n          return {\n            ...state,\n            todo: reducer(state.todo, deleteTask),\n            completed: reducer(\n              state.completed,\n              taskActions.createTask({\n                ...task,\n                ...state.todo.byIds[uuid],\n                hidden: true\n              })\n            )\n          };\n        } else if (action.payload.status === 'needsAction') {\n          const { id, hidden, ...task } = state.completed.byIds[uuid];\n          return {\n            ...state,\n            todo: reducer(\n              state.todo,\n              taskActions.createTask({\n                ...task,\n                ...action.payload,\n                hidden: false\n              })\n            ),\n            completed: reducer(state.completed, deleteTask)\n          };\n        }\n\n        return {\n          ...state,\n          ...(isTodoTask\n            ? { todo: reducer(state.todo, updateTask) }\n            : { completed: reducer(state.completed, updateTask) })\n        };\n      })();\n\n    case 'DELETE_TASK':\n      return (() => {\n        const { uuid, prevTaskIndex } = action.payload;\n        const isTodoTask = state.todo.ids.includes(uuid);\n        const [newState, task] = isTodoTask\n          ? [reducer(state.todo, action), state.todo.byIds[uuid]]\n          : [reducer(state.completed, action), state.completed.byIds[uuid]];\n        const prevTask =\n          typeof prevTaskIndex === 'number' && state.todo.ids[prevTaskIndex];\n        const [first, second] = state.todo.ids;\n\n        return {\n          ...state,\n          ...(isTodoTask ? { todo: newState } : { completed: newState }),\n          deleted: {\n            ...state.deleted,\n            [uuid]: task!\n          },\n          focused:\n            // focus if task deleted by backspace or it is the first task and focused\n            prevTask || (state.focused && first === uuid && second) || null\n        };\n      })();\n\n    case 'FOCUS_TASK':\n      return {\n        ...state,\n        focused:\n          (typeof action.payload === 'number'\n            ? state.todo.ids[\n                Math.max(0, Math.min(state.todo.ids.length - 1, action.payload))\n              ]\n            : action.payload) || null\n      };\n\n    case 'CREATE_TASK_SUCCESS':\n    case 'UPDATE_TASK_SUCCESS':\n      return (() => {\n        const task = {\n          ...action.payload,\n          ...taskSelector(action.payload.uuid)({ task: state })\n        };\n        const isTodoTask = state.todo.ids.includes(task.uuid);\n        const _action = taskActions.update(task);\n        const newState = isTodoTask\n          ? { todo: reducer(state.todo, _action) }\n          : { completed: reducer(state.completed, _action) };\n\n        return {\n          ...state,\n          ...newState\n        };\n      })();\n\n    case 'MOVE_TASK':\n      const { from, to } = action.payload;\n\n      return to < 0\n        ? state\n        : {\n            ...state,\n            todo: {\n              ...state.todo,\n              ids: move(state.todo.ids, from, to),\n              list: move(state.todo.list, from, to)\n            }\n          };\n\n    case 'DELETE_ALL_COMPLETED_TASKS_SUCCESS':\n      return { ...state, completed: initialState.completed };\n\n    case 'MOVE_TO_ANOTHER_LIST':\n      return (() => {\n        const { uuid } = action.payload;\n        return {\n          ...state,\n          focued: null,\n          deleted: {\n            ...state.deleted,\n            [uuid]: state.todo.byIds[uuid]!\n          },\n          todo: reducer(state.todo, taskActions.deleteTask({ uuid }))\n        };\n      })();\n\n    default:\n      return state;\n  }\n}\n\n// credit: https://github.com/sindresorhus/array-move\nfunction move<T>(arr: T[], from: number, to: number) {\n  const clone = arr.slice();\n  clone.splice(to < 0 ? clone.length + to : to, 0, clone.splice(from, 1)[0]);\n  return clone;\n}\n"
  },
  {
    "path": "src/store/reducers/taskList.ts",
    "content": "import {\n  TaskListActionTypes,\n  TaskListActions,\n  taskListActions\n} from '../actions/taskList';\nimport { createCRUDReducer, removeFromArray } from '../../hooks/crud-reducer';\nimport { Schema$TaskList, ExtractAction } from '../../typings';\nimport { TaskActions } from '../actions';\n\ntype DefaultState = typeof defaultState;\n\ninterface State extends DefaultState {\n  loading: boolean;\n  sortByDate: string[];\n}\n\nconst [defaultState, reducer] = createCRUDReducer<Schema$TaskList, 'id'>('id', {\n  prefill: false,\n  actionTypes: TaskListActionTypes\n});\n\nconst initialState: State = {\n  ...defaultState,\n  loading: true,\n  sortByDate: window.taskListSortByDateStorage.get()\n};\n\nexport function taskListReducer(\n  state = initialState,\n  action: TaskListActions | ExtractAction<TaskActions, 'PAGINATE_TASK'>\n): State {\n  switch (action.type) {\n    case 'PAGINATE_TASK':\n      return {\n        ...state,\n        loading: false\n      };\n\n    case 'GET_TASKLISTS':\n    case 'NEW_TASK_LIST':\n      return {\n        ...state,\n        loading: true\n      };\n\n    case 'CREATE_TASK_LIST':\n      return {\n        ...state,\n        ...reducer(state, action),\n        loading: false\n      };\n\n    case 'DELETE_TASK_LIST':\n      return {\n        ...state,\n        ...reducer(state, action),\n        loading: false,\n        sortByDate: removeFromArray(\n          state.sortByDate,\n          state.sortByDate.indexOf(action.payload.id)\n        )\n      };\n\n    case 'DELETE_CURRENT_TASKLIST':\n      return {\n        ...state,\n        loading: true\n      };\n\n    case 'SORT_TASKLIST_BY':\n      const { id, orderType } = action.payload;\n      const { sortByDate } = state;\n      return {\n        ...state,\n        sortByDate:\n          orderType === 'date'\n            ? [...state.sortByDate, id]\n            : removeFromArray(sortByDate, sortByDate.indexOf(id))\n      };\n\n    case 'SYNC_TASKLIST':\n      return {\n        ...state,\n        ...reducer(initialState, taskListActions.paginate(action.payload)),\n        loading: false\n      };\n\n    default:\n      return {\n        ...state,\n        ...reducer(state, action)\n      };\n  }\n}\n"
  },
  {
    "path": "src/store/selectors/index.ts",
    "content": "export * from './task';\nexport * from './taskList';\nexport * from './preferences';\n"
  },
  {
    "path": "src/store/selectors/preferences.ts",
    "content": "import { RootState } from '../reducers';\n\nexport const preferencesSelector = (state: RootState) => state.preferences;\n\nexport const titleBarSelector = (state: RootState) =>\n  state.preferences.titleBar;\n\nexport const themeSelector = (state: RootState) => state.preferences.theme;\n"
  },
  {
    "path": "src/store/selectors/task.ts",
    "content": "import { RootState } from '../reducers';\nimport { createSelector } from 'reselect';\nimport { Schema$Task } from '../../typings';\n\nexport const todoTaskIdsSelector = (state: RootState) => state.task.todo.ids;\nexport const todoTaskListSelector = (state: RootState) => state.task.todo.list;\nexport const todoTaskByIdsSelector = (state: RootState) =>\n  state.task.todo.byIds;\n\nexport const completedTaskIdsSelector = (state: RootState) =>\n  state.task.completed.ids;\n\nexport const taskSelector = (id: string) => (state: Pick<RootState, 'task'>) =>\n  state.task.todo.byIds[id] || state.task.completed.byIds[id];\n\nexport const todoTaskSelector = (id: string) => (state: RootState) =>\n  state.task.todo.byIds[id];\n\nexport const completedTaskSelector = (id: string) => (state: RootState) =>\n  state.task.completed.byIds[id];\n\nexport const focusedSelector = (id: string) => (state: RootState) =>\n  state.task.focused === id;\n\nconst future = new Date(10 ** 15).getTime();\nconst getDate = (t?: Schema$Task) =>\n  t && t.due ? new Date(t.due).getTime() : future;\n\nexport const getDateLabel = (due: string | null | undefined, now: Date) => {\n  let label = 'No date';\n  if (due) {\n    const date = new Date(due);\n    const dayDiff = date.dayDiff(now);\n    if (dayDiff > 0) {\n      label = 'Past';\n    } else if (dayDiff === 0) {\n      label = 'Today';\n    } else if (dayDiff === -1) {\n      label = 'Tomorrow';\n    } else if (dayDiff < -1) {\n      label = 'Due ' + date.format('D, j M');\n    }\n  }\n\n  return label;\n};\n\nexport const todoTasksIdsByDateSelector = createSelector(\n  todoTaskIdsSelector,\n  todoTaskByIdsSelector,\n  (ids, byIds) => {\n    const order = ids\n      .slice()\n      .sort((a, b) => getDate(byIds[a]) - getDate(byIds[b]));\n    const map = order.reduce((result, id) => {\n      const task = byIds[id];\n      if (task) {\n        const label = getDateLabel(task.due, new Date());\n        return { ...result, [label]: [...(result[label] || []), task] };\n      }\n      return result;\n    }, {} as Record<string, Schema$Task[]>);\n    return { order, tasks: Object.entries(map) };\n  }\n);\n"
  },
  {
    "path": "src/store/selectors/taskList.ts",
    "content": "import { createSelector } from 'reselect';\nimport { matchPath } from 'react-router-dom';\nimport { RootState } from '../reducers';\nimport { PATHS } from '../../constants';\nimport { Schema$TaskList } from '../../typings';\n\nexport const taskListIdsSelector = (state: RootState) => state.taskList.ids;\n\nexport const taskListsSelector = (id: string) => (state: RootState) =>\n  state.taskList.byIds[id];\n\nexport const currentTaskListsSelector = (state: RootState) => {\n  const matches = matchPath<{ taskListId: string }>(\n    state.router.location.pathname,\n    {\n      path: PATHS.TASKLIST,\n      exact: true\n    }\n  );\n  const id = matches && matches.params.taskListId;\n  return id\n    ? state.taskList.byIds[id]\n    : (state.taskList.list[0] as Schema$TaskList);\n};\n\nexport const isMasterTaskListSelector = createSelector(\n  taskListIdsSelector,\n  currentTaskListsSelector,\n  (ids, list) => list && list.id === ids[0]\n);\n\nexport const isSortByDateSelector = (id: string) => (state: RootState) =>\n  state.taskList.sortByDate.includes(id);\n"
  },
  {
    "path": "src/theme.ts",
    "content": "import { createMuiTheme } from '@material-ui/core/styles';\n\nexport const theme = createMuiTheme({\n  typography: {\n    fontSize: 14,\n    fontFamily: 'Roboto'\n  },\n  props: {\n    MuiDivider: {\n      light: true,\n      style: {\n        margin: '0.4em 0',\n        backgroundColor: 'var(--border-color)'\n      }\n    },\n    MuiSvgIcon: {\n      fontSize: 'small',\n      color: 'inherit'\n    },\n    MuiButton: {\n      color: 'inherit'\n    }\n  }\n});\n\nexport default theme;\n"
  },
  {
    "path": "src/typings/index.ts",
    "content": "import { tasks_v1 } from 'googleapis';\n\nexport interface Schema$Task {\n  uuid: string;\n\n  completed?: string | null;\n\n  hidden?: boolean | null;\n\n  id?: string | null;\n\n  notes?: string | null;\n\n  position?: string | null;\n\n  status?: string | null;\n\n  title?: string | null;\n\n  due?: string | null;\n}\n\nexport interface Schema$TaskList extends tasks_v1.Schema$TaskList {\n  id: string;\n}\n\nexport type ExtractAction<\n  T1 extends { type: string },\n  T2 extends T1['type']\n> = T1 extends { type: T2 } ? T1 : never;\n"
  },
  {
    "path": "src/utils/date.ts",
    "content": "/* eslint-disable */\n/* tslint:disable:only-arrow-functions */\n\n// Date class extension\n\n// reference :\n// http://stackoverflow.com/questions/5495815/javascript-code-for-showing-yesterdays-date-and-todays-date?answertab=votes#tab-top\n// http://stackoverflow.com/questions/9192956/getting-previous-date-using-javascript?answertab=votes#tab-top\n// http://stackoverflow.com/a/43020185\n\nexport const _ = '';\n\nDate.prototype.getDayCn = function () {\n  const days_full: DayCn[] = ['日', '一', '二', '三', '四', '五', '六'];\n  return days_full[this.getDay()];\n};\n\nDate.prototype.addDays = function (n: number) {\n  const date = new Date();\n  const time = this.getTime();\n  const changedDate = new Date(time + n * 24 * 60 * 60 * 1000);\n  date.setTime(changedDate.getTime());\n  return date;\n};\n\nDate.prototype.addMonths = function (n: number) {\n  const month = this.getMonth();\n  const lastYear = month === 0 && n < 0;\n  const nextYear = month === 11 && n > 0;\n  if (lastYear) {\n    return new Date(this.getFullYear() + n, 11, 1);\n  } else if (nextYear) {\n    return new Date(this.getFullYear() + n, 0, 1);\n  } else {\n    return new Date(this.getFullYear(), month + n, 1);\n  }\n};\n\n// Provide month names\nDate.prototype.getMonthName = function () {\n  const month_names: MonthFullName[] = [\n    'January',\n    'February',\n    'March',\n    'April',\n    'May',\n    'June',\n    'July',\n    'August',\n    'September',\n    'October',\n    'November',\n    'December'\n  ];\n  return month_names[this.getMonth()];\n};\n\n// Provide month abbreviation\nDate.prototype.getMonthAbbr = function () {\n  const month_abbrs: MonthAbbr[] = [\n    'Jan',\n    'Feb',\n    'Mar',\n    'Apr',\n    'May',\n    'Jun',\n    'Jul',\n    'Aug',\n    'Sep',\n    'Oct',\n    'Nov',\n    'Dec'\n  ];\n  return month_abbrs[this.getMonth()];\n};\n\n// Provide full day of week name\nDate.prototype.getDayFull = function () {\n  const days_full: DayFullName[] = [\n    'Sunday',\n    'Monday',\n    'Tuesday',\n    'Wednesday',\n    'Thursday',\n    'Friday',\n    'Saturday'\n  ];\n  return days_full[this.getDay()];\n};\n\n// Provide full day of week name\nDate.prototype.getDayAbbr = function () {\n  const days_abbr: DayAbbr[] = [\n    'Sun',\n    'Mon',\n    'Tue',\n    'Wed',\n    'Thur',\n    'Fri',\n    'Sat'\n  ];\n  return days_abbr[this.getDay()];\n};\n\n// Provide the day of year 1-365\nDate.prototype.getDayOfYear = function () {\n  const onejan = new Date(this.getFullYear(), 0, 1);\n  return Math.ceil((Number(this) - Number(onejan)) / 86400000);\n};\n\n// Provide the day suffix (st,nd,rd,th)\nDate.prototype.getDaySuffix = function () {\n  const d = this.getDate();\n  const sfx: DaySuffix[] = ['th', 'st', 'nd', 'rd'];\n  const val = d % 100;\n  return sfx[(val - 20) % 10] || sfx[val] || sfx[0];\n};\n\n// Provide Week of Year\nDate.prototype.getWeekOfYear = function () {\n  const onejan = new Date(this.getFullYear(), 0, 1);\n  return Math.ceil(\n    ((Number(this) - Number(onejan)) / 86400000 + onejan.getDay() + 1) / 7\n  );\n};\n\n// Provide if it is a leap year or not\nDate.prototype.isLeapYear = function () {\n  const yr = String(this.getFullYear());\n  if (parseInt(yr, 10) % 4 === 0) {\n    if (parseInt(yr, 10) % 100 === 0) {\n      if (parseInt(yr, 10) % 400 !== 0) {\n        return false;\n      }\n      if (parseInt(yr, 10) % 400 === 0) {\n        return true;\n      }\n    }\n    if (parseInt(yr, 10) % 100 !== 0) {\n      return true;\n    }\n  }\n  if (parseInt(yr, 10) % 4 !== 0) {\n    return false;\n  }\n\n  return false;\n};\n\n// Provide Number of Days in a given month\nDate.prototype.getMonthDayCount = function () {\n  const month_day_counts = [\n    31,\n    this.isLeapYear() ? 29 : 28,\n    31,\n    30,\n    31,\n    30,\n    31,\n    31,\n    30,\n    31,\n    30,\n    31\n  ];\n  return month_day_counts[this.getMonth()];\n};\n\nDate.prototype.compare = function (dateObj: Date) {\n  const date = dateObj.getDate();\n  const month = dateObj.getMonth();\n  const year = dateObj.getFullYear();\n  const curMonth = this.getMonth();\n  const curDate = this.getDate();\n  const sameYear = this.getFullYear() === year;\n  const sameMonth = curMonth === month && sameYear;\n  const sameDate = curDate === date && sameMonth;\n  const lastMonth = curMonth === 1 ? month === 12 : month < curMonth;\n  const nextMonth = curMonth === 12 ? month === 1 : month > curMonth;\n\n  return {\n    sameYear,\n    sameDate,\n    sameMonth,\n    lastMonth,\n    nextMonth\n  };\n};\n\nDate.prototype.dayDiff = function (d: Date = new Date()) {\n  return Math.floor((d.getTime() - this.getTime()) / 1000 / 60 / 60 / 24);\n};\n\nDate.prototype.isToday = function () {\n  return this.dayDiff(new Date()) === 0;\n};\n\nDate.prototype.toISODateString = function () {\n  const str = this.toISOString();\n  return (\n    str.substring(0, str.indexOf('T')) +\n    str.substring(str.indexOf('T')).replace(/\\d/g, '0')\n  );\n};\n\n// format provided date into this.format format\nDate.prototype.format = function (dateFormat_: string) {\n  // break apart format string into array of characters\n  const dateFormat = dateFormat_.split('');\n  const date = this.getDate();\n  const month = this.getMonth();\n  const hours = this.getHours();\n  const minutes = this.getMinutes();\n  const seconds = this.getSeconds();\n  // get all date properties ( based on PHP date object functionality )\n  const date_props = {\n    d: date < 10 ? '0' + date : date,\n    D: this.getDayAbbr(),\n    j: this.getDate(),\n    l: this.getDayFull(),\n    S: this.getDaySuffix(),\n    w: this.getDay(),\n    z: this.getDayOfYear(),\n    W: this.getWeekOfYear(),\n    F: this.getMonthName(),\n    m: month < 9 ? '0' + (month + 1) : month + 1,\n    M: this.getMonthAbbr(),\n    n: month + 1,\n    t: this.getMonthDayCount(),\n    L: this.isLeapYear() ? '1' : '0',\n    Y: this.getFullYear(),\n    y: this.getFullYear() + ''.substring(2, 4),\n    a: hours > 12 ? 'pm' : 'am',\n    A: hours > 12 ? 'PM' : 'AM',\n    g: hours % 12 > 0 ? hours % 12 : 12,\n    G: hours > 0 ? hours : '12',\n    h: hours % 12 > 0 ? hours % 12 : 12,\n    H: hours,\n    i: minutes < 10 ? '0' + minutes : minutes,\n    s: seconds < 10 ? '0' + seconds : seconds\n  };\n  // loop through format array of characters and add matching data else add the format character (:,/, etc.)\n  let date_string = '';\n  for (const f of dateFormat) {\n    if (f.match(/[a-zA-Z]/g)) {\n      // @ts-ignore\n      date_string += date_props[f] ? date_props[f] : '';\n    } else {\n      date_string += f;\n    }\n  }\n  return date_string;\n};\n"
  },
  {
    "path": "src/utils/form/form.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport React, { ReactElement, ReactNode } from 'react';\nimport RcForm, { Field as RcField, useForm as RcUseForm } from 'rc-field-form';\nimport { FormProps as RcFormProps } from 'rc-field-form/es/Form';\nimport { FieldProps as RcFieldProps } from 'rc-field-form/es/Field';\nimport { Meta, FieldError, Store } from 'rc-field-form/lib/interface';\nimport { Validator, compose as composeValidator } from './validators';\nimport { NamePath, Paths, PathType, DeepPartial, Control } from './typings';\n\ntype HTMLDivProps = React.DetailedHTMLProps<\n  React.HTMLAttributes<HTMLDivElement>,\n  HTMLDivElement\n>;\n\ntype HTMLLabelProps = React.DetailedHTMLProps<\n  React.LabelHTMLAttributes<HTMLLabelElement>,\n  HTMLLabelElement\n>;\n\nexport interface FieldData<S extends {} = Store, Name = NamePath<S>>\n  extends Partial<Omit<Meta, 'name'>> {\n  name: Name;\n  value?: Name extends Paths<S>\n    ? PathType<S, Name>\n    : Name extends keyof S\n    ? S[Name]\n    : undefined;\n}\n\nexport type FormInstance<S extends {} = Store> = {\n  getFieldValue<K extends keyof S>(name: K): S[K];\n  getFieldValue<T extends Paths<S>>(name: T): PathType<S, T>;\n  getFieldsValue(nameList?: NamePath<S>[]): S;\n  getFieldError(name: NamePath<S>): string[];\n  getFieldsError(nameList?: NamePath<S>[]): FieldError[];\n  isFieldsTouched(\n    nameList?: NamePath<S>[],\n    allFieldsTouched?: boolean\n  ): boolean;\n  isFieldsTouched(allFieldsTouched?: boolean): boolean;\n  isFieldTouched(name: NamePath<S>): boolean;\n  isFieldValidating(name: NamePath<S>): boolean;\n  isFieldsValidating(nameList: NamePath<S>[]): boolean;\n  resetFields(fields?: NamePath<S>[]): void;\n  setFields(fields: FieldData<S, keyof S | NamePath<S>>[]): void;\n  setFieldsValue(value: DeepPartial<S>): void;\n  validateFields<K extends keyof S>(nameList?: NamePath<K>[]): Promise<S>;\n  submit: () => void;\n};\n\nexport interface FormProps<S extends {} = Store, V = S>\n  extends Omit<RcFormProps, 'form' | 'onFinish' | 'onValuesChange'> {\n  form?: FormInstance<S>;\n  initialValues?: DeepPartial<V>;\n  onFinish?: (values: V) => void;\n  onValuesChange?: (changes: DeepPartial<S>, values: S) => void;\n  ref?: React.Ref<FormInstance<S>>;\n  transoformInitialValues?: (payload: DeepPartial<V>) => DeepPartial<S>;\n  beforeSubmit?: (payload: S) => V;\n}\n\ntype OmititedRcFieldProps = Omit<\n  RcFieldProps,\n  'name' | 'dependencies' | 'children' | 'rules'\n>;\n\nexport interface FormItemLabelProps extends HTMLDivProps {\n  label?: ReactNode;\n}\n\ninterface BasicFormItemProps<S extends {} = Store>\n  extends OmititedRcFieldProps {\n  name?: NamePath<S>;\n  children?: ReactElement | ((value: S, fields: FieldData<S>) => ReactElement);\n  validators?:\n    | Array<Validator | null>\n    | ((value: S) => Array<Validator | null>);\n  label?: ReactNode;\n  noStyle?: boolean;\n  className?: string;\n}\n\ntype Deps<S> = Array<NamePath<S>>;\ntype FormItemPropsDeps<S extends {} = Store> =\n  | {\n      deps?: Deps<S>;\n      children?: ReactElement;\n      validators?: Array<Validator | null>;\n    }\n  | {\n      deps: Deps<S>;\n      validators: (value: S) => Array<Validator | null>;\n    }\n  | {\n      deps: Deps<S>;\n      children: (value: S, fields: FieldData<S>) => ReactElement;\n    };\n\nexport type FormItemProps<S extends {} = Store> = BasicFormItemProps<S> &\n  FormItemPropsDeps<S>;\n\nexport interface FormItemClassName {\n  item?: string;\n  label?: string;\n  error?: string;\n  touched?: string;\n  validating?: string;\n  help?: string;\n}\n\ntype Rule = NonNullable<RcFieldProps['rules']>[number];\n\nconst getValues = (obj: any, paths: (string | number)[]) =>\n  paths.reduce((result, key) => result && result[key], obj);\n\nexport function createShouldUpdate(\n  names: Array<string | number | (string | number)[]> = []\n): RcFieldProps['shouldUpdate'] {\n  return (prev, curr) => {\n    for (const name of names) {\n      const paths = Array.isArray(name) ? name : [name];\n      if (getValues(prev, paths) !== getValues(curr, paths)) {\n        return true;\n      }\n    }\n    return false;\n  };\n}\n\nconst defaultFormItemClassName: Required<FormItemClassName> = {\n  item: 'rc-form-item',\n  label: 'rc-form-item-label',\n  error: 'rc-form-item-error',\n  touched: 'rc-form-item-touched',\n  validating: 'rc-form-item-validating',\n  help: 'rc-form-item-help'\n};\n\nexport function createForm<S extends {} = Store, V = S>({\n  itemClassName,\n  ...defaultProps\n}: Partial<FormItemProps<S>> & { itemClassName?: FormItemClassName } = {}) {\n  const ClassNames = { ...defaultFormItemClassName, ...itemClassName };\n\n  const FormItemLabel: React.FC<FormItemLabelProps> = ({\n    className = defaultProps.className,\n    children,\n    label,\n    ...props\n  }) =>\n    React.createElement<HTMLDivProps>(\n      'div',\n      {\n        ...props,\n        className: [className, ClassNames.item].filter(Boolean).join(' ').trim()\n      },\n      React.createElement<HTMLLabelProps>(\n        'label',\n        { className: ClassNames.label },\n        label\n      ),\n      children\n    );\n\n  const FormItem = (itemProps: FormItemProps<S>) => {\n    const {\n      name,\n      children,\n      validators = [],\n      deps = [],\n      noStyle,\n      label,\n      className = '',\n      ...props\n    } = {\n      ...defaultProps,\n      ...itemProps\n    } as FormItemProps<S> & {\n      deps?: Array<string | number | (string | number)[]>;\n      name: string | number;\n    };\n\n    const rules: Rule[] = [\n      typeof validators === 'function'\n        ? ({ getFieldsValue }) => ({\n            validator: composeValidator(validators(getFieldsValue(deps) as S))\n          })\n        : { validator: composeValidator(validators) }\n    ];\n\n    return React.createElement(\n      RcField,\n      {\n        name,\n        rules,\n        ...(deps.length\n          ? { dependencies: deps, shouldUpdate: createShouldUpdate(deps) }\n          : {}),\n        ...props\n      },\n      (\n        control: Control<unknown>,\n        fields: FieldData<S>,\n        form: FormInstance<S>\n      ) => {\n        const { getFieldsValue } = form;\n        const { touched, validating, errors } = fields;\n\n        const childNode =\n          typeof children === 'function'\n            ? children(getFieldsValue(deps), fields)\n            : name\n            ? React.cloneElement(children as React.ReactElement, {\n                ...control\n              })\n            : children;\n\n        if (noStyle) {\n          return childNode;\n        }\n\n        const error = errors && errors[0];\n\n        return React.createElement<FormItemLabelProps>(\n          FormItemLabel,\n          {\n            label,\n            className: [\n              className,\n              error && ClassNames.error,\n              touched && ClassNames.touched,\n              validating && ClassNames.validating\n            ]\n              .filter(Boolean)\n              .join(' ')\n              .trim()\n          },\n          childNode,\n          React.createElement<HTMLDivProps>(\n            'div',\n            { className: ClassNames.help },\n            error\n          )\n        );\n      }\n    );\n  };\n\n  const Form = React.forwardRef<FormInstance<S>, FormProps<S, V>>(\n    (\n      {\n        children,\n        onFinish,\n        beforeSubmit,\n        initialValues,\n        transoformInitialValues,\n        ...props\n      },\n      ref\n    ) =>\n      React.createElement(\n        RcForm,\n        {\n          ...props,\n          ref,\n          initialValues:\n            initialValues && transoformInitialValues\n              ? transoformInitialValues(initialValues)\n              : initialValues,\n          onFinish:\n            onFinish &&\n            ((store: unknown) => {\n              onFinish(beforeSubmit ? beforeSubmit(store as S) : (store as V));\n            })\n        } as RcFormProps,\n        children\n      )\n  );\n\n  const useForm: () => [FormInstance<S>] = RcUseForm as any;\n\n  return {\n    Form,\n    FormItem,\n    FormList: RcForm.List,\n    FormProvider: RcForm.FormProvider,\n    FormItemLabel,\n    useForm\n  };\n}\n\nexport const {\n  Form,\n  FormItem,\n  FormItemLabel,\n  FormList,\n  useForm,\n  FormProvider\n} = createForm();\n"
  },
  {
    "path": "src/utils/form/index.ts",
    "content": "import * as validators from './validators';\n\nexport { validators };\nexport * from './form';\nexport * from './typings';\n"
  },
  {
    "path": "src/utils/form/typings.ts",
    "content": "type ValueOf<T> = T[keyof T];\n\ntype Cons<H, T> = T extends readonly any[]\n  ? ((h: H, ...t: T) => void) extends (...r: infer R) => void\n    ? R\n    : never\n  : never;\n\ntype Prev = [\n  never,\n  0,\n  1,\n  2,\n  3,\n  4,\n  5,\n  6,\n  7,\n  8,\n  9,\n  10,\n  11,\n  12,\n  13,\n  14,\n  15,\n  16,\n  17,\n  18,\n  19,\n  20,\n  ...0[]\n];\n\n// https://stackoverflow.com/a/58436959\nexport type Paths<T, D extends number = 10> = [D] extends [never]\n  ? never\n  : T extends any[] // for array\n  ? [number]\n  : T extends object // eslint-disable-line @typescript-eslint/ban-types\n  ? {\n      [K in keyof T]-?: K extends keyof T[K] & keyof ValueOf<T[K]>\n        ? (keyof T)[] // this turn [string, string, string] into string[]\n        :\n            | [K]\n            | (Paths<T[K], Prev[D]> extends infer P\n                ? P extends []\n                  ? never\n                  : Cons<K, P>\n                : never);\n    }[keyof T]\n  : [];\n\nexport type DeepPartial<T> = T extends any[] | (() => void)\n  ? T\n  : T extends object // eslint-disable-line @typescript-eslint/ban-types\n  ? {\n      [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]; // eslint-disable-line @typescript-eslint/ban-types\n    }\n  : T;\n\ninterface NextInt {\n  0: 1;\n  1: 2;\n  2: 3;\n  3: 4;\n  4: 5;\n  [rest: number]: number;\n}\n\nexport type PathType<T, P extends any[], Index extends keyof P & number = 0> = {\n  [K in keyof P & number & Index]: P[K] extends undefined\n    ? T\n    : P[K] extends keyof T\n    ? NextInt[K] extends keyof P & number\n      ? PathType<T[P[K]], P, Extract<NextInt[K], keyof P & number>>\n      : T[P[K]]\n    : never;\n}[Index];\n\nexport type NamePath<T, D extends number = 4> = keyof T | Paths<T, D>;\n\nexport interface Control<T = any> {\n  value?: T;\n  onChange?: (value?: T) => void;\n}\n\nexport interface ControlProps<T = unknown> extends Control<T> {\n  name?: (string | number)[];\n}\n"
  },
  {
    "path": "src/utils/form/validators.ts",
    "content": "export type Validator = (rule: any, value: any) => Promise<void>;\n\nexport const compose = (validators: Array<Validator | null>): Validator => {\n  return async (rule, value) => {\n    for (const validator of validators) {\n      if (validator) {\n        try {\n          await validator(rule, value);\n        } catch (err) {\n          return Promise.reject(err);\n        }\n      }\n    }\n    return Promise.resolve();\n  };\n};\n\nexport const required = (msg: string): Validator => (_, value) => {\n  const val = typeof value === 'string' ? value.trim() : value;\n  const valid = !!(Array.isArray(val)\n    ? val.length\n    : typeof value === 'boolean' // for checkbox\n    ? value\n    : !(typeof val === 'undefined' || val === null || val === ''));\n  return valid ? Promise.resolve() : Promise.reject(msg);\n};\n\nexport const number: Validator = (_, value) =>\n  !value || /^-?\\d+\\.?\\d*$/.test(value)\n    ? Promise.resolve()\n    : Promise.reject('Plase input number only');\n\nexport const integer = (\n  msg: string = 'Plase input integer only'\n): Validator => (_, value) =>\n  value === '' || isNaN(Number(value)) || /^(-)?\\d*$/.test(value)\n    ? Promise.resolve()\n    : Promise.reject(msg);\n\nexport const maxDecimal = (max: number): Validator => (_, value) =>\n  value === '' ||\n  isNaN(Number(value)) ||\n  new RegExp(`^(\\\\d+|\\\\d+\\\\.\\\\d{0,${max}})$`).test(value)\n    ? Promise.resolve()\n    : Promise.reject(`Should not more then ${max} decimal`);\n\nconst numberComparation = (\n  callback: (value: number, flag: number) => boolean\n) => (flag: number, msg: string, inclusive = false) => {\n  const validator: Validator = (_, value) => {\n    const num = Number(value);\n    return value === '' ||\n      isNaN(num) ||\n      callback(num, flag) ||\n      (inclusive && num === flag)\n      ? Promise.resolve()\n      : Promise.reject(msg);\n  };\n  return validator;\n};\n\nexport const min = numberComparation((value, flag) => value > flag);\nexport const max = numberComparation((value, flag) => value < flag);\n\nconst lengthComparation = (\n  callback: (length: number, flag: number) => boolean\n) => (flag: number, msg: string) => {\n  const validator: Validator = (_, value) => {\n    if (Array.isArray(value) || typeof value === 'string') {\n      return callback(value.length, flag)\n        ? Promise.resolve()\n        : Promise.reject(msg);\n    }\n    return Promise.resolve();\n  };\n  return validator;\n};\n\nexport const minLength = lengthComparation(\n  (length, minLength) => length >= minLength\n);\n\nexport const maxLength = lengthComparation(\n  (length, maxLength) => length <= maxLength\n);\n\nexport const passwordFormat = (\n  msg: string = 'Password must contain number and english character'\n): Validator => (_, value) =>\n  /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z_]{6,20}$/.test(value)\n    ? Promise.resolve()\n    : Promise.reject(msg);\n\nexport const shouldBeEqual = (val: any, msg: string): Validator => (_, value) =>\n  value === val ? Promise.resolve() : Promise.reject(msg);\n\nexport const shouldNotBeEqual = (val: any, msg: string): Validator => (\n  _,\n  value\n) => (value !== val ? Promise.resolve() : Promise.reject(msg));\n"
  },
  {
    "path": "src/utils/nprogress.ts",
    "content": "import NProgress from 'nprogress';\nimport 'nprogress/nprogress.css';\n\nNProgress.configure({\n  parent: '#root',\n  easing: 'ease',\n  showSpinner: false,\n  trickleSpeed: 50\n});\n\nexport { NProgress };\n\nexport const NProgressDone = () => NProgress.done();\n\nexport default NProgress;\n"
  },
  {
    "path": "src/utils/uuid.ts",
    "content": "export class UUID {\n  private count: number = 0;\n\n  next() {\n    this.count++;\n    // to string avoid conflict with index\n    return String(this.count);\n  }\n\n  reset() {\n    this.count = 0;\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"pretty\": true,\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"include\": [\"src\", \"common.d.ts\"],\n  \"exclude\": [\"node_modules\", \"**/node_modules/*\"]\n}\n"
  }
]