From 30b9424c10c0fc0cd9715eba01d6422109ad0286 Mon Sep 17 00:00:00 2001 From: Darkhan Nausharipov <31556582+nausharipov@users.noreply.github.com> Date: Mon, 24 Apr 2023 21:16:12 +0600 Subject: [PATCH] [Tour of Beam] Save code, Delete account & other tasks (#26010) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Squashed commit of the following: commit 9f42763a819ae82ae7fc7fe495d43cabae00df25 Merge: 6b8ba1afc7 04c2de61e5 Author: darkhan.nausharipov Date: Tue Mar 14 20:05:42 2023 +0600 Merge branch 'master' into issue25255-delete-account commit 6b8ba1afc7155f2357ca3a5c5f23394f770e2ae7 Author: darkhan.nausharipov Date: Wed Feb 22 21:41:34 2023 +0600 flutter v3.7 support (#25255) commit dc44ba77db8edd7fdf063ed88ee97e7c8e45bc8d Merge: 3f3747cfa0 8cfee7d05e Author: darkhan.nausharipov Date: Wed Feb 22 21:34:45 2023 +0600 Merge branch 'master' into issue25255-delete-account commit 3f3747cfa0d48e0a5df367894269a0840b615156 Author: darkhan.nausharipov Date: Tue Feb 14 18:28:43 2023 +0600 comment fixes (#25255) commit a5110e4ef360e3957c3e539ec11fb5a0124838f7 Author: darkhan.nausharipov Date: Tue Feb 14 11:00:55 2023 +0600 progress dialog is undismissible (#25255) commit 2cb7c585da3ebe0fb599dca5ce6887bfa0f65310 Author: darkhan.nausharipov Date: Mon Feb 13 22:50:18 2023 +0600 updated to app_state 0.9.2 (#25255) commit 7626f8fef3ea42ba792cf3a9869dfb36a827191c Author: darkhan.nausharipov Date: Mon Feb 13 22:40:25 2023 +0600 pop progress dialog with navigatorKey (#25255) commit 12882fbce983e26547c06bc23e10f35adde87e2b Author: darkhan.nausharipov Date: Fri Feb 10 21:46:08 2023 +0600 addressing review (#25255) commit 440d0b04e913d368bdb8bc46f018dee74eca5a63 Author: darkhan.nausharipov Date: Fri Feb 10 21:17:03 2023 +0600 addressing review comments (#25255) commit 6005103ac778669665518c7312ed726a27c99d8d Author: darkhan.nausharipov Date: Fri Feb 10 15:13:54 2023 +0600 addressing review comments (#25255) commit fde6ace2e43ecc559426e733faea49dbb491ec7b Author: darkhan.nausharipov Date: Thu Feb 9 21:18:15 2023 +0600 showOverlay (#25255) commit 09a5526aa9f415081914e60b0737ceeef2686502 Author: darkhan.nausharipov Date: Thu Feb 9 20:08:48 2023 +0600 addException in deleteAccount (#25255) commit a2f43bf4d5850032306192490bc485e8e70aaa45 Author: darkhan.nausharipov Date: Thu Feb 9 20:00:04 2023 +0600 BeamRouterDelegate for showing toasts (#25255) commit f1a2cecd9369dfe98cd1fa635fa1aef700f0a3a5 Author: darkhan.nausharipov Date: Thu Feb 9 19:54:12 2023 +0600 comment fixes (#25255) commit 8c3e4b5205e257edf2efc6e2faf44d2d9a15d7ab Author: darkhan.nausharipov Date: Thu Feb 9 14:40:07 2023 +0600 toastNotifier todo (#25255) commit eda3f16254e6bebb503359efd2c62a4b0bd48091 Author: darkhan.nausharipov Date: Thu Feb 9 14:39:43 2023 +0600 showProgressOverlay (#25255) commit 2ab46ec1199414d3a2335d2d00523f15446a5124 Author: darkhan.nausharipov Date: Wed Feb 8 19:07:00 2023 +0600 GitHub auth untested (#25255) commit 17b8c429d59842e04f2ff6b072fd9f0674e56a9f Author: darkhan.nausharipov Date: Tue Feb 7 14:51:56 2023 +0600 comment fixes (#25255) commit bc4d930945bc76e261c7a815ff54961b862032ca Author: darkhan.nausharipov Date: Mon Feb 6 23:05:50 2023 +0600 todo (#25255) commit d67cf474ceb5932b996438953536a87a355a6077 Author: darkhan.nausharipov Date: Mon Feb 6 22:51:55 2023 +0600 pre-review fixes (#25255) commit eda0bfec9a04d2a26d923085410dde66fb3fe235 Author: darkhan.nausharipov Date: Mon Feb 6 22:39:12 2023 +0600 delete my account (#25255) commit a34b1701c822cde7c41d7e3b98947cc5d1124a05 Merge: aee2c844ad f9f8c3ad6a Author: darkhan.nausharipov Date: Mon Feb 6 21:05:19 2023 +0600 hint warning (#25116) commit f9f8c3ad6a3a3e4ddecda49807066aeae4787223 Author: darkhan.nausharipov Date: Mon Feb 6 21:03:21 2023 +0600 text and popup (#25116) * Squashed commit of the following: commit 95c82ec5be90e64f8e62072d4b7742db77b0558f Merge: 7066061152 c796d856f7 Author: Darkhan Nausharipov <31556582+nausharipov@users.noreply.github.com> Date: Tue Mar 28 16:07:43 2023 +0600 Merge pull request #441 from akvelon/issue25283-tobf-sucfuu [ToB] [Frontend] Save code for unauthenticated users commit c796d856f74dfb3d3371c9421e2c09eb4a5196a8 Author: darkhan.nausharipov Date: Tue Mar 28 16:05:08 2023 +0600 _isSnippetTypeSavable (#25283) commit 7b487a0b4c2fd83512bc2da53bf403d60520ae3d Author: darkhan.nausharipov Date: Tue Mar 28 15:49:29 2023 +0600 disabled saving solution code (#25283) commit be3e3460527263146eb13c440056d553ae867938 Author: Alexey Inkin Date: Tue Mar 28 13:22:05 2023 +0400 Improve comments, remove unused code (#25283) commit 3434381ed159e646949e5e736a491b95e0cad66d Author: darkhan.nausharipov Date: Mon Mar 27 20:28:39 2023 +0600 getSavedDescriptor (#25283) commit 7dbb6b44dd7dcb5e0835f62d4d9454b4c5cb7c9a Author: darkhan.nausharipov Date: Mon Mar 27 20:28:07 2023 +0600 _getStandardOrEmptyDescriptor (#25283) commit b9d76e61660b4180efed79cfb19470aad84e7e00 Author: darkhan.nausharipov Date: Mon Mar 27 18:19:49 2023 +0600 comments (#25283) commit cc7ff54d6c231285dc38eb048da753cb7ddf59a8 Merge: 8410493f38 7066061152 Author: darkhan.nausharipov Date: Fri Mar 24 21:23:19 2023 +0600 Merge branch 'issue24538-save-user-code' into issue25283-tobf-sucfuu commit 8410493f388ab91f42bb04c26a087331dc25f2e5 Author: darkhan.nausharipov Date: Fri Mar 24 20:37:49 2023 +0600 comments (#25283) commit 7066061152a1415297be5b1fb4729a3322b9676f Merge: 879795b632 ddae966f33 Author: darkhan.nausharipov Date: Fri Mar 24 20:03:34 2023 +0600 Merge branch 'master' into issue24538-save-user-code commit 8575c6e8f1f4a837c62403e941cf31e1511cf290 Author: darkhan.nausharipov Date: Fri Mar 24 12:33:28 2023 +0600 addressing comments (#25283) commit 172f18f33dc5648e0d5a6105430a10e2baaca8fb Author: darkhan.nausharipov Date: Mon Mar 20 19:26:20 2023 +0600 comment fixes (#25283) commit 099b0f19955dc119e0de53a098d424d4cf2d6ad3 Author: darkhan.nausharipov Date: Fri Mar 17 13:28:42 2023 +0600 changes for demo (#25283) commit 426d43ee066b02816ceef4deab62f96ee855fdc8 Author: darkhan.nausharipov Date: Wed Mar 15 21:42:15 2023 +0600 untested multi-file saving (#25283) commit 879795b632f5863ae9f477c30f40cb18164cb205 Merge: 1521ab9d93 04c2de61e5 Author: darkhan.nausharipov Date: Tue Mar 14 20:39:29 2023 +0600 Merge branch 'master' into issue24538-save-user-code commit 8c7807bbb47906cfc4d68c6a30dc740325688bf5 Author: darkhan.nausharipov Date: Thu Mar 2 20:24:01 2023 +0600 setUnit & HiveLocalStorageCache (#25283) commit 6b54331285e912d3e989ffcc6d7a5c446616ea2f Author: darkhan.nausharipov Date: Tue Feb 28 18:32:03 2023 +0600 unfinished ContentExampleLoadingDescriptor (#25283) commit 20e097ef00ab04cc25c4fca0598de4d4cd6a7b9a Author: darkhan.nausharipov Date: Mon Feb 27 12:03:59 2023 +0600 deleted google_sign_in from pubspec (#25283) commit bdf6f4948f9a9a8624f2c4417dd9645b6e188f7e Merge: f755775829 1521ab9d93 Author: darkhan.nausharipov Date: Mon Feb 27 12:01:19 2023 +0600 Merge branch 'issue24538-save-user-code' into issue25283-tobf-sucfuu commit 1521ab9d93f1ea1d964a58ac0f7b87213702cd01 Author: darkhan.nausharipov Date: Thu Feb 23 19:36:50 2023 +0600 CodeBlock widget (#25529) commit b638355e33cfbc1cd73e8ccaf7b8b65dfa6c7184 Author: darkhan.nausharipov Date: Thu Feb 23 19:34:00 2023 +0600 added missing notifyListeners (#24538) commit 5d22b4cda7a944cbbd650e6673ed55d63f4b5dc1 Author: darkhan.nausharipov Date: Thu Feb 23 19:33:01 2023 +0600 scrollbar in markdown code block (#25529) commit f7557758290b48fda83b8abaf6487bce36ea6c5e Author: darkhan.nausharipov Date: Wed Feb 22 11:37:58 2023 +0600 resolve deprecation warnings todo (#25283) commit 56264acd2da1a565f3dbb98dab1a41dff54ef54b Author: darkhan.nausharipov Date: Wed Feb 22 11:30:22 2023 +0600 flutter v3.7 support (#24538) commit 08989ab7e0a8bf7623aff19de10e22b38ecb20dd Merge: 9e3666c706 40838f7644 Author: darkhan.nausharipov Date: Wed Feb 22 11:00:47 2023 +0600 Merge branch 'master' into issue24538-save-user-code commit 6860d1b367e739abc73e35e48774869ae016872f Author: darkhan.nausharipov Date: Mon Feb 20 11:39:36 2023 +0600 Flutter v3.7.3 support for ToB (#25283) commit 9e3666c706ff3bc6a3a02ccaf33738a116820502 Author: darkhan.nausharipov Date: Wed Feb 8 17:29:53 2023 +0600 set sdkId to fix a bug (#24538) commit 5381d9e0c10d29e2e605c941d0b74d0db16a7b27 Author: darkhan.nausharipov Date: Mon Feb 6 13:37:00 2023 +0600 _Buttons padding (#24538) commit 4a419a611c8e7e28e178fb0488bd308d6c52add8 Author: darkhan.nausharipov Date: Mon Feb 6 11:26:47 2023 +0600 moved hints to the left (#24538) commit 5637c4171a65ccb4a87019b492245b8cdaec38a5 Author: darkhan.nausharipov Date: Mon Feb 6 11:22:07 2023 +0600 scrollable hints (#24538) commit c7c51b1234b87fe374bce6f9b7dc27d299605b00 Author: darkhan.nausharipov Date: Mon Feb 6 11:00:15 2023 +0600 commit to relaunch RAT check (#24538) commit 8eed5d3174fdf193b9a4c62631e3687c2c505d6f Author: darkhan.nausharipov Date: Fri Feb 3 14:53:06 2023 +0600 comment fixes (#24538) commit 7f943d51dcc975f39dc666b19e107c8956fb2048 Author: darkhan.nausharipov Date: Fri Feb 3 12:48:46 2023 +0600 organized TourNotifier (#24538) commit 52227d538363a74fb96eb7410eb1d606ef34429f Author: darkhan.nausharipov Date: Fri Feb 3 12:37:55 2023 +0600 deleted unitProgressByUnitId getter (#24538) commit 4e3d3cfe134688a8e14813092d1819e97a552b01 Author: darkhan.nausharipov Date: Fri Feb 3 12:31:54 2023 +0600 alphabetic order (#24538) commit bfc759666b71f4c265285633f1b57bb3e549eabd Author: darkhan.nausharipov Date: Fri Feb 3 12:26:10 2023 +0600 getUnitSavedSnippetId (#24538) commit a836d0180e328d0bcecf614eb7178734422446ff Author: darkhan.nausharipov Date: Thu Feb 2 18:57:05 2023 +0600 comment fixes(#24538) commit 01611b02f9947d4401ed056528b92e707c619a27 Author: darkhan.nausharipov Date: Wed Feb 1 20:26:36 2023 +0600 comment fixes (#24538) comment fixes (#24538) commit fff0bfa7e4f3e68ae5ac6e7191f7600b22236dca Author: darkhan.nausharipov Date: Fri Jan 27 01:29:59 2023 +0600 save code status (#24538) commit d4401c9156a8599cb44b2f92e189906615eff5cf Author: darkhan.nausharipov Date: Thu Jan 26 14:14:18 2023 +0600 comment fixes (#24538) commit f3d1c0f66fad5072008434516ec93bb23bf1f138 Author: darkhan.nausharipov Date: Thu Jan 26 10:48:50 2023 +0600 snippet type enum switcher (#24538) commit 25dec321066e89e2ce9f1f1574b42d002c61fa80 Author: darkhan.nausharipov Date: Wed Jan 25 00:48:14 2023 +0600 comment fixes (#24538) commit f2d2f8fa304b19c6ebf7411745365e5cb59c3557 Author: darkhan.nausharipov Date: Mon Jan 23 21:32:19 2023 +0600 reverted to unitProgress (#24538) commit a5310e0eb21d00dc8e3256562e5530bc21fa496c Author: darkhan.nausharipov Date: Mon Jan 23 21:28:40 2023 +0600 missing notifyListeners (#24538) commit 18ecd23b17cc9397311c2a0a51bbef25c62e2e0b Author: darkhan.nausharipov Date: Mon Jan 23 21:26:52 2023 +0600 reset is shown immediately (#24538) commit ea8acc0d68528fd435a2134feb1ad35076114fa4 Author: darkhan.nausharipov Date: Mon Jan 23 18:58:41 2023 +0600 removed comment after discussion (#24538) commit 92b4ec47c0526523da672f4472f2776a46e40de5 Author: darkhan.nausharipov Date: Mon Jan 23 09:06:57 2023 +0600 tested with show solution (#24538) commit ebce111cc242e6f2323717c6fda0761d1e69bdc9 Author: darkhan.nausharipov Date: Sun Jan 22 22:28:47 2023 +0600 set show solution false on reset (#24538) commit ece905cfb6d8abe5fc64f3f8d023699a17047fd5 Author: darkhan.nausharipov Date: Sun Jan 22 22:08:53 2023 +0600 pre-review fixes (#24538) commit 725e8d108678c0b9da1718a478468207c1e4c446 Author: darkhan.nausharipov Date: Fri Jan 20 19:06:51 2023 +0600 post-merge commit (#24538) commit b6cb6b93e92df4571df84bdbc524a815d7ca2308 Merge: eb23673d01 428ec97e30 Author: darkhan.nausharipov Date: Fri Jan 20 19:06:03 2023 +0600 Merge branch 'master' into issue24538-save-user-code commit eb23673d0178f90b313fb18c591505f8952eb24b Author: darkhan.nausharipov Date: Fri Jan 20 18:39:35 2023 +0600 setSaveCodeListener to activeFileController.codeController (#24538) commit d3b0085253c985e52351bf541587d447db08c94d Author: darkhan.nausharipov Date: Tue Jan 17 15:28:17 2023 +0600 merge fixes (#24538) commit 8cd106d00f6369b8dec1f92ca44190ded3378d30 Merge: a8502624ca a5e6d90081 Author: darkhan.nausharipov Date: Tue Jan 17 15:27:42 2023 +0600 Merge branch 'master' into issue24538-save-user-code commit a8502624ca74909e27e5f90508c0bea22619b382 Author: darkhan.nausharipov Date: Tue Dec 27 19:06:23 2022 +0600 getUnitSnippets in unit progress (#24538) commit 8dd70984cdb3aebd676c2d92f4508b3b0d6a7ae5 Author: darkhan.nausharipov Date: Thu Dec 8 22:33:32 2022 +0600 user menu without padding commit 90b9e3d49adf6c075fbdeef881ee161b109fd1df Author: darkhan.nausharipov Date: Thu Dec 8 22:32:37 2022 +0600 save user code barely works commit 44172c57c2d806b6521fc966febabb949e3e77a7 Merge: d442e63668 59d0fd3129 Author: darkhan.nausharipov Date: Thu Dec 8 14:15:37 2022 +0600 Merge branch 'issue24394-tobf-hint' into issue24538-tobf-save-user-code commit d442e63668b15330bd8a68fb18312f5cc06e5640 Author: darkhan.nausharipov Date: Wed Dec 7 15:22:57 2022 +0600 no duplicate slash with reg exp (#24538) commit e76860497a6983cd28e29f3d4065c9331ac32660 Author: darkhan.nausharipov Date: Tue Dec 6 10:11:12 2022 +0600 updated import (#24538) commit 63745d564ace082d34be5360cfd264cfaef6e86a Merge: d15913b289 af936d0b11 Author: darkhan.nausharipov Date: Tue Dec 6 10:02:57 2022 +0600 Merge branch 'issue23692_auth' into issue24538-tobf-save-user-code commit 59d0fd3129bcab3b55a66f671898ce6c5bcc84df Author: darkhan.nausharipov Date: Fri Dec 2 20:50:12 2022 +0600 solution managed inside of tour notifier (#24394) commit 5ca484661d330a0d7bea371a767f8d37402db520 Author: darkhan.nausharipov Date: Fri Dec 2 15:57:30 2022 +0600 show solution snippets (#24394) commit 5e3ac5857e6fbe585e8f6b43cca75265b59e25c2 Author: darkhan.nausharipov Date: Fri Dec 2 14:20:43 2022 +0600 renamed to unit content widget (#24394) commit 372b97fb5cd457125137c3ceb71332acecbc9117 Author: darkhan.nausharipov Date: Fri Dec 2 12:01:39 2022 +0600 group progress indicator size adjustment (#24394) commit 48ea57d7c97a817458741d7062955add3b758c76 Author: darkhan.nausharipov Date: Fri Dec 2 11:50:08 2022 +0600 title in unit content (#24394) commit 338c67317cdc9eec5a4046af9e5599737408baf2 Author: darkhan.nausharipov Date: Fri Dec 2 11:01:57 2022 +0600 extracted widgets, hints emptiness check (#24394) commit 36d3105f52fad0aa37ddaeaac340ee31a8a4e86b Author: darkhan.nausharipov Date: Thu Dec 1 21:12:05 2022 +0600 hint in dialog (#24394) commit af936d0b115bf42f21310719dc1b18e580010421 Author: darkhan.nausharipov Date: Thu Dec 1 14:59:52 2022 +0600 added firebase_options.dart into gradle rat exclusions & added a missing license (#23692) commit 9e1bbb081c575358445750694d27e393ebd72406 Author: darkhan.nausharipov Date: Thu Dec 1 14:02:46 2022 +0600 specific imports (#23692) commit 9720c134b5733fb439ae2b4df12bff07c359a95b Author: darkhan.nausharipov Date: Thu Dec 1 13:06:57 2022 +0600 moved dismissible overlay (#23692) commit 869b40cefa9222b5a61db0125f6c54340dfc918c Author: darkhan.nausharipov Date: Thu Dec 1 13:06:40 2022 +0600 extracted overlay body (#23692) commit c8db1ef778f401a9029c113a784faeb94919d660 Author: darkhan.nausharipov Date: Thu Dec 1 13:05:12 2022 +0600 refined open overlay (#23692) commit 8b639d08b3d6101ba9b13a6f6fe32b7cc7261559 Author: darkhan.nausharipov Date: Thu Dec 1 12:50:35 2022 +0600 all caches extend cache (#23692) commit a10795205507a66b8cd90ed8caaf112663a60c8c Author: darkhan.nausharipov Date: Thu Dec 1 12:47:19 2022 +0600 renamed user progress cache to unit progress (#23692) commit e215ec4c676e88d85fad3ba08e222ac19d8953b8 Author: darkhan.nausharipov Date: Wed Nov 30 10:37:05 2022 +0600 generated files commit 96e41f7d01c50e2cfa96fbc120fca69b3d29cb39 Merge: 1e38328e56 37fb90c98d Author: darkhan.nausharipov Date: Wed Nov 30 10:30:05 2022 +0600 Merge from master commit 1e38328e564f72b4e18439c60610902423223dbd Author: darkhan.nausharipov Date: Tue Nov 29 15:40:15 2022 +0600 removed "fix exception" todo after filing an issue (#23692) commit b6c4e3e66c30fa4cbdfbbefd1a41e649c32b9ecb Author: darkhan.nausharipov Date: Tue Nov 29 13:52:29 2022 +0600 added async (#23692) commit 27b26e41a81f07faac060c3d2e45f5b7bea19d5a Author: darkhan.nausharipov Date: Tue Nov 29 13:31:24 2022 +0600 clearUpdatingUnitId (#23692) commit 4b2d855920d6d63dfff4412fc9bdfb9ddbbdd695 Author: darkhan.nausharipov Date: Tue Nov 29 13:30:12 2022 +0600 renamed user progress model to unit progress (#23692) commit ef637403aa114f43f82ac005254632f2d35e0bad Author: darkhan.nausharipov Date: Tue Nov 29 13:09:58 2022 +0600 rearranged completeUnit (#23692) commit fb1fea1af8069ae02ba261781ca923cd40796c97 Author: darkhan.nausharipov Date: Tue Nov 29 11:43:06 2022 +0600 comment fixes (4) commit 749d8b19bea072be84cace76bd1fb998a3a76531 Author: darkhan.nausharipov Date: Tue Nov 29 11:03:34 2022 +0600 pubspec.lock commit 0a76b4019647ebcbec76739b078b01fddd37789b Author: darkhan.nausharipov Date: Tue Nov 29 11:03:17 2022 +0600 pubspec.lock ignored only in PGC commit c6004b0f220be0959b1fadc1fcffb04e4004cd97 Author: darkhan.nausharipov Date: Mon Nov 28 21:17:16 2022 +0600 no final else commit d55622e30df308eb61a42ecd1f03930012ec0cb8 Author: darkhan.nausharipov Date: Mon Nov 28 21:15:15 2022 +0600 missing await commit 65dc5219943a948e7922da44ce34b54474bffaf1 Author: darkhan.nausharipov Date: Mon Nov 28 21:12:22 2022 +0600 updating & blocking complete unit button (#23692) commit 1bbc09279604dd2025ca6c93cfc85d2df9bece09 Author: darkhan.nausharipov Date: Mon Nov 28 14:36:25 2022 +0600 comment fixes (3) commit 860780836bdf8cdbbcbe5952232abc3544effc9e Author: darkhan.nausharipov Date: Mon Nov 28 12:44:52 2022 +0600 pubspec.lock in gitignore commit a575717869b239f59374caf74f81db74191c3781 Author: darkhan.nausharipov Date: Mon Nov 28 12:41:44 2022 +0600 deleted pubspec.lock commit c2f74a59ed6a364d04a9cb0d11ea0e1f0ce5ffbf Author: darkhan.nausharipov Date: Sun Nov 27 21:05:49 2022 +0600 review comments (2) commit adbdeb6eec823178d487a3a3b3bba07ceda6efa9 Author: darkhan.nausharipov Date: Thu Nov 24 15:15:57 2022 +0600 deleted file (#23692) commit 6ed38acb2beb4b960eb7a77da1282683e2805bc0 Author: darkhan.nausharipov Date: Thu Nov 24 15:14:07 2022 +0600 unused import (#23692) commit 2c90ced90376df81d0989c233995320a73c7feb1 Author: darkhan.nausharipov Date: Thu Nov 24 14:55:05 2022 +0600 login_overlay (#23692) commit 6d21e1ea3f0837616a185e4b1f0d48398799791d Author: darkhan.nausharipov Date: Thu Nov 24 14:52:08 2022 +0600 kOpenLoginOverlay (#23692) commit 64965a1e1f49f0240b4e27a387aad099caade1b2 Author: darkhan.nausharipov Date: Thu Nov 24 14:44:19 2022 +0600 canCompleteCurrentUnit (#23692) commit 7fbaf3843394530602a87805fa274ace7620ae29 Author: darkhan.nausharipov Date: Wed Nov 23 15:21:56 2022 +0600 new configs & todos commit 0b6fc8f805d3417f819ebafadc40f2571d73df99 Author: darkhan.nausharipov Date: Tue Nov 22 21:07:35 2022 +0600 candidate repository commit 3df27bc7fc71f12d48b3aa0bf5d58e57ba1327b9 Author: darkhan.nausharipov Date: Tue Nov 22 15:12:05 2022 +0600 untested refinement (1) commit 52838e500fc106414dbb47d0c05fe4042c812509 Author: darkhan.nausharipov Date: Mon Nov 21 21:54:53 2022 +0600 notifier objects naming commit 2068ce9e8da8b6caa41e1be09dfa04e26e2ca567 Author: darkhan.nausharipov Date: Mon Nov 21 21:50:25 2022 +0600 deleted show sdk selector commit a6597d434a3aed640fc230976afd3637217d8792 Author: darkhan.nausharipov Date: Mon Nov 21 21:44:55 2022 +0600 untested refinement commit 10b287b89456368b146fd9bc1edc38bf0c28fc73 Merge: 4aedd5a607 5ad067f7cc Author: Darkhan Nausharipov <31556582+nausharipov@users.noreply.github.com> Date: Mon Nov 21 19:06:30 2022 +0600 Merge pull request #324 from akvelon/tour-header-sdk-selector SDK selector in tour scaffold commit 5ad067f7ccdb976fcdb26102d983bea936a9ba83 Author: darkhan.nausharipov Date: Mon Nov 21 17:55:38 2022 +0600 non-nullable onChanged commit 4aedd5a6072e92dbfd7487652619e7a14e291375 Merge: 6f53e30d39 70c0d61d3a Author: darkhan.nausharipov Date: Mon Nov 21 16:06:38 2022 +0600 SDK selector with fixed comments commit 6f53e30d39ba793850153a2a6077fbe6bbab2b51 Author: darkhan.nausharipov Date: Mon Nov 21 16:04:31 2022 +0600 cache license commit 70c0d61d3a96e120d312f1aecc6a38e73e551697 Author: darkhan.nausharipov Date: Sat Nov 19 20:46:18 2022 +0600 comments (0) commit de273baf06cc76e4da1f94064131f0093e265f0c Author: darkhan.nausharipov Date: Fri Nov 18 20:51:31 2022 +0600 hide back button after navigating from welcome commit 3a10e8d7633bb4d6d9bd39ba5515aacae6208554 Author: darkhan.nausharipov Date: Fri Nov 18 20:48:13 2022 +0600 show sdk selector in welcome screen commit de0f7c34f2a686f22137c464d8815a5f6c2cb9c4 Author: darkhan.nausharipov Date: Fri Nov 18 20:00:17 2022 +0600 sdk selection works with auth commit 3c534d864b5a36cf4f63b64af0332753fd420b9a Merge: 12a685d1f0 b78bbd33dc Author: Darkhan Nausharipov Date: Fri Nov 18 19:41:06 2022 +0600 Merge remote-tracking branch 'origin/tour-header-sdk-selector' into auth_sdk_demo commit 12a685d1f00140981c8241a2477b5ee0707e47be Author: darkhan.nausharipov Date: Fri Nov 18 19:36:34 2022 +0600 unit file fix commit d25eaf52df834b655421319b14e43e0afc7c63f1 Merge: 5297d2548c cf56af2799 Author: Darkhan Nausharipov Date: Fri Nov 18 19:22:22 2022 +0600 Merge branch 'master' into issue23692_auth commit 5297d2548ced6027c58b75b9d917fbfeb7052369 Author: darkhan.nausharipov Date: Wed Oct 19 19:50:43 2022 +0600 auth, complete unit, user progress AuthNotifier draft (#23692) Comments (#23692) Comments (#23692)(1) sign in with google works (#23692) new configs (#23692) get user progress draft (#23692) comment fixes (#23692) sign in in IntroTextBody (#23692) reverted config (#23692) comment fixes (#23692) WIP before rebase (merge) (#23692) Squashed commit of the following: commit bff4919ff00ec3b5d7186efde41c884dfc4c8344 Merge: 79ba69483a ce8d618c77 Author: Alexey Romanenko <33895511+aromanenko-dev@users.noreply.github.com> Date: Thu Nov 17 10:34:02 2022 +0100 Merge pull request #24186: Uses _all to follow alias/datastreams when estimating index size commit 79ba69483a84ea0278d0b0ddb141200739607c77 Merge: 245fea9040 b7e860a762 Author: Chamikara Jayalath Date: Wed Nov 16 20:47:40 2022 -0800 Merge pull request #24218: Update Python wheel format for RC validation commit 245fea904014cd58d4148807463dbaa40000774c Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed Nov 16 18:12:33 2022 -0800 Bump loader-utils from 1.4.1 to 1.4.2 in /sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel (#24191) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit e1de8e78deeb5d17617fda6591429eaaf8abb8a2 Author: Yi Hu Date: Wed Nov 16 20:48:06 2022 -0500 Fix PythonLint (#24219) commit b7e860a7621771c300dcec625655f87e62591323 Author: Chamikara Jayalath Date: Wed Nov 16 17:28:31 2022 -0800 updates commit c2feb09ea49dd815b69c65e531ce34128756d988 Author: Chamikara Jayalath Date: Wed Nov 16 17:06:08 2022 -0800 updates commit ce8d618c77d23e20a1ddb128bb8183048597d096 Author: egalpin Date: Wed Nov 16 16:43:57 2022 -0800 Adds test for following aliases when estimating index size commit 959719d01c627328c0ca2849d2b7e2c9b322d4d1 Author: Chamikara Jayalath Date: Wed Nov 16 15:16:06 2022 -0800 Temporary update Python RC validation job commit b952b41788acc20edbe5b75b2196f30dbf8fdeb0 Author: Yi Hu Date: Wed Nov 16 14:18:12 2022 -0500 Python TextIO Performance Test (#23951) * Python TextIO Performance Test * Add filebasedio_perf_test module for unified test framework for Python file-based IOs * Fix MetricsReader publishes metrics duplicately if more than one load test declared. This is because MetricsReader.publishers was static class variable * Fix pylint * Distribute Python performance tests random time at a day instead of all at 3PM * Add information about length conversion commit 017f2cbde124af40a43be99ec88289fcf63c1c95 Merge: fef8acdbc0 88dba4f494 Author: Chamikara Jayalath Date: Wed Nov 16 10:39:52 2022 -0800 Merge pull request #24187: Add a reference to Java RunInference example commit fef8acdbc0ecbcc85b49144adaf8830e3bc6b2de Merge: 6e9187e67e ead245539d Author: Ahmet Altay Date: Wed Nov 16 10:24:53 2022 -0800 Merge pull request #24199 from Laksh47/issue#24196 refs: issue-24196, fix broken hyperlink commit 6e9187e67e1bd8f73997f437f0ed4c29880ed73b Author: Darkhan Nausharipov <31556582+nausharipov@users.noreply.github.com> Date: Wed Nov 16 22:33:50 2022 +0600 [Tour of Beam] [Frontend] Content tree URLs (#23776) * Content tree navigation (#23593) Unit content navigation (#23593) Update URL on node click (#23593) Active unit color (#23593) removeListener in unit (#23593) First unit is opened on group title click (#23593) WIP by Alexey Inkin (#23593) selectedUnitColor (#23593) Unit borderRadius (#23593) RegExp todo (#23593) added referenced collection package to remove warning (#23593) small refinement (#23593) expand on group tap, padding, openNode (#23593) group expansion bug fix (#23593) selected & unselected progress indicators (#23593) * AnimatedBuilders instead of StatefulWidgets in unit & group (#23593) * fixed _getNodeAncestors (#23593) * get sdkId (#23593) * addressing comments (#23593) * sdkId getter & StatelessExpansionTile (#23593) * expand & collapse group (#23593) * StatelessExpansionTile (#23593) * license (#23593) * ValueChanged and ValueKey in StatelessExpansionTile (#23593) Co-authored-by: darkhan.nausharipov Co-authored-by: Alexey Inkin commit b33fac2aa533d77cfa47f88466c8cd6bd3f3e864 Author: Bruno Volpato Date: Wed Nov 16 10:51:11 2022 -0500 Use only ValueProviders in SpannerConfig (#24156) commit 5f013ab6567ec75b460b2081d7f89d332320caff Author: Robert Burke Date: Wed Nov 16 07:23:10 2022 -0800 revert upgrade to go 1.19 for action unit tests (#24189) commit 9337f4dbecc929886f8559949a082a649fd9d1bb Author: Yi Hu Date: Wed Nov 16 10:18:42 2022 -0500 Fix Python PostCommit Example CustomPTransformIT on portable (#24159) * Fix Python PostCommit Examples on portable * Fix custom_ptransform pipeline options gets modified * Specify flinkConfDir commit ead245539d01dec0f3e08699c1e1cc6777a5ef0e Author: Laksh Date: Wed Nov 16 09:32:46 2022 -0500 refs: issue-24196, fix broken hyperlink commit e83a996d4374d467d95bcfad7166905622ec615c Merge: 2fc56ec663 ffdee0b6ed Author: Jan Lukavský Date: Wed Nov 16 15:15:31 2022 +0100 Merge pull request #24192: Re-use serializable pipeline options when already available. commit ffdee0b6edb8638c78a65ec85c727ea5dde1cb2f Author: Jozef Vilcek Date: Mon Nov 14 16:48:18 2022 +0100 Re-use serializable pipeline options when already available (#24192) commit 88dba4f494829b2b3530b767fb8c5252e0d2ba44 Author: Chamikara Jayalath Date: Tue Nov 15 16:21:22 2022 -0800 Add a reference to Java RunInference example commit 2fc56ec663e335cfcf37dc57d471f79b601414f4 Merge: f763186987 83f1bc19b9 Author: Kenn Knowles Date: Tue Nov 15 16:16:47 2022 -0800 Merge pull request #24142: Fix arguments to checkState in BatchViewOverrides commit f763186987c00ba1d26efdc35406436a1fa69a9a Merge: c2bc2135e9 0d7ca04182 Author: Ning Kang Date: Tue Nov 15 15:25:20 2022 -0800 Addresses #24161 Updated README of Interactive Beam commit c2bc2135e9bce715990a5d5551e2bc2dc0311da4 Author: Doug Judd Date: Tue Nov 15 14:48:26 2022 -0800 Strip FGAC database role from changestreams metadata requests (#24177) Co-authored-by: Doug Judd commit af637974f96ad1b5110d7dea3f9a26c68e19a51b Author: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> Date: Tue Nov 15 17:16:43 2022 -0500 Add custom inference function support to the PyTorch model handler (#24062) * Initial type def and function signature * [Draft] Add custom inference fn support to Pytorch Model Handler * Formatting * Split out default * Remove Keyed version for testing * Move device optimization * Make default available for import, add to test classes * Remove incorrect default from keyed test * Keyed impl * Fix device arg * custom inference test * formatting * Add helpers to define custom inference functions using model methods * Trailing whitespace * Unit tests * Fix incorrect getattr syntax * Type typo * Fix docstring * Fix keyed helper, add basic generate route * Modify generate() to be different than forward() * formatting * Remove extra generate() def commit a014637106970a0a0e9eb7944aa5caf79fa5fd37 Author: egalpin Date: Tue Nov 15 13:57:54 2022 -0800 Uses _all to follow alias/datastreams when estimating index size Fixes #24117 commit 0d7ca041823bc2b09f76f86fdfd1d0b9508c9c88 Author: Ning Kang Date: Tue Nov 15 13:57:27 2022 -0800 Minor update commit e8fc759d756f4a987e41d2b9da56b906a6cd7736 Author: Ning Kang Date: Tue Nov 15 13:52:18 2022 -0800 Updated README of Interactive Beam Removed deprecated cache_dir runner param in favor of the cache_root global option. commit 08d5f72e5f35d41f3e9fa9fe799caea6bed1b7a7 Author: Anand Inguva <34158215+AnandInguva@users.noreply.github.com> Date: Tue Nov 15 16:34:21 2022 -0500 [Python]Support pipe operator as Union (PEP -604) (#24106) Fixes https://github.com/apache/beam/issues/21972 commit 526e7a58b62682582c27173ab21ed8667ddab766 Author: Scott Strong Date: Tue Nov 15 16:26:45 2022 -0500 Using Teardown context instead of deprecated finalize (#24180) * Using Teardown context instead of deprecated finalize * making function public Co-authored-by: Scott Strong commit fb4d1d4dea7b26ed538a9f6aca0ed41e8c300e37 Author: Danny McCormick Date: Tue Nov 15 16:25:22 2022 -0500 Fix broken json for notebook (#24183) commit f98db2008a97f4546d036ddf0dddfee8c87eb58a Author: Robert Burke Date: Tue Nov 15 12:49:23 2022 -0800 Update automation to use Go 1.19 (#24175) Co-authored-by: lostluck <13907733+lostluck@users.noreply.github.com> commit e5f58504eef1fdeebe0402cda8a2df259169c704 Author: Brian Hulette Date: Tue Nov 15 12:25:13 2022 -0800 Add error reporting for BatchConverter match failure (#24022) * add error reporting for BatchConverters * Test pytorch * Finish up torch tests * yapf * yapf * Remove else commit 3037747f66f0d71d65b6c65745b4f8942c22f05a Author: Danny McCormick Date: Tue Nov 15 14:13:04 2022 -0500 Fix broken notebook (#24179) commit b2b1c739ce37690923891934ee317f799db937a2 Author: MakarkinSAkvelon <67736809+MakarkinSAkvelon@users.noreply.github.com> Date: Tue Nov 15 21:53:06 2022 +0500 [Playground] Move Playground in GKE and Infrastructure change (#23928) * changes to updated master branch * Change workflow * ingress changes * Certificate was added * Updates for cloud build backend * Update main.tf * Create main.tf * Create variables.tf * Update variables.tf * Update main.tf * Update variables.tf * Update main.tf * Create output.tf * Update output.tf * Update output.tf * Update main.tf * Update build.gradle.kts * Update output.tf * Update main.tf * Update main.tf * Update main.tf * Update variables.tf * Update main.tf * Update variables.tf * Update main.tf * Update main.tf * Update main.tf * Update main.tf * Update main.tf * Update main.tf * Update variables.tf * Update main.tf * Update variables.tf * Update main.tf * Update variables.tf * Update main.tf * Update main.tf * Update main.tf * Update main.tf * Update main.tf * Update main.tf * Update main.tf * Update main.tf * Update output.tf * Update main.tf * Update main.tf * Update output.tf * Create variables.tf * Update main.tf * Update main.tf * Delete playground/terraform/infrastructure/cluddns directory * Update main.tf * Update output.tf * Update output.tf * Update build.gradle.kts * Update build.gradle.kts * Update build.gradle.kts * Update build.gradle.kts * Update build.gradle.kts * Update build.gradle.kts * Update README.md * Update README.md * helm folder name was changed * Update README.md * Update build.gradle.kts * Update build.gradle.kts * Update build.gradle.kts * Updates to readme * Fix DNS name * HelmChart was changed * Some workflows were changed * Remove unused file * playground-examples return * add license information * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * remove "stg" folder * Update README.md * Remove unused files * DNS Removed * var name changed * remove DNSName from var file * 1 * Clear terraform * remove unused records * gradle check * grade last change * issue fix * fix * 1 * run * test * Index creation for Gradle * Add IndexCreation in gradle * Update README.md * Update README.md * Fix names for Frontend * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Redis fix * services fix * Update variables.tf * change order in gradle * Fix Config.g.dart file issue * Update README.md * Playground workflow update Co-authored-by: Sergey Makarkin Co-authored-by: Sergey Makarkin Co-authored-by: ruslan-ikhsan Co-authored-by: Alex Kosolapov commit 85df5f2eb2f299e28b36be0cce7b9c19d62124da Author: Yi Hu Date: Tue Nov 15 11:38:13 2022 -0500 Eliminate CalciteUtil.CharType logical type (#24013) * Eliminate CalciteUtils.CharType logical type * Replace CalciteUtils.CharType to String Note that CalciteUtils still omits the precision of BINARY/VARBINARY/CHAR/VARCHAR as what it originally did. Support of the precision of these calcite types involves make use of making use of the overload method RelDataTypeFactory.createSqlType(var1, var2). * Replace every reference of CalciteUtil.CharType to generic PassThroughLogicalType check * Add TODO to Support sql types with arguments * Use VariableString in LogicalTypeTestCase commit f349f41010c5b238ff6020f7de718f938eef3c5e Author: alexeyinkin Date: Tue Nov 15 20:04:01 2022 +0400 Configure flutter_code_editor options with Hugo shortcode (#23926) (#24031) * Configure flutter_code_editor options with Hugo shortcode (#23926) * Minor fixes (#23926) * Refactor after review (#23926) commit 0f4ca6363b3ce0e5de3ad36517bb406aa6391a18 Author: Rebecca Szper <98840847+rszper@users.noreply.github.com> Date: Tue Nov 15 06:10:13 2022 -0800 Editorial review of the ML notebooks. (#24125) * Editorial review of the ML notebooks. * Editorial review of the ML notebooks. * Editorial review of the ML notebooks. * Update examples/notebooks/beam-ml/custom_remote_inference.ipynb Co-authored-by: Danny McCormick * Updating based on feedback * Update examples/notebooks/beam-ml/run_inference_sklearn.ipynb Co-authored-by: Danny McCormick * Update examples/notebooks/beam-ml/run_inference_tensorflow.ipynb Co-authored-by: Danny McCormick * Update examples/notebooks/beam-ml/run_inference_tensorflow.ipynb Co-authored-by: Danny McCormick * Update examples/notebooks/beam-ml/run_inference_tensorflow.ipynb Co-authored-by: Danny McCormick * Updating based on feedback Co-authored-by: Danny McCormick commit 5bd34ede026253326ebff1a7e4f9edb5f71b4a2c Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue Nov 15 07:17:28 2022 -0500 Bump github.com/aws/aws-sdk-go-v2/feature/s3/manager in /sdks (#24131) Bumps [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2) from 1.3.2 to 1.11.39. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.3.2...feature/s3/manager/v1.11.39) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/manager dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 2ee809fa0ca7689dd0279e186ebc02d9569a8429 Merge: e3b9bdb2e6 563c66d6fd Author: Alexey Romanenko <33895511+aromanenko-dev@users.noreply.github.com> Date: Tue Nov 15 11:01:14 2022 +0100 Merge pull request #23065: [Website] Update copy icon styles commit e3b9bdb2e607d85a4017ba7839000e92a0ad83c4 Author: Moritz Mack Date: Tue Nov 15 10:40:50 2022 +0100 [Dockerized Jenkins] Fix build of dockerized jenkins (fixes #24053) (#24054) commit faaac2ab6e010374cb2be0e95a5dd345836a2a2c Author: Moritz Mack Date: Tue Nov 15 10:38:59 2022 +0100 [Dockerized Jenkins] Update README how to use local repo (#24055) commit 689e70b5131620540faf52e2f1e2dca7a36f269d Author: Damon Date: Mon Nov 14 17:34:29 2022 -0800 Implement embedded WebAssembly example (#24081) commit e1bf6c42950e8013f35e35fb9fee8017e01e5010 Merge: eddac84126 10337d2868 Author: Robert Bradshaw Date: Mon Nov 14 15:22:14 2022 -0800 Merge pull request #24160 Rename the test_splits flag to direct_test_splits. commit eddac841261228a2c63fa9b225c520ae0f853806 Author: Pablo Date: Mon Nov 14 15:05:05 2022 -0800 More dataset templates to clean up (#24162) commit 2adb68bd12743566cc89b596bf204d7c807eb62d Author: Pablo Date: Mon Nov 14 13:28:13 2022 -0800 Adding a quickstart to README for the TS SDK (#23509) * More of a quickstart for the TS SDK * Update sdks/typescript/README.md Co-authored-by: Danny McCormick * Update sdks/typescript/README.md Co-authored-by: Danny McCormick Co-authored-by: Danny McCormick commit 10337d28685ad5712e2ad8608977ec5c5e0e6b6b Author: Robert Bradshaw Date: Mon Nov 14 12:46:32 2022 -0800 Rename the test_splits flag to direct_test_splits. This avoids possible flag conflicts. commit 48c70cc30742b45b17a1d18ece2f0d079bee3915 Author: arne-alex <108519096+arne-alex@users.noreply.github.com> Date: Mon Nov 14 21:33:02 2022 +0100 Merge pull request #23333: Track time on Cloud Dataflow streaming data reads and export via heartbeats commit 9c83de646ab52bd0b05e3346190dd55cd68b2a8b Author: Johanna Öjeling <51084516+johannaojeling@users.noreply.github.com> Date: Mon Nov 14 21:19:44 2022 +0100 Add more tests for S3 filesystem (#24138) commit 9e9c6d797ba52b460f83131431c8e53aebbbc9ac Merge: d5d76b9745 c600444e1d Author: Ning Kang Date: Mon Nov 14 12:06:15 2022 -0800 Merge pull request #24029 from apache/dependabot/npm_and_yarn/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/loader-utils-1.4.1 Bump loader-utils from 1.4.0 to 1.4.1 in /sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel commit d5d76b974592d45de368ab641647ca5cc4ec12ec Author: Yi Hu Date: Mon Nov 14 15:03:28 2022 -0500 Support SqlTypes Date and Timestamp (MicrosInstant) in AvroUtils (#23969) * Support SqlTypes Date and Timestamp (MicrosInstant) in AvroUtils * Add TODO about java.time migration commit 330cc2010c9f4a2d4e30318bf50a4109ec1cd392 Author: Pablo Date: Mon Nov 14 12:02:10 2022 -0800 Cleanup stale BQ datasets (#24158) * Cleanup stale BQ datasets * addressing comments commit 4a044999b8ed4bcd41f816f3a23ccb5da00c4c38 Merge: e563b9dd2f 5bd75c25de Author: Heejong Lee Date: Mon Nov 14 11:16:00 2022 -0800 Merge pull request #24076 from chamikaramj/multilang_java_updates Updates Multi-lang Java quickstart commit e563b9dd2f3aa0484e6cdc08869991b5e438023e Author: Evgeny Antyshev Date: Mon Nov 14 20:56:35 2022 +0300 [Tour Of Beam] verify that unit exists when saving progress (#24118) * AIO * Update learning/tour-of-beam/backend/integration_tests/auth_test.go Co-authored-by: Danny McCormick * nit Co-authored-by: Danny McCormick commit 774923e0dd089de870bfa5c77063ae2b28f79347 Merge: 71785de528 1ad0cbc445 Author: Kenn Knowles Date: Mon Nov 14 09:52:26 2022 -0800 Merge pull request #24141: Fix checkArgument format in GcsPath commit 71785de52864313c2e3b14fe72a2a63281343617 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Nov 14 11:54:37 2022 -0500 Bump github.com/aws/aws-sdk-go-v2/config from 1.17.10 to 1.18.0 in /sdks (#24151) Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.17.10 to 1.18.0. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.17.10...config/v1.18.0) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/config dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 50d591d6cb3e799bee4e29dfc593c693a86e6276 Author: Bruno Volpato Date: Mon Nov 14 11:50:01 2022 -0500 Change DataflowBatchWorkerHarness doWork error level to INFO (#24135) commit 5a72696bfda09fdb905ba8e58b636f8494ef955f Merge: ee0a5836d6 0633fe9634 Author: Kenn Knowles Date: Mon Nov 14 08:12:12 2022 -0800 Merge pull request #24149: Remove extraneous jetbrains annotation commit ee0a5836d69b776834eb3bd9b2bd02eb5252c333 Merge: d001a69e1a 137799672e Author: Kenn Knowles Date: Mon Nov 14 08:11:00 2022 -0800 Merge pull request #24132: Fix checkArgument format string in AvroIO commit d001a69e1a58701d6ed4fcb5e3fb7a0921301dad Author: Yi Hu Date: Mon Nov 14 10:56:54 2022 -0500 Test Dataproc 2.1 with Flink load tests (#24129) * Test Dataproc 2.1 with Flink load tests * Minor fix flink_cluster script commit caabd9be52887ad70c8a4269395c893811ac6a84 Author: Israel Herraiz Date: Mon Nov 14 16:03:39 2022 +0100 Make MonotonicWatermarkEstimator work like its Java SDK equivalent (#24146) * Make MonotonicWatermarkEstimator work like its Java SDK equivalent The current implementation of MonotonicWatermarkEstimator raises an exception with late messages, which makes the watermark estimator barely usable in real world scenarios. This PR fixes #20041 by making this watermark estimator work like its Java SDK equivalent (`WatermarkEstimators.MonotonicallyIncreasing`). * Update unit tests too * Make linter happy commit 451f6b3e7f58d0a3782ad942c6a1fd9f63932024 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Nov 14 09:48:23 2022 -0500 Bump golang.org/x/net from 0.1.0 to 0.2.0 in /sdks (#24153) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.1.0 to 0.2.0. - [Release notes](https://github.com/golang/net/releases) - [Commits](https://github.com/golang/net/compare/v0.1.0...v0.2.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 2bb03d62e2d7dc2d8e39040fc9adebccbde74fde Merge: 4e39ef2041 623083cd0a Author: Alexey Romanenko <33895511+aromanenko-dev@users.noreply.github.com> Date: Mon Nov 14 15:01:13 2022 +0100 Merge pull request #24000: [Website] Change headers size from h4,h3 to h2 commit 563c66d6fd32165da14a07747f2764c17a5d24ea Author: bulat safiullin Date: Wed Sep 7 18:28:42 2022 +0600 [Website] update pre tag copy link styles #23064 commit 4e39ef20410ee51c6040317bcd60171e64c5171f Merge: 223768f782 105ed6fedc Author: Alexey Romanenko <33895511+aromanenko-dev@users.noreply.github.com> Date: Mon Nov 14 10:55:33 2022 +0100 Merge pull request #24115: [Website] update go-dependencies.md java-dependencies.md links commit 223768f782f771f0033b8d0686d86cf4c71fad75 Merge: aa0a35dabf a9da2abee6 Author: Kenn Knowles Date: Sun Nov 13 18:53:13 2022 -0800 Merge pull request #24136: Fix checkArgument format string in ExecutionStateTracker commit 0633fe9634fe61df7cbc0ecac205d81124fd504a Author: Kenneth Knowles Date: Sat Nov 12 15:15:16 2022 -0800 Remove extraneous jetbrains annotation commit 83f1bc19b95935e60ca1f4027d4b60c7e738a84a Author: Kenneth Knowles Date: Sat Nov 12 14:16:09 2022 -0800 Fix arguments to checkState in BatchViewOverrides commit 1ad0cbc44594d8405bf4b07a126265238013a02a Author: Kenneth Knowles Date: Sat Nov 12 13:41:02 2022 -0800 Fix checkArgument format in GcsPath commit aa0a35dabf9c2a0d9822faff06d939d9a77a3ab6 Author: Kenn Knowles Date: Fri Nov 11 20:26:30 2022 -0800 Fix checkArgument format string in TestStream (#24134) commit a9da2abee6455bc2cf0f18ba5f6cd7bbaeae669f Author: Kenneth Knowles Date: Fri Nov 11 16:54:27 2022 -0800 Fix checkArgument format string in ExecutionStateTracker commit 369e2ba8622d3474c14c39b941b2c618842d1e47 Author: Ryan Thompson Date: Fri Nov 11 19:46:07 2022 -0500 Add a ValidatesContainer integration test for use_sibling_sdk_workers (#24099) commit 137799672eb559a7586262e6a8a73d1ab3580e44 Author: Kenneth Knowles Date: Fri Nov 11 15:30:01 2022 -0800 Fix checkArgument format string in AvroIO commit 5d2dbf957e4e82fb3980726940df02ac67e563cd Author: Anand Inguva <34158215+AnandInguva@users.noreply.github.com> Date: Fri Nov 11 15:57:28 2022 -0500 Update staging of Python wheels (#24114) Fixes https://github.com/apache/beam/issues/24110 commit c2021bee1eba0322b43c90841397859048296b21 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri Nov 11 15:33:14 2022 -0500 Bump google.golang.org/api from 0.102.0 to 0.103.0 in /sdks (#24049) Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.102.0 to 0.103.0. - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.102.0...v0.103.0) --- updated-dependencies: - dependency-name: google.golang.org/api dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 6557c91c79480b9d90573d52d257a11c2b160196 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri Nov 11 11:47:12 2022 -0800 Bump github.com/aws/aws-sdk-go-v2/service/s3 in /sdks (#24112) Bumps [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) from 1.29.1 to 1.29.2. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.29.1...service/s3/v1.29.2) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/service/s3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 96f9da1ab652156cd143d57e3aa3d94836338f2b Author: Yi Hu Date: Fri Nov 11 14:34:22 2022 -0500 More cleanup containers (#24105) * More cleanup containers * prebuilt_beam_sdk/beam_python_prebuilt_sdk no longer exists in gcr.io Add correct paths to cleanup * Bump grace time to 15 days * Run job daily as in code comment * Set grace period to 30 days commit 836766ddb83d37463e4b036f156b8d7e15e3864b Author: Ritesh Ghorse Date: Fri Nov 11 14:30:30 2022 -0500 upgrade testcontainer dependency (#24123) commit 9fcd20c3712536f2d4580beead678cdbb6fd4746 Author: Damon Date: Fri Nov 11 11:12:11 2022 -0800 Implement PubsubRowToMessage transform (#23897) * Begin PubsubRowToMessage Impl * Complete working draft * Unit tests validate user and non-user fields * Finish tests on supporting methods * Pass checks before finalizing tests * WIP * fix timestamp * finalize tests * Finalize code comments * Clean up check findings * Add InputSchemaFactory * Patch code comment typo commit 3a6fcc1ca4d07a467464ed1214a94b5c9c147295 Author: Evgeny Antyshev Date: Fri Nov 11 19:13:45 2022 +0300 disable (#24121) commit 027fb142038b45c443d87af96cac082264c43188 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri Nov 11 10:26:23 2022 -0500 Bump cloud.google.com/go/bigtable from 1.17.0 to 1.18.0 in /sdks (#24113) Bumps [cloud.google.com/go/bigtable](https://github.com/googleapis/google-cloud-go) from 1.17.0 to 1.18.0. - [Release notes](https://github.com/googleapis/google-cloud-go/releases) - [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-cloud-go/compare/pubsub/v1.17.0...pubsub/v1.18.0) --- updated-dependencies: - dependency-name: cloud.google.com/go/bigtable dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit b2d28a64bb0837dd97e8be7f97f31f88d380f110 Author: Danny McCormick Date: Fri Nov 11 08:45:14 2022 -0500 Add TFX support in pydoc (#23960) * Add TFX support in pydoc * Wording commit 105ed6fedcb6ff33d4a43940c342a653e67cb25e Author: bulat safiullin Date: Thu Nov 10 17:47:54 2022 +0600 [Website] update go-dependencies.md java-dependencies.md programming-guide.md links #24084 commit 4b96193250a63b27721a8c5e1a3bd6ecff983093 Author: Brian Hulette Date: Thu Nov 10 16:51:06 2022 -0800 Remove TheNeuralBit from the pool of Python reviewers (#24108) commit 8f8f089a5f565f179905984cef3522a0577d9219 Author: Sanil Jain Date: Thu Nov 10 16:47:31 2022 -0800 Wire SamzaPipelineOptions to Exeption listener interface (#24109) commit b3186ba91f9bb22764d47b78eb2b7ff017080f75 Author: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Thu Nov 10 18:04:50 2022 -0500 Support using BigQueryIO Storage Read API with SchemaTransforms (#23827) * support schema transform for bq direct read method * use vendor Strings import * add BigQueryServices argument to config object * suppress nullability errors * add package-info.java file to providers subdirectory * removing accidentally created file * added documentation and moved configuration filclass into provider class * validate config params before expansion. config has a validaate me method * updated URN identifier to follow standards commit a4a94da1936d8cd2f8d1145fd2e8329fe06a2990 Author: Oleh Borysevych Date: Fri Nov 11 00:28:43 2022 +0200 fixing linter error (#24104) commit 156a6099d71a155c3379340a0b98256306e1755e Author: Danny McCormick Date: Thu Nov 10 17:26:15 2022 -0500 Add blog post on new ML resources (#24071) * Create ml-resources.md * Add ensemble notebook image * Add image link * Fix image link * Add ml-landing page image * Add image + move around * Add paragraph on upcoming changes * Remove bad whitespace commit 38742d40c895f5d38ee6ee5eb328d11b3262307e Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu Nov 10 16:12:40 2022 -0500 Bump github.com/aws/aws-sdk-go-v2/config from 1.5.0 to 1.17.10 in /sdks (#24080) Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.5.0 to 1.17.10. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.5.0...config/v1.17.10) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/config dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 92cef32915c40d353b51529a87d8f0131b0a3538 Author: BjornPrime <32173247+BjornPrime@users.noreply.github.com> Date: Thu Nov 10 15:30:37 2022 -0500 Num failed inferences (#23830) * created test_increment_num_failed_inferences and test_num_failed_inferences_no_failures * added assertRaises to test_increment_num_failed_inferences * added num_failed_inferences to _MetricsCollector * changed error handling and update() implementation * updated metric name in tests * removed unnecessary else blocking * removed unnecessary inference_args from test_increment_failed_batches_counter() * changed final test_increment_failed_batches_counter assertion * clarified error handling and updated failed_batches_counter initialization * decreased examples array length to 1 to ensure repeatability * troubleshooting tests * trying to get test_increment_failed_batches_counter to fail as expected * corrected assertion details * simplified assertRaises and added reminder comment to assertEqual counter * lint test * lint test passed, resetting pre-commit-config.yaml * fixed lingering linting issues * shortened comment line to comply with linting * formatter worked its magic commit 2341f61d48d838ddaf2e4fb990a49987244c3513 Author: Evgeny Antyshev Date: Thu Nov 10 21:24:55 2022 +0300 [Tour Of Beam] handle CORS pre-flight requests (#24083) * cors * README * nit * nit * -headers, -cache * allow-headers * +1h cache,comment commit 36b0c8f1a76865e4dc3b1eaec1566c3ef9fd4345 Author: Evgeny Antyshev Date: Thu Nov 10 21:21:21 2022 +0300 [Playground] update snippet by persistence_key (#24056) * proto * AIO * -frontend * testify * +license * -build * index * skipKey * Update playground/backend/internal/db/datastore/datastore_db.go Co-authored-by: Danny McCormick Co-authored-by: Danny McCormick commit df553d1a4c6c6cc9b37ada2a6c84c88d8aef38b9 Author: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Thu Nov 10 13:17:36 2022 -0500 Add random string at the end of BigQuery query job name to make it resilient to retries (#24041) * add random string at the end of query job name * use deterministic temp table name commit e439f4120ef4c25aa36e5b03756dc7391bdbd211 Author: Pablo Date: Thu Nov 10 10:06:26 2022 -0800 Improving stale container cleanup script (#24040) * Improving stale container cleanup script * Avoid also latest image * Update .test-infra/tools/stale_dataflow_prebuilt_image_cleaner.sh Co-authored-by: Yi Hu * Update .test-infra/tools/stale_dataflow_prebuilt_image_cleaner.sh Co-authored-by: Yi Hu Co-authored-by: Yi Hu commit 8d585242e158babd8dd2ca5a6d5a49d353be2935 Author: Anand Inguva <34158215+AnandInguva@users.noreply.github.com> Date: Thu Nov 10 12:58:41 2022 -0500 [Python]Set pickle library at the Pipeline creation stage (#24069) Fixes https://github.com/apache/beam/issues/21615 commit 08b6a524fecc333a12ce42971733ef64ae7d02f1 Author: Janek Bevendorff Date: Thu Nov 10 18:37:09 2022 +0100 [BEAM-12792] Install pipline dependencies to temporary venv (#16658) commit 74f87b0282a23aa01ecb63288013311d207960aa Merge: 38a85b288a 383ea77bc9 Author: Robert Bradshaw Date: Thu Nov 10 09:10:37 2022 -0800 Merge pull request #23985 Support dynamic sharding in the worker. commit 38a85b288ad10171c444d4092eb969ca5de58af3 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu Nov 10 10:39:15 2022 -0500 Bump github.com/aws/aws-sdk-go-v2/service/s3 in /sdks (#24077) Bumps [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) from 1.11.1 to 1.29.1. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Changelog](https://github.com/aws/aws-sdk-g… * Fixes, BeamSdkVersion, initializeRepositories, tryLoadSharedExample (#446) (#26025) (#25468) * _initializeRepositories in ToB * _trySetSnippetType in onAuthChanged * hardcoded SDKs * removed isEnabled from run button * deleted scio from tob locator * beam version in tob footer (#26025) * moved _BeamVersion * beam sdk version comments (#26025) * _tryLoadSharedExample (#25468) * MultipleExceptions (#25468) * previousException (#25468) * stackTraces in MultipleExceptions (#25468) --------- Co-authored-by: darkhan.nausharipov * Delete local user progress on account deletion (#25255) (#25283) (#449) * local deleteUserProgress (#25255) (#25283) * comments * deleted comment --------- Co-authored-by: darkhan.nausharipov * CloudFunctionsTobClient refactoring (#451) * _makeRequest * _makeRequest comments --------- Co-authored-by: darkhan.nausharipov * onNodePressed (#456) Co-authored-by: darkhan.nausharipov * Feedback (#452) * FeedbackWidget (#26075) * local saving bug fix * clickable links in markdown (#26115) * feedback refinement (#26075) * feedback analytics (#26075) * comment fixes (#26075) * newline fix (#26075) --------- Co-authored-by: darkhan.nausharipov * Clean up * 18.04.23 Refinement (#457) * narrow tour with tabs * clear overlay warnings * remove sdkId todo * flutter_code_editor v0.2.16 * flutter_code_editor v0.2.16 .lock * DefaultKeyedTabController * todo deletion proposals * Cancel running example test fix * comment fixes * regenerated assets * flutter_code_editor v0.2.13 * flutter_code_editor: 0.2.14 * deleted redundant code & new configs * no client in AuthNotifier --------- Co-authored-by: darkhan.nausharipov --------- Co-authored-by: darkhan.nausharipov Co-authored-by: Alexey Inkin --- .../frontend/assets/translations/en.yaml | 23 +- .../frontend/lib/assets/assets.gen.dart | 24 +- .../frontend/lib/auth/notifier.dart | 21 +- .../frontend/lib/cache/content_tree.dart | 10 +- .../frontend/lib/cache/unit_content.dart | 9 +- .../frontend/lib/cache/unit_progress.dart | 141 +++++-- .../lib/components/builders/content_tree.dart | 7 +- .../frontend/lib/components/footer.dart | 52 ++- .../frontend/lib/components/login/button.dart | 2 +- .../lib/components/login/content.dart | 16 +- .../lib/components/profile/avatar.dart | 2 +- .../lib/components/profile/user_menu.dart | 19 +- .../frontend/lib/components/scaffold.dart | 8 +- .../frontend/lib/components/sdk_dropdown.dart | 14 +- .../tour-of-beam/frontend/lib/config.dart | 8 +- .../lib/constants/hive_box_names.dart | 28 ++ .../frontend/lib/constants/sizes.dart | 2 +- .../frontend/lib/enums/save_code_status.dart | 23 ++ .../frontend/lib/enums/snippet_type.dart | 23 ++ .../frontend/lib/enums/tour_view.dart | 26 ++ .../frontend/lib/enums/tour_view.g.dart | 162 ++++++++ .../frontend/lib/firebase_options.dart | 12 +- .../tour-of-beam/frontend/lib/locator.dart | 40 +- learning/tour-of-beam/frontend/lib/main.dart | 2 +- .../frontend/lib/models/unit_progress.dart | 6 +- .../frontend/lib/models/unit_progress.g.dart | 8 + .../pages/tour/controllers/content_tree.dart | 48 ++- .../lib/pages/tour/controllers/unit.dart | 58 --- .../frontend/lib/pages/tour/page.dart | 7 +- .../frontend/lib/pages/tour/path.dart | 9 +- .../frontend/lib/pages/tour/screen.dart | 65 ++-- .../frontend/lib/pages/tour/state.dart | 329 +++++++++++----- .../pages/tour/widgets/binary_progress.dart | 5 +- .../tour/widgets/complete_unit_button.dart | 15 +- .../lib/pages/tour/widgets/content_tree.dart | 22 +- .../lib/pages/tour/widgets/group.dart | 2 +- .../lib/pages/tour/widgets/hints.dart | 35 +- .../tour/widgets/markdown/code_builder.dart | 27 +- .../tour/widgets/markdown/tob_markdown.dart | 6 + .../lib/pages/tour/widgets/module.dart | 2 +- .../frontend/lib/pages/tour/widgets/unit.dart | 2 +- .../lib/pages/tour/widgets/unit_content.dart | 178 ++++++--- .../frontend/lib/pages/welcome/screen.dart | 47 +-- .../frontend/lib/pages/welcome/state.dart | 1 - .../lib/repositories/client/client.dart | 10 + .../client/cloud_functions_client.dart | 126 ++++-- .../repositories/user_progress/abstract.dart | 47 +++ .../lib/repositories/user_progress/cloud.dart | 80 ++++ .../lib/repositories/user_progress/hive.dart | 140 +++++++ learning/tour-of-beam/frontend/lib/state.dart | 22 +- learning/tour-of-beam/frontend/pubspec.lock | 360 ++++++++---------- learning/tour-of-beam/frontend/pubspec.yaml | 6 +- .../frontend/assets/translations/en.yaml | 3 + .../common/common_finders.dart | 14 +- .../integration_test/initial_urls_test.dart | 2 +- .../miscellaneous_ui/feedback_test.dart | 2 +- .../dropdown_button/dropdown_button.dart | 2 +- .../frontend/lib/controllers/factories.dart | 2 +- .../modules/examples/example_selector.dart | 2 +- .../handlers/set_content_message_handler.dart | 2 +- .../feedback/feedback_dropdown_content.dart | 198 ---------- .../feedback_dropdown_icon_button.dart | 194 ---------- .../widgets/feedback/playground_feedback.dart | 89 ----- .../widgets/playground_page_footer.dart | 8 +- .../assets/svg/thumb_down.svg | 24 ++ .../assets/svg/thumb_down_filled.svg | 24 ++ .../assets/svg/thumb_up.svg | 24 ++ .../assets/svg/thumb_up_filled.svg | 24 ++ .../assets/translations/en.yaml | 11 +- .../lib/playground_components.dart | 9 +- .../lib/src/assets/assets.gen.dart | 21 +- .../lib/src/cache/example_cache.dart | 3 +- .../lib/src/constants/sizes.dart | 1 + .../example_loaders/examples_loader.dart | 7 +- .../example_loaders/hive_example_loader.dart | 49 +++ .../standard_example_loader.dart | 51 +-- .../src/controllers/feedback_controller.dart | 41 ++ .../controllers/playground_controller.dart | 27 +- .../src/exceptions/multiple_exceptions.dart | 42 ++ .../lib/src/locator.dart | 2 + .../lib/src/models/dataset.dart | 10 + .../lib/src/models/dataset.g.dart | 23 ++ .../lib/src/models/example.dart | 14 + .../lib/src/models/example.g.dart | 72 ++++ .../lib/src/models/example_base.dart | 9 + .../lib/src/models/example_base.g.dart | 63 +++ .../hive_example_loading_descriptor.dart | 78 ++++ .../lib/src/models/example_view_options.dart | 14 + .../src/models/example_view_options.g.dart | 31 ++ .../lib/src/models/sdk.dart | 7 +- .../lib/src/models/sdk.g.dart | 5 + .../services/analytics/events/app_rated.dart | 1 + .../analytics/events/feedback_form_sent.dart | 1 + .../lib/src/theme/theme.dart | 1 + .../lib/src/widgets/dialogs/confirm.dart | 102 +++++ .../lib/src/widgets/dialogs/progress.dart | 54 +-- .../lib/src/widgets/feedback.dart | 223 +++++++++++ .../lib/src/widgets/overlay/opener.dart | 10 +- .../overlay/{dismissible.dart => widget.dart} | 18 +- .../lib/src/widgets/run_or_cancel_button.dart | 1 - .../playground_components/pubspec.yaml | 4 +- .../example_loaders/examples_loader_test.dart | 6 +- .../hive_example_loader_test.dart | 62 +++ .../playground_controller_test.dart | 2 +- .../playground_controller_test.mocks.dart | 10 + .../hive_example_loading_descriptor_test.dart | 52 +++ playground/frontend/pubspec.lock | 184 ++++----- .../example_selector_state_test.mocks.dart | 10 + 108 files changed, 2936 insertions(+), 1346 deletions(-) create mode 100644 learning/tour-of-beam/frontend/lib/constants/hive_box_names.dart create mode 100644 learning/tour-of-beam/frontend/lib/enums/save_code_status.dart create mode 100644 learning/tour-of-beam/frontend/lib/enums/snippet_type.dart create mode 100644 learning/tour-of-beam/frontend/lib/enums/tour_view.dart create mode 100644 learning/tour-of-beam/frontend/lib/enums/tour_view.g.dart delete mode 100644 learning/tour-of-beam/frontend/lib/pages/tour/controllers/unit.dart create mode 100644 learning/tour-of-beam/frontend/lib/repositories/user_progress/abstract.dart create mode 100644 learning/tour-of-beam/frontend/lib/repositories/user_progress/cloud.dart create mode 100644 learning/tour-of-beam/frontend/lib/repositories/user_progress/hive.dart delete mode 100644 playground/frontend/lib/pages/standalone_playground/widgets/feedback/feedback_dropdown_content.dart delete mode 100644 playground/frontend/lib/pages/standalone_playground/widgets/feedback/feedback_dropdown_icon_button.dart delete mode 100644 playground/frontend/lib/pages/standalone_playground/widgets/feedback/playground_feedback.dart create mode 100644 playground/frontend/playground_components/assets/svg/thumb_down.svg create mode 100644 playground/frontend/playground_components/assets/svg/thumb_down_filled.svg create mode 100644 playground/frontend/playground_components/assets/svg/thumb_up.svg create mode 100644 playground/frontend/playground_components/assets/svg/thumb_up_filled.svg create mode 100644 playground/frontend/playground_components/lib/src/controllers/example_loaders/hive_example_loader.dart create mode 100644 playground/frontend/playground_components/lib/src/controllers/feedback_controller.dart create mode 100644 playground/frontend/playground_components/lib/src/exceptions/multiple_exceptions.dart create mode 100644 playground/frontend/playground_components/lib/src/models/dataset.g.dart create mode 100644 playground/frontend/playground_components/lib/src/models/example.g.dart create mode 100644 playground/frontend/playground_components/lib/src/models/example_base.g.dart create mode 100644 playground/frontend/playground_components/lib/src/models/example_loading_descriptors/hive_example_loading_descriptor.dart create mode 100644 playground/frontend/playground_components/lib/src/models/example_view_options.g.dart create mode 100644 playground/frontend/playground_components/lib/src/widgets/dialogs/confirm.dart rename learning/tour-of-beam/frontend/lib/pages/tour/widgets/solution_button.dart => playground/frontend/playground_components/lib/src/widgets/dialogs/progress.dart (53%) create mode 100644 playground/frontend/playground_components/lib/src/widgets/feedback.dart rename playground/frontend/playground_components/lib/src/widgets/overlay/{dismissible.dart => widget.dart} (74%) create mode 100644 playground/frontend/playground_components/test/src/controllers/example_loaders/hive_example_loader_test.dart create mode 100644 playground/frontend/playground_components/test/src/models/example_loading_descriptors/hive_example_loading_descriptor_test.dart diff --git a/learning/tour-of-beam/frontend/assets/translations/en.yaml b/learning/tour-of-beam/frontend/assets/translations/en.yaml index 0bb0ca900d4dc..380bb0ee4a5a2 100644 --- a/learning/tour-of-beam/frontend/assets/translations/en.yaml +++ b/learning/tour-of-beam/frontend/assets/translations/en.yaml @@ -17,14 +17,18 @@ ui: about: About Tour of Beam - builtWith: Built with Apache Beam + builtWith: Built with Apache Beam {beamSdkVersion} + cancel: Cancel continueGitHub: Continue with GitHub continueGoogle: Continue with Google - hint: Hint - deleteAccount: Delete my account + copyright: © The Apache Software Foundation + deleteMyAccount: Delete my account + deleteTobAccount: Delete my Tour of Beam account + feedbackTitle: Enjoying Tour of Beam? + privacyPolicy: Privacy Policy + reportIssue: Report Issue in GitHub signIn: Sign in signOut: Sign out - solution: Solution toWebsite: To Apache Beam website pages: @@ -35,11 +39,22 @@ pages: startTour: Start your tour title: Welcome to the Tour of Beam! tour: + assignment: Assignment completeUnit: Complete Unit + content: Content + hint: Hint + showSolution: Show the solution + solution: Solution + solveYourself: Before revealing the solution, try solving the challenge on your own. Remember, the more you practice, the better you will become. Give it a shot and see how far you can get. + example: Example + myCode: My code + playground: Playground + saving: Saving... summaryTitle: Table of Contents dialogs: signInIf: If you would like to save your progress and track completed modules + deleteAccountWarning: Are you sure you want to delete your Tour of Beam account? This will permanently erase your learning progress. complexity: basic: Basic level diff --git a/learning/tour-of-beam/frontend/lib/assets/assets.gen.dart b/learning/tour-of-beam/frontend/lib/assets/assets.gen.dart index 924b1e38993b7..9cfcc7360cb1f 100644 --- a/learning/tour-of-beam/frontend/lib/assets/assets.gen.dart +++ b/learning/tour-of-beam/frontend/lib/assets/assets.gen.dart @@ -5,7 +5,7 @@ // coverage:ignore-file // ignore_for_file: type=lint -// ignore_for_file: directives_ordering,unnecessary_import +// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use import 'package:flutter/widgets.dart'; @@ -23,6 +23,9 @@ class $AssetsPngGen { /// File path: assets/png/profile-website.png AssetGenImage get profileWebsite => const AssetGenImage('assets/png/profile-website.png'); + + /// List of all assets + List get values => [laptopDark, laptopLight, profileWebsite]; } class $AssetsSvgGen { @@ -57,6 +60,20 @@ class $AssetsSvgGen { /// File path: assets/svg/welcome-progress-0.svg String get welcomeProgress0 => 'assets/svg/welcome-progress-0.svg'; + + /// List of all assets + List get values => [ + githubLogo, + googleLogo, + hint, + profileAbout, + profileDelete, + profileLogout, + solution, + unitProgress0, + unitProgress100, + welcomeProgress0 + ]; } class $AssetsTranslationsGen { @@ -64,6 +81,9 @@ class $AssetsTranslationsGen { /// File path: assets/translations/en.yaml String get en => 'assets/translations/en.yaml'; + + /// List of all assets + List get values => [en]; } class Assets { @@ -132,6 +152,8 @@ class AssetGenImage { ); } + ImageProvider provider() => AssetImage(_assetName); + String get path => _assetName; String get keyName => _assetName; diff --git a/learning/tour-of-beam/frontend/lib/auth/notifier.dart b/learning/tour-of-beam/frontend/lib/auth/notifier.dart index 2eb7be819f39c..2d59208277e3d 100644 --- a/learning/tour-of-beam/frontend/lib/auth/notifier.dart +++ b/learning/tour-of-beam/frontend/lib/auth/notifier.dart @@ -21,6 +21,10 @@ import 'dart:async'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:playground_components/playground_components.dart'; + +import '../cache/unit_progress.dart'; class AuthNotifier extends ChangeNotifier { AuthNotifier() { @@ -36,10 +40,25 @@ class AuthNotifier extends ChangeNotifier { } Future logIn(AuthProvider authProvider) async { - await FirebaseAuth.instance.signInWithPopup(authProvider); + try { + await FirebaseAuth.instance.signInWithPopup(authProvider); + } on Exception catch (e) { + PlaygroundComponents.toastNotifier.addException(e); + } } Future logOut() async { await FirebaseAuth.instance.signOut(); } + + Future deleteAccount() async { + try { + // If there are more things to do before account deletion, + // add final _accountDeletionListeners = []. + await GetIt.instance.get().deleteUserProgress(); + await FirebaseAuth.instance.currentUser?.delete(); + } on Exception catch (e) { + PlaygroundComponents.toastNotifier.addException(e); + } + } } diff --git a/learning/tour-of-beam/frontend/lib/cache/content_tree.dart b/learning/tour-of-beam/frontend/lib/cache/content_tree.dart index d67d0b891e8f1..fd683d21b8ba2 100644 --- a/learning/tour-of-beam/frontend/lib/cache/content_tree.dart +++ b/learning/tour-of-beam/frontend/lib/cache/content_tree.dart @@ -18,6 +18,8 @@ import 'dart:async'; +import 'package:playground_components/playground_components.dart'; + import '../models/content_tree.dart'; import 'cache.dart'; @@ -29,12 +31,12 @@ class ContentTreeCache extends Cache { final _treesBySdkId = {}; final _futuresBySdkId = >{}; - ContentTreeModel? getContentTree(String sdkId) { - if (!_futuresBySdkId.containsKey(sdkId)) { - unawaited(_loadContentTree(sdkId)); + ContentTreeModel? getContentTree(Sdk sdk) { + if (!_futuresBySdkId.containsKey(sdk.id)) { + unawaited(_loadContentTree(sdk.id)); } - return _treesBySdkId[sdkId]; + return _treesBySdkId[sdk.id]; } Future _loadContentTree(String sdkId) async { diff --git a/learning/tour-of-beam/frontend/lib/cache/unit_content.dart b/learning/tour-of-beam/frontend/lib/cache/unit_content.dart index d8499a641e272..8470ca3115d8d 100644 --- a/learning/tour-of-beam/frontend/lib/cache/unit_content.dart +++ b/learning/tour-of-beam/frontend/lib/cache/unit_content.dart @@ -29,13 +29,16 @@ class UnitContentCache extends Cache { final _unitContents = >{}; final _futures = >>{}; - UnitContentModel? getUnitContent(String sdkId, String unitId) { + Future getUnitContent( + String sdkId, + String unitId, + ) async { final future = _futures[sdkId]?[unitId]; if (future == null) { - unawaited(_loadUnitContent(sdkId, unitId)); + await _loadUnitContent(sdkId, unitId); } - return _unitContents[sdkId]?[unitId]; + return _unitContents[sdkId]![unitId]!; } Future _loadUnitContent(String sdkId, String unitId) async { diff --git a/learning/tour-of-beam/frontend/lib/cache/unit_progress.dart b/learning/tour-of-beam/frontend/lib/cache/unit_progress.dart index 5649e7464674d..db72d000331a0 100644 --- a/learning/tour-of-beam/frontend/lib/cache/unit_progress.dart +++ b/learning/tour-of-beam/frontend/lib/cache/unit_progress.dart @@ -18,23 +18,92 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; +import 'package:playground_components/playground_components.dart'; import '../auth/notifier.dart'; +import '../enums/snippet_type.dart'; import '../enums/unit_completion.dart'; +import '../models/unit_progress.dart'; +import '../repositories/client/client.dart'; import '../repositories/models/get_user_progress_response.dart'; +import '../repositories/user_progress/abstract.dart'; +import '../repositories/user_progress/cloud.dart'; +import '../repositories/user_progress/hive.dart'; import '../state.dart'; -import 'cache.dart'; -class UnitProgressCache extends Cache { - UnitProgressCache({required super.client}); +class UnitProgressCache extends ChangeNotifier { + final _cloudUserProgressRepository = CloudUserProgressRepository( + client: GetIt.instance.get(), + ); + final _localStorageUserProgressRepository = HiveUserProgressRepository(); + + AbstractUserProgressRepository _getUserProgressRepository() { + if (isAuthenticated) { + return _cloudUserProgressRepository; + } + return _localStorageUserProgressRepository; + } + + Future? _future; + + var _unitProgress = []; + final _unitProgressByUnitId = {}; final _completedUnitIds = {}; final _updatingUnitIds = {}; - Future? _future; + + bool get isAuthenticated => + GetIt.instance.get().isAuthenticated; + + Future loadUnitProgress(Sdk sdk) async { + _future = _getUserProgressRepository().getUserProgress(sdk); + final result = await _future; + + _unitProgressByUnitId.clear(); + if (result != null) { + _unitProgress = result.units; + for (final unitProgress in _unitProgress) { + _unitProgressByUnitId[unitProgress.id] = unitProgress; + } + } else { + _unitProgress = []; + } + notifyListeners(); + } + + List _getUnitProgress() { + if (_future == null) { + unawaited(loadUnitProgress(GetIt.instance.get().sdk!)); + } + return _unitProgress; + } + + // Completion + + Future completeUnit(String sdkId, String unitId) async { + try { + addUpdatingUnitId(unitId); + await _getUserProgressRepository().completeUnit(sdkId, unitId); + } finally { + await loadUnitProgress(GetIt.instance.get().sdk!); + clearUpdatingUnitId(unitId); + } + } Set getUpdatingUnitIds() => _updatingUnitIds; + Set getCompletedUnits() { + _completedUnitIds.clear(); + for (final unitProgress in _getUnitProgress()) { + if (unitProgress.isCompleted) { + _completedUnitIds.add(unitProgress.id); + } + } + return _completedUnitIds; + } + void addUpdatingUnitId(String unitId) { _updatingUnitIds.add(unitId); notifyListeners(); @@ -52,6 +121,14 @@ class UnitProgressCache extends Cache { return _getUnitCompletion(unitId) == UnitCompletion.uncompleted; } + bool isUnitCompleted(String? unitId) { + return getCompletedUnits().contains(unitId); + } + + String? getUnitSavedSnippetId(String? unitId) { + return _unitProgressByUnitId[unitId]?.userSnippetId; + } + UnitCompletion _getUnitCompletion(String unitId) { final authNotifier = GetIt.instance.get(); if (!authNotifier.isAuthenticated) { @@ -66,38 +143,40 @@ class UnitProgressCache extends Cache { return UnitCompletion.uncompleted; } - bool isUnitCompleted(String? unitId) { - return getCompletedUnits().contains(unitId); - } + // Snippet - Future updateCompletedUnits() async { - final sdkId = GetIt.instance.get().sdkId; - if (sdkId != null) { - await _loadCompletedUnits(sdkId); - } + bool hasSavedSnippet(String? unitId) { + return _unitProgressByUnitId[unitId]?.userSnippetId != null; } - Set getCompletedUnits() { - if (_future == null) { - unawaited(updateCompletedUnits()); - } - - return _completedUnitIds; + Future saveSnippet({ + required Sdk sdk, + required List snippetFiles, + required SnippetType snippetType, + required String unitId, + }) async { + await _getUserProgressRepository().saveUnitSnippet( + sdk: sdk, + snippetFiles: snippetFiles, + snippetType: snippetType, + unitId: unitId, + ); } - Future _loadCompletedUnits(String sdkId) async { - _future = client.getUserProgress(sdkId); - final result = await _future; - - _completedUnitIds.clear(); - if (result != null) { - for (final unitProgress in result.units) { - if (unitProgress.isCompleted) { - _completedUnitIds.add(unitProgress.id); - } - } - } + Future getSavedDescriptor({ + required Sdk sdk, + required String unitId, + }) async { + return _getUserProgressRepository().getSavedDescriptor( + sdk: sdk, + unitId: unitId, + ); + } - notifyListeners(); + Future deleteUserProgress() async { + await Future.wait([ + _localStorageUserProgressRepository.deleteUserProgress(), + _cloudUserProgressRepository.deleteUserProgress(), + ]); } } diff --git a/learning/tour-of-beam/frontend/lib/components/builders/content_tree.dart b/learning/tour-of-beam/frontend/lib/components/builders/content_tree.dart index 8ef706ead45ad..7be3430a8a307 100644 --- a/learning/tour-of-beam/frontend/lib/components/builders/content_tree.dart +++ b/learning/tour-of-beam/frontend/lib/components/builders/content_tree.dart @@ -18,16 +18,17 @@ import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; +import 'package:playground_components/playground_components.dart'; import '../../cache/content_tree.dart'; import '../../models/content_tree.dart'; class ContentTreeBuilder extends StatelessWidget { - final String sdkId; + final Sdk sdk; final ValueWidgetBuilder builder; const ContentTreeBuilder({ - required this.sdkId, + required this.sdk, required this.builder, }); @@ -39,7 +40,7 @@ class ContentTreeBuilder extends StatelessWidget { animation: contentTreeCache, builder: (context, child) => builder( context, - contentTreeCache.getContentTree(sdkId), + contentTreeCache.getContentTree(sdk), child, ), ); diff --git a/learning/tour-of-beam/frontend/lib/components/footer.dart b/learning/tour-of-beam/frontend/lib/components/footer.dart index 93c405ea3380f..daba2dbe6e96f 100644 --- a/learning/tour-of-beam/frontend/lib/components/footer.dart +++ b/learning/tour-of-beam/frontend/lib/components/footer.dart @@ -18,9 +18,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; import 'package:playground_components/playground_components.dart'; import '../constants/sizes.dart'; +import '../state.dart'; class Footer extends StatelessWidget { const Footer({ @@ -40,18 +42,16 @@ class Footer extends StatelessWidget { spacing: BeamSizes.size16, crossAxisAlignment: WrapCrossAlignment.center, children: [ + FeedbackWidget( + controller: GetIt.instance.get(), + title: 'ui.feedbackTitle'.tr(), + ), ReportIssueButton(playgroundController: playgroundController), const PrivacyPolicyButton(), const CopyrightWidget(), ], ), - // TODO(nausharipov): get version, https://github.com/apache/beam/issues/23038 - Text( - '${'ui.builtWith'.tr()} (TODO: Version)', - style: const TextStyle( - color: BeamColors.grey3, - ), - ), + const _BeamVersion(), ], ), ); @@ -85,3 +85,41 @@ class _Body extends StatelessWidget { ); } } + +class _BeamVersion extends StatelessWidget { + const _BeamVersion(); + + Future _getBeamSdkVersion() async { + final sdk = GetIt.instance.get().sdk; + if (sdk == null) { + return null; + } + final runnerVersion = await GetIt.instance + .get() + .getRunnerVersion(sdk); + return runnerVersion.beamSdkVersion; + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: GetIt.instance.get(), + builder: (context, child) => FutureBuilder( + // ignore: discarded_futures + future: _getBeamSdkVersion(), + builder: (context, snapshot) => snapshot.hasData + ? Text( + 'ui.builtWith'.tr( + namedArgs: { + 'beamSdkVersion': snapshot.data!, + }, + ), + style: const TextStyle( + color: BeamColors.grey3, + ), + ) + : Container(), + ), + ); + } +} diff --git a/learning/tour-of-beam/frontend/lib/components/login/button.dart b/learning/tour-of-beam/frontend/lib/components/login/button.dart index 2fb2038d6bfd8..6ecf2ba5da3b0 100644 --- a/learning/tour-of-beam/frontend/lib/components/login/button.dart +++ b/learning/tour-of-beam/frontend/lib/components/login/button.dart @@ -30,7 +30,7 @@ class LoginButton extends StatelessWidget { return TextButton( onPressed: () { final closeNotifier = PublicNotifier(); - openOverlay( + showOverlay( context: context, closeNotifier: closeNotifier, positioned: Positioned( diff --git a/learning/tour-of-beam/frontend/lib/components/login/content.dart b/learning/tour-of-beam/frontend/lib/components/login/content.dart index ae1fe80fa0281..88f1d2b1a3c18 100644 --- a/learning/tour-of-beam/frontend/lib/components/login/content.dart +++ b/learning/tour-of-beam/frontend/lib/components/login/content.dart @@ -84,10 +84,13 @@ class _BrandedLoginButtons extends StatelessWidget { required this.onLoggedIn, }); + Future _logIn(AuthProvider authProvider) async { + await GetIt.instance.get().logIn(authProvider); + onLoggedIn(); + } + @override Widget build(BuildContext context) { - final authNotifier = GetIt.instance.get(); - final isLightTheme = Theme.of(context).brightness == Brightness.light; final textStyle = MaterialStatePropertyAll(Theme.of(context).textTheme.bodyMedium); @@ -124,16 +127,17 @@ class _BrandedLoginButtons extends StatelessWidget { return Column( children: [ ElevatedButton.icon( - onPressed: () {}, + onPressed: () { + _logIn(GithubAuthProvider()); + }, style: isLightTheme ? githubLightButtonStyle : darkButtonStyle, icon: SvgPicture.asset(Assets.svg.githubLogo), label: const Text('ui.continueGitHub').tr(), ), const SizedBox(height: BeamSizes.size16), ElevatedButton.icon( - onPressed: () async { - await authNotifier.logIn(GoogleAuthProvider()); - onLoggedIn(); + onPressed: () { + _logIn(GoogleAuthProvider()); }, style: isLightTheme ? googleLightButtonStyle : darkButtonStyle, icon: SvgPicture.asset(Assets.svg.googleLogo), diff --git a/learning/tour-of-beam/frontend/lib/components/profile/avatar.dart b/learning/tour-of-beam/frontend/lib/components/profile/avatar.dart index d4b88584dab65..d6e6621e97f0e 100644 --- a/learning/tour-of-beam/frontend/lib/components/profile/avatar.dart +++ b/learning/tour-of-beam/frontend/lib/components/profile/avatar.dart @@ -32,7 +32,7 @@ class Avatar extends StatelessWidget { return GestureDetector( onTap: () { final closeNotifier = PublicNotifier(); - openOverlay( + showOverlay( context: context, closeNotifier: closeNotifier, positioned: Positioned( diff --git a/learning/tour-of-beam/frontend/lib/components/profile/user_menu.dart b/learning/tour-of-beam/frontend/lib/components/profile/user_menu.dart index 5fb49fa7192b1..f3f633b0028f9 100644 --- a/learning/tour-of-beam/frontend/lib/components/profile/user_menu.dart +++ b/learning/tour-of-beam/frontend/lib/components/profile/user_menu.dart @@ -125,9 +125,24 @@ class _Buttons extends StatelessWidget { ), const BeamDivider(), _IconLabel( - onTap: () {}, + onTap: () async { + closeOverlayCallback(); + final confirmed = await ConfirmDialog.show( + context: context, + confirmButtonText: 'ui.deleteMyAccount'.tr(), + subtitle: 'dialogs.deleteAccountWarning'.tr(), + title: 'ui.deleteTobAccount'.tr(), + ); + if (confirmed) { + ProgressDialog.show( + future: authNotifier.deleteAccount(), + navigatorKey: + GetIt.instance.get().navigatorKey!, + ); + } + }, iconPath: Assets.svg.profileDelete, - label: 'ui.deleteAccount'.tr(), + label: 'ui.deleteMyAccount'.tr(), ), ], ); diff --git a/learning/tour-of-beam/frontend/lib/components/scaffold.dart b/learning/tour-of-beam/frontend/lib/components/scaffold.dart index cbbade4c26c84..ea91b4cbe0792 100644 --- a/learning/tour-of-beam/frontend/lib/components/scaffold.dart +++ b/learning/tour-of-beam/frontend/lib/components/scaffold.dart @@ -103,13 +103,13 @@ class _SdkSelector extends StatelessWidget { return AnimatedBuilder( animation: appNotifier, builder: (context, child) { - final sdkId = appNotifier.sdkId; - return sdkId == null + final sdk = appNotifier.sdk; + return sdk == null ? Container() : SdkDropdown( - sdkId: sdkId, + value: sdk, onChanged: (value) { - appNotifier.sdkId = value; + appNotifier.sdk = value; }, ); }, diff --git a/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart b/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart index 9e84e3698e374..45e884db58a1e 100644 --- a/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart +++ b/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart @@ -22,11 +22,11 @@ import 'package:playground_components/playground_components.dart'; import 'builders/sdks.dart'; class SdkDropdown extends StatelessWidget { - final String sdkId; - final ValueChanged onChanged; + final Sdk value; + final ValueChanged onChanged; const SdkDropdown({ - required this.sdkId, + required this.value, required this.onChanged, }); @@ -40,10 +40,10 @@ class SdkDropdown extends StatelessWidget { return _DropdownWrapper( child: DropdownButton( - value: sdkId, - onChanged: (sdk) { - if (sdk != null) { - onChanged(sdk); + value: value.id, + onChanged: (sdkId) { + if (sdkId != null) { + onChanged(Sdk.parseOrCreate(sdkId)); } }, items: sdks diff --git a/learning/tour-of-beam/frontend/lib/config.dart b/learning/tour-of-beam/frontend/lib/config.dart index 3fb4be59eff9e..e82b333e16631 100644 --- a/learning/tour-of-beam/frontend/lib/config.dart +++ b/learning/tour-of-beam/frontend/lib/config.dart @@ -18,8 +18,10 @@ // TODO(alexeyinkin): Generate this file on deployment. -const _cloudFunctionsProjectRegion = 'us-central1'; -const _cloudFunctionsProjectId = 'tour-of-beam-2'; +const environment = 'stg_'; + +const _cloudFunctionsProjectRegion = 'us-west1'; +const _cloudFunctionsProjectId = 'apache-beam-testing'; const cloudFunctionsBaseUrl = 'https://' '$_cloudFunctionsProjectRegion-$_cloudFunctionsProjectId' - '.cloudfunctions.net'; + '.cloudfunctions.net/$environment'; diff --git a/learning/tour-of-beam/frontend/lib/constants/hive_box_names.dart b/learning/tour-of-beam/frontend/lib/constants/hive_box_names.dart new file mode 100644 index 0000000000000..c3900ccc5823c --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/constants/hive_box_names.dart @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:playground_components/playground_components.dart'; + +class HiveBoxNames { + static const unitProgress = 'unit_progress'; + static const snippets = 'snippets'; + + static String getSdkBoxName(Sdk sdk, String boxName) { + return '${sdk.id}_$boxName'; + } +} diff --git a/learning/tour-of-beam/frontend/lib/constants/sizes.dart b/learning/tour-of-beam/frontend/lib/constants/sizes.dart index fd0eec3b97d18..9bd2c9477622e 100644 --- a/learning/tour-of-beam/frontend/lib/constants/sizes.dart +++ b/learning/tour-of-beam/frontend/lib/constants/sizes.dart @@ -19,7 +19,7 @@ class TobSizes { static const double footerHeight = 35; static const double authOverlayWidth = 260; - static const double hintPopupWidth = 420; + static const double hintPopupWidth = 510; } class ScreenSizes { diff --git a/learning/tour-of-beam/frontend/lib/enums/save_code_status.dart b/learning/tour-of-beam/frontend/lib/enums/save_code_status.dart new file mode 100644 index 0000000000000..cd5d302a198a0 --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/enums/save_code_status.dart @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +enum SaveCodeStatus { + error, + saved, + saving, +} diff --git a/learning/tour-of-beam/frontend/lib/enums/snippet_type.dart b/learning/tour-of-beam/frontend/lib/enums/snippet_type.dart new file mode 100644 index 0000000000000..202d1c6fb9da5 --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/enums/snippet_type.dart @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +enum SnippetType { + original, + saved, + solution, +} diff --git a/learning/tour-of-beam/frontend/lib/enums/tour_view.dart b/learning/tour-of-beam/frontend/lib/enums/tour_view.dart new file mode 100644 index 0000000000000..06eaea6fe766c --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/enums/tour_view.dart @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import 'package:enum_map/enum_map.dart'; + +part 'tour_view.g.dart'; + +@unmodifiableEnumMap +enum TourView { + content, + playground, +} diff --git a/learning/tour-of-beam/frontend/lib/enums/tour_view.g.dart b/learning/tour-of-beam/frontend/lib/enums/tour_view.g.dart new file mode 100644 index 0000000000000..c131f8aeca0c5 --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/enums/tour_view.g.dart @@ -0,0 +1,162 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tour_view.dart'; + +// ************************************************************************** +// UnmodifiableEnumMapGenerator +// ************************************************************************** + +class UnmodifiableTourViewMap extends UnmodifiableEnumMap { + final V content; + final V playground; + + const UnmodifiableTourViewMap({ + required this.content, + required this.playground, + }); + + @override + Map cast() { + return Map.castFrom(this); + } + + @override + bool containsValue(Object? value) { + if (this.content == value) return true; + if (this.playground == value) return true; + return false; + } + + @override + bool containsKey(Object? key) { + return key.runtimeType == TourView; + } + + @override + V? operator [](Object? key) { + switch (key) { + case TourView.content: + return this.content; + case TourView.playground: + return this.playground; + } + + return null; + } + + @override + void operator []=(TourView key, V value) { + throw Exception("Cannot modify this map."); + } + + @override + Iterable> get entries { + return [ + MapEntry(TourView.content, this.content), + MapEntry(TourView.playground, this.playground), + ]; + } + + @override + Map map(MapEntry transform(TourView key, V value)) { + final content = transform(TourView.content, this.content); + final playground = transform(TourView.playground, this.playground); + return { + content.key: content.value, + playground.key: playground.value, + }; + } + + @override + void addEntries(Iterable> newEntries) { + throw Exception("Cannot modify this map."); + } + + @override + V update(TourView key, V update(V value), {V Function()? ifAbsent}) { + throw Exception("Cannot modify this map."); + } + + @override + void updateAll(V update(TourView key, V value)) { + throw Exception("Cannot modify this map."); + } + + @override + void removeWhere(bool test(TourView key, V value)) { + throw Exception("Objects in this map cannot be removed."); + } + + @override + V putIfAbsent(TourView key, V ifAbsent()) { + return this.get(key); + } + + @override + void addAll(Map other) { + throw Exception("Cannot modify this map."); + } + + @override + V? remove(Object? key) { + throw Exception("Objects in this map cannot be removed."); + } + + @override + void clear() { + throw Exception("Objects in this map cannot be removed."); + } + + @override + void forEach(void action(TourView key, V value)) { + action(TourView.content, this.content); + action(TourView.playground, this.playground); + } + + @override + Iterable get keys { + return TourView.values; + } + + @override + Iterable get values { + return [ + this.content, + this.playground, + ]; + } + + @override + int get length { + return 2; + } + + @override + bool get isEmpty { + return false; + } + + @override + bool get isNotEmpty { + return true; + } + + V get(TourView key) { + switch (key) { + case TourView.content: + return this.content; + case TourView.playground: + return this.playground; + } + } + + @override + String toString() { + final buffer = StringBuffer("{"); + buffer.write("TourView.content: ${this.content}"); + buffer.write(", "); + buffer.write("TourView.playground: ${this.playground}"); + buffer.write("}"); + return buffer.toString(); + } +} diff --git a/learning/tour-of-beam/frontend/lib/firebase_options.dart b/learning/tour-of-beam/frontend/lib/firebase_options.dart index e2a871d637b36..e64b594af4335 100644 --- a/learning/tour-of-beam/frontend/lib/firebase_options.dart +++ b/learning/tour-of-beam/frontend/lib/firebase_options.dart @@ -53,11 +53,11 @@ class DefaultFirebaseOptions { } static const FirebaseOptions web = FirebaseOptions( - apiKey: 'AIzaSyBtAreurqJ5D4IK6cNisZh5dnDRKljbJAw', - authDomain: 'astest-369409.firebaseapp.com', - projectId: 'astest-369409', - storageBucket: 'astest-369409.appspot.com', - messagingSenderId: '534850967604', - appId: '1:534850967604:web:55c6af8da7940df1ddd261', + apiKey: 'AIzaSyAg7ZLslQRrEhwVVVzZb1OFMRMHL8kNL38', + authDomain: 'apache-beam-testing.firebaseapp.com', + projectId: 'apache-beam-testing', + storageBucket: 'apache-beam-testing.appspot.com', + messagingSenderId: '844138762903', + appId: '1:844138762903:web:c0094c4e6bba87d73d8fd2', ); } diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/locator.dart index e627c3b99ca6b..46095e92948a9 100644 --- a/learning/tour-of-beam/frontend/lib/locator.dart +++ b/learning/tour-of-beam/frontend/lib/locator.dart @@ -25,7 +25,6 @@ import 'cache/content_tree.dart'; import 'cache/sdk.dart'; import 'cache/unit_content.dart'; import 'cache/unit_progress.dart'; -import 'config.dart'; import 'pages/welcome/page.dart'; import 'repositories/client/client.dart'; import 'repositories/client/cloud_functions_client.dart'; @@ -33,13 +32,36 @@ import 'router/page_factory.dart'; import 'router/route_information_parser.dart'; import 'state.dart'; +final _client = CloudFunctionsTobClient(); + Future initializeServiceLocator() async { + await _initializeRepositories(); _initializeAuth(); _initializeState(); _initializeServices(); _initializeCaches(); } +Future _initializeRepositories() async { + final routerUrl = await getRouterUrl(); + + final codeClient = GrpcCodeClient( + url: routerUrl, + // TODO(nausharipov): Remove the hardcoded SDKs when runners are hidden. + runnerUrlsById: { + Sdk.java.id: await getRunnerUrl(Sdk.java), + Sdk.go.id: await getRunnerUrl(Sdk.go), + Sdk.python.id: await getRunnerUrl(Sdk.python), + }, + ); + final exampleClient = GrpcExampleClient(url: routerUrl); + + GetIt.instance.registerSingleton(codeClient); + GetIt.instance.registerSingleton(CodeRepository(client: codeClient)); + GetIt.instance.registerSingleton(exampleClient); + GetIt.instance.registerSingleton(ExampleRepository(client: exampleClient)); +} + void _initializeAuth() { GetIt.instance.registerSingleton(AuthNotifier()); } @@ -51,18 +73,18 @@ void _initializeCaches() { GetIt.instance.registerSingleton(ContentTreeCache(client: client)); GetIt.instance.registerSingleton(SdkCache(client: client)); GetIt.instance.registerSingleton(UnitContentCache(client: client)); - GetIt.instance.registerSingleton(UnitProgressCache(client: client)); + GetIt.instance.registerSingleton(UnitProgressCache()); } void _initializeState() { - GetIt.instance.registerSingleton(AppNotifier()); - GetIt.instance.registerSingleton( - PageStack( - bottomPage: WelcomePage(), - createPage: PageFactory.createPage, - routeInformationParser: TobRouteInformationParser(), - ), + final pageStack = PageStack( + bottomPage: WelcomePage(), + createPage: PageFactory.createPage, + routeInformationParser: TobRouteInformationParser(), ); + GetIt.instance.registerSingleton(AppNotifier()); + GetIt.instance.registerSingleton(pageStack); + GetIt.instance.registerSingleton(BeamRouterDelegate(pageStack)); } void _initializeServices() { diff --git a/learning/tour-of-beam/frontend/lib/main.dart b/learning/tour-of-beam/frontend/lib/main.dart index 405c99b25da61..c8493293960c2 100644 --- a/learning/tour-of-beam/frontend/lib/main.dart +++ b/learning/tour-of-beam/frontend/lib/main.dart @@ -42,7 +42,7 @@ void main() async { const englishLocale = Locale('en'); final pageStack = GetIt.instance.get(); - final routerDelegate = PageStackRouterDelegate(pageStack); + final routerDelegate = GetIt.instance.get(); final routeInformationParser = TobRouteInformationParser(); final backButtonDispatcher = PageStackBackButtonDispatcher(pageStack); diff --git a/learning/tour-of-beam/frontend/lib/models/unit_progress.dart b/learning/tour-of-beam/frontend/lib/models/unit_progress.dart index 473c8ae0d4e6d..e3753b81048d1 100644 --- a/learning/tour-of-beam/frontend/lib/models/unit_progress.dart +++ b/learning/tour-of-beam/frontend/lib/models/unit_progress.dart @@ -20,16 +20,20 @@ import 'package:json_annotation/json_annotation.dart'; part 'unit_progress.g.dart'; -@JsonSerializable(createToJson: false) +@JsonSerializable() class UnitProgressModel { final String id; final bool isCompleted; + final String? userSnippetId; const UnitProgressModel({ required this.id, required this.isCompleted, + required this.userSnippetId, }); factory UnitProgressModel.fromJson(Map json) => _$UnitProgressModelFromJson(json); + + Map toJson() => _$UnitProgressModelToJson(this); } diff --git a/learning/tour-of-beam/frontend/lib/models/unit_progress.g.dart b/learning/tour-of-beam/frontend/lib/models/unit_progress.g.dart index c1a773cd66a9c..5c5511604b2be 100644 --- a/learning/tour-of-beam/frontend/lib/models/unit_progress.g.dart +++ b/learning/tour-of-beam/frontend/lib/models/unit_progress.g.dart @@ -10,4 +10,12 @@ UnitProgressModel _$UnitProgressModelFromJson(Map json) => UnitProgressModel( id: json['id'] as String, isCompleted: json['isCompleted'] as bool, + userSnippetId: json['userSnippetId'] as String?, ); + +Map _$UnitProgressModelToJson(UnitProgressModel instance) => + { + 'id': instance.id, + 'isCompleted': instance.isCompleted, + 'userSnippetId': instance.userSnippetId, + }; diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/controllers/content_tree.dart b/learning/tour-of-beam/frontend/lib/pages/tour/controllers/content_tree.dart index bcdb686a10b75..aa61187782195 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/controllers/content_tree.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/controllers/content_tree.dart @@ -26,9 +26,8 @@ import '../../../models/node.dart'; import '../../../models/unit.dart'; class ContentTreeController extends ChangeNotifier { - String _sdkId; + Sdk _sdk; List _treeIds; - // TODO(nausharipov): non-nullable currentNode? NodeModel? _currentNode; final _contentTreeCache = GetIt.instance.get(); final _expandedIds = {}; @@ -36,9 +35,9 @@ class ContentTreeController extends ChangeNotifier { Set get expandedIds => _expandedIds; ContentTreeController({ - required String initialSdkId, + required Sdk initialSdk, List initialTreeIds = const [], - }) : _sdkId = initialSdkId, + }) : _sdk = initialSdk, _treeIds = initialTreeIds { _expandedIds.addAll(initialTreeIds); @@ -46,29 +45,23 @@ class ContentTreeController extends ChangeNotifier { _onContentTreeCacheChange(); } - Sdk get sdk => Sdk.parseOrCreate(_sdkId); - String get sdkId => _sdkId; - set sdkId(String newValue) { - _sdkId = newValue; + Sdk get sdk => _sdk; + + set sdk(Sdk newValue) { + _sdk = newValue; notifyListeners(); } List get treeIds => _treeIds; NodeModel? get currentNode => _currentNode; - void openNode(NodeModel node) { - if (!_expandedIds.contains(node.id)) { - _expandedIds.add(node.id); - } - - if (node == _currentNode) { - return; - } - + void onNodePressed(NodeModel node) { if (node is GroupModel) { - openNode(node.nodes.first); + _onGroupPressed(node); } else if (node is UnitModel) { - _currentNode = node; + if (node != _currentNode) { + _currentNode = node; + } } if (_currentNode != null) { @@ -77,6 +70,19 @@ class ContentTreeController extends ChangeNotifier { notifyListeners(); } + void _onGroupPressed(GroupModel group) { + if (_expandedIds.contains(group.id)) { + _expandedIds.remove(group.id); + notifyListeners(); + } else { + _expandedIds.add(group.id); + final groupFirstUnit = group.nodes.first; + if (groupFirstUnit != _currentNode) { + onNodePressed(groupFirstUnit); + } + } + } + void expandGroup(GroupModel group) { _expandedIds.add(group.id); notifyListeners(); @@ -98,12 +104,12 @@ class ContentTreeController extends ChangeNotifier { } void _onContentTreeCacheChange() { - final contentTree = _contentTreeCache.getContentTree(_sdkId); + final contentTree = _contentTreeCache.getContentTree(_sdk); if (contentTree == null) { return; } - openNode( + onNodePressed( contentTree.getNodeByTreeIds(_treeIds) ?? contentTree.getFirstUnit(), ); diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/controllers/unit.dart b/learning/tour-of-beam/frontend/lib/pages/tour/controllers/unit.dart deleted file mode 100644 index 2008b403c39ca..0000000000000 --- a/learning/tour-of-beam/frontend/lib/pages/tour/controllers/unit.dart +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/widgets.dart'; -import 'package:get_it/get_it.dart'; -import 'package:playground_components/playground_components.dart'; - -import '../../../cache/unit_progress.dart'; -import '../../../models/event_context.dart'; -import '../../../models/unit.dart'; -import '../../../repositories/client/client.dart'; -import '../../../services/analytics/events/unit_completed.dart'; - -/// The state object for the [unit] being currently open. -class UnitController extends ChangeNotifier { - final UnitModel unit; - final Sdk sdk; - - UnitController({ - required this.unit, - required this.sdk, - }); - - Future completeUnit() async { - final client = GetIt.instance.get(); - final unitProgressCache = GetIt.instance.get(); - try { - unitProgressCache.addUpdatingUnitId(unit.id); - await client.postUnitComplete(sdk.id, unit.id); - } finally { - PlaygroundComponents.analyticsService.sendUnawaited( - UnitCompletedTobAnalyticsEvent( - tobContext: TobEventContext( - sdkId: sdk.id, - unitId: unit.id, - ), - ), - ); - await unitProgressCache.updateCompletedUnits(); - unitProgressCache.clearUpdatingUnitId(unit.id); - } - } -} diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/page.dart b/learning/tour-of-beam/frontend/lib/pages/tour/page.dart index 1272a5a35fbb1..628576e3a2179 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/page.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/page.dart @@ -18,6 +18,7 @@ import 'package:app_state/app_state.dart'; import 'package:flutter/widgets.dart'; +import 'package:playground_components/playground_components.dart'; import 'screen.dart'; import 'state.dart'; @@ -27,12 +28,12 @@ class TourPage extends StatefulMaterialPage { /// Called when navigating to the page programmatically. TourPage({ - required String sdkId, + required Sdk sdk, List treeIds = const [], }) : super( key: const ValueKey(classFactoryKey), state: TourNotifier( - initialSdkId: sdkId, + initialSdk: sdk, initialTreeIds: treeIds, ), createScreen: TourScreen.new, @@ -43,7 +44,7 @@ class TourPage extends StatefulMaterialPage { final treeIds = state['treeIds']; return TourPage( - sdkId: state['sdkId'], + sdk: Sdk.parseOrCreate(state['sdkId']), treeIds: treeIds is List ? treeIds.cast() : const [], ); } diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/path.dart b/learning/tour-of-beam/frontend/lib/pages/tour/path.dart index 07dd386bdfcbb..af8452d175487 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/path.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/path.dart @@ -26,7 +26,8 @@ class TourPath extends PagePath { final String sdkId; final List treeIds; - static final _regExp = RegExp(r'^/tour/([a-z]+)((/[-a-zA-Z0-9]+)*)$'); + static final _regExp = + RegExp(r'^/tour/([a-z]+)/?([-a-zA-Z0-9]+(/[-a-zA-Z0-9]+)*)?/?$'); TourPath({ required this.sdkId, @@ -49,10 +50,8 @@ class TourPath extends PagePath { final sdkId = matches[1] ?? (throw Error()); final treeIdsString = matches[2]; - final treeIds = (treeIdsString == null) - ? const [] - // TODO(nausharipov): use RegExp to remove the slash - : treeIdsString.substring(1).split('/'); + final treeIds = + treeIdsString == null ? const [] : treeIdsString.split('/'); return TourPath( sdkId: sdkId, diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart index 383e5f315438d..21bb110177273 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart @@ -16,11 +16,14 @@ * limitations under the License. */ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:keyed_collection_widgets/keyed_collection_widgets.dart'; import 'package:playground_components/playground_components.dart'; import '../../components/scaffold.dart'; import '../../constants/sizes.dart'; +import '../../enums/tour_view.dart'; import '../../shortcuts/shortcuts_manager.dart'; import 'state.dart'; import 'widgets/content_tree.dart'; @@ -78,36 +81,42 @@ class _NarrowTour extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ContentTreeWidget(controller: tourNotifier.contentTreeController), - Expanded(child: UnitContentWidget(tourNotifier)), - ], - ), - DecoratedBox( - decoration: BoxDecoration( - border: Border( - top: BorderSide(color: Theme.of(context).dividerColor), - ), + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ContentTreeWidget(controller: tourNotifier.contentTreeController), + Expanded( + child: DefaultKeyedTabController.fromKeys( + keys: TourView.values, + child: Column( + children: [ + KeyedTabBar.withDefaultController( + tabs: UnmodifiableTourViewMap( + content: Tab( + text: 'pages.tour.content'.tr(), + ), + playground: Tab( + text: 'pages.tour.playground'.tr(), + ), + ), + ), + Expanded( + child: KeyedTabBarView.withDefaultController( + children: UnmodifiableTourViewMap( + content: UnitContentWidget( + tourNotifier, + ), + playground: PlaygroundWidget( + tourNotifier: tourNotifier, + ), + ), + ), + ), + ], ), - child: const _NarrowScreenPlayground(), ), - ], - ), + ), + ], ); } } - -class _NarrowScreenPlayground extends StatelessWidget { - const _NarrowScreenPlayground(); - - @override - Widget build(BuildContext context) { - // TODO(alexeyinkin): Even this way the narrow layout breaks, https://github.com/apache/beam/issues/23244 - return const Center(child: Text('TODO: Playground for narrow screen')); - } -} diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/state.dart b/learning/tour-of-beam/frontend/lib/pages/tour/state.dart index c069b384d73f0..ea2d2ac113f81 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/state.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/state.dart @@ -22,11 +22,14 @@ import 'package:app_state/app_state.dart'; import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; import 'package:playground_components/playground_components.dart'; +import 'package:rate_limiter/rate_limiter.dart'; import '../../auth/notifier.dart'; import '../../cache/unit_content.dart'; import '../../cache/unit_progress.dart'; import '../../config.dart'; +import '../../enums/save_code_status.dart'; +import '../../enums/snippet_type.dart'; import '../../models/event_context.dart'; import '../../models/unit.dart'; import '../../models/unit_content.dart'; @@ -34,13 +37,14 @@ import '../../services/analytics/events/unit_closed.dart'; import '../../services/analytics/events/unit_opened.dart'; import '../../state.dart'; import 'controllers/content_tree.dart'; -import 'controllers/unit.dart'; import 'path.dart'; class TourNotifier extends ChangeNotifier with PageStateMixin { + static const _saveUserCodeDebounceDuration = Duration(seconds: 2); + Debounce? _saveCodeDebounced; + final ContentTreeController contentTreeController; final PlaygroundController playgroundController; - UnitController? currentUnitController; final _appNotifier = GetIt.instance.get(); final _authNotifier = GetIt.instance.get(); final _unitContentCache = GetIt.instance.get(); @@ -52,113 +56,125 @@ class TourNotifier extends ChangeNotifier with PageStateMixin { TobEventContext get tobEventContext => _tobEventContext; TourNotifier({ - required String initialSdkId, + required Sdk initialSdk, List initialTreeIds = const [], }) : contentTreeController = ContentTreeController( - initialSdkId: initialSdkId, + initialSdk: initialSdk, initialTreeIds: initialTreeIds, ), - playgroundController = _createPlaygroundController(initialSdkId) { + playgroundController = _createPlaygroundController(initialSdk.id) { + _appNotifier.sdk ??= initialSdk; contentTreeController.addListener(_onUnitChanged); _unitContentCache.addListener(_onUnitChanged); _appNotifier.addListener(_onAppNotifierChanged); - _authNotifier.addListener(_onUnitProgressChanged); - _onUnitChanged(); + _authNotifier.addListener(_onAuthChanged); + _saveCodeDebounced = _saveCode.debounced( + _saveUserCodeDebounceDuration, + ); + // setSdk creates snippetEditingController if it doesn't exist. + playgroundController.setSdk(currentSdk); + _listenToCurrentSnippetEditingController(); + unawaited(_onUnitChanged()); + } + + @override + void setStateMap(Map state) { + super.setStateMap(state); + _appNotifier.sdk = Sdk.parseOrCreate(state['sdkId']); } @override PagePath get path => TourPath( - sdkId: contentTreeController.sdkId, + sdkId: contentTreeController.sdk.id, treeIds: contentTreeController.treeIds, ); - String? get currentUnitId => currentUnitController?.unit.id; + bool get isAuthenticated => _authNotifier.isAuthenticated; + + Sdk get currentSdk => _appNotifier.sdk!; + String? get currentUnitId => _currentUnitContent?.id; UnitContentModel? get currentUnitContent => _currentUnitContent; - bool get doesCurrentUnitHaveSolution => - currentUnitContent?.solutionSnippetId != null; - bool _isShowingSolution = false; - bool get isShowingSolution => _isShowingSolution; - - void toggleShowingSolution() { - if (doesCurrentUnitHaveSolution) { - _isShowingSolution = !_isShowingSolution; - - final snippetId = _isShowingSolution - ? _currentUnitContent?.solutionSnippetId - : _currentUnitContent?.taskSnippetId; - if (snippetId != null) { - // TODO(nausharipov): store/recover - unawaited(_setPlaygroundSnippet(snippetId)); - } - - notifyListeners(); - } - } - void _createCurrentUnitController(Sdk sdk, UnitModel unit) { - currentUnitController = UnitController( - unit: unit, - sdk: sdk, - ); - } + bool get hasSolution => currentUnitContent?.solutionSnippetId != null; + bool get isCodeSaved => _unitProgressCache.hasSavedSnippet(currentUnitId); + + SnippetType _snippetType = SnippetType.original; + SnippetType get snippetType => _snippetType; - Future _onUnitProgressChanged() async { - await _unitProgressCache.updateCompletedUnits(); + SaveCodeStatus _saveCodeStatus = SaveCodeStatus.saved; + SaveCodeStatus get saveCodeStatus => _saveCodeStatus; + set saveCodeStatus(SaveCodeStatus saveCodeStatus) { + _saveCodeStatus = saveCodeStatus; + notifyListeners(); } - void _onAppNotifierChanged() { - final sdkId = _appNotifier.sdkId; - if (sdkId != null) { - playgroundController.setSdk(Sdk.parseOrCreate(sdkId)); - contentTreeController.sdkId = sdkId; - _onUnitProgressChanged(); + Future _onAuthChanged() async { + await _unitProgressCache.loadUnitProgress(currentSdk); + // The local changes are preserved if the user signs in. + if (_snippetType != SnippetType.saved || !isAuthenticated) { + _trySetSnippetType(SnippetType.saved); + await _loadSnippetByType(); } + notifyListeners(); + } + + Future _onAppNotifierChanged() async { + contentTreeController.sdk = currentSdk; + playgroundController.setSdk(currentSdk); + _listenToCurrentSnippetEditingController(); + + await _unitProgressCache.loadUnitProgress(currentSdk); + _trySetSnippetType(SnippetType.saved); + await _loadSnippetByType(); } - void _onUnitChanged() { + Future _onUnitChanged() async { emitPathChanged(); final currentNode = contentTreeController.currentNode; - if (currentNode is UnitModel) { - final sdk = contentTreeController.sdk; - _createCurrentUnitController(contentTreeController.sdk, currentNode); - _setCurrentUnitContent(currentNode, sdk: sdk); + if (currentNode is! UnitModel) { + await _emptyPlayground(); } else { - _emptyPlayground(); + final sdk = contentTreeController.sdk; + final content = await _unitContentCache.getUnitContent( + sdk.id, + currentNode.id, + ); + _setUnitContent(content); + await _unitProgressCache.loadUnitProgress(currentSdk); + _trySetSnippetType(SnippetType.saved); + await _loadSnippetByType(); } - notifyListeners(); } - Future _setCurrentUnitContent( - UnitModel unit, { - required Sdk sdk, - }) async { - final content = _unitContentCache.getUnitContent( - sdk.id, - unit.id, - ); - if (content == _currentUnitContent) { + void _setUnitContent(UnitContentModel? unitContent) { + if (unitContent == null || unitContent == _currentUnitContent) { return; } if (_currentUnitOpenedAt != null && _currentUnitContent != null) { - PlaygroundComponents.analyticsService.sendUnawaited( - UnitClosedTobAnalyticsEvent( - tobContext: _tobEventContext, - timeSpent: DateTime.now().difference(_currentUnitOpenedAt!), - ), - ); + _trackUnitClosed(); } - _currentUnitContent = content; - if (content == null) { - return; - } + _currentUnitContent = unitContent; + _trackUnitOpened(unitContent.id); + } + + void _trackUnitClosed() { + PlaygroundComponents.analyticsService.sendUnawaited( + UnitClosedTobAnalyticsEvent( + tobContext: _tobEventContext, + timeSpent: DateTime.now().difference(_currentUnitOpenedAt!), + ), + ); + } + + void _trackUnitOpened(String unitId) { _currentUnitOpenedAt = DateTime.now(); _tobEventContext = TobEventContext( - sdkId: sdk.id, - unitId: unit.id, + sdkId: currentSdk.id, + unitId: unitId, ); playgroundController .requireSnippetEditingController() @@ -168,60 +184,165 @@ class TourNotifier extends ChangeNotifier with PageStateMixin { tobContext: _tobEventContext, ), ); + } - final taskSnippetId = content.taskSnippetId; - await _setPlaygroundSnippet(taskSnippetId); - _isShowingSolution = false; + // Save user code. + + Future showSnippetByType(SnippetType snippetType) async { + _trySetSnippetType(snippetType); + await _loadSnippetByType(); + notifyListeners(); } - Future _setPlaygroundSnippet(String? snippetId) async { - if (snippetId == null) { - await _emptyPlayground(); - return; + void _listenToCurrentSnippetEditingController() { + playgroundController.snippetEditingController?.addListener( + _onActiveFileControllerChanged, + ); + } + + void _onActiveFileControllerChanged() { + playgroundController + .snippetEditingController?.activeFileController?.codeController + .addListener(_onCodeChanged); + } + + void _onCodeChanged() { + final snippetEditingController = + playgroundController.snippetEditingController!; + final isCodeChanged = + snippetEditingController.activeFileController?.isChanged ?? false; + final snippetFiles = snippetEditingController.getFiles(); + + final doSave = _isSnippetTypeSavable() && + isCodeChanged && + _currentUnitContent != null && + snippetFiles.isNotEmpty; + + if (doSave) { + // Snapshot of sdk and unitId at the moment of editing. + final sdk = currentSdk; + final unitId = currentUnitId; + _saveCodeDebounced?.call([], { + const Symbol('sdk'): sdk, + const Symbol('snippetFiles'): snippetFiles, + const Symbol('unitId'): unitId, + }); } + } - final selectedSdk = _appNotifier.sdk; - if (selectedSdk != null) { - await playgroundController.examplesLoader.load( - ExamplesLoadingDescriptor( - descriptors: [ - UserSharedExampleLoadingDescriptor( - sdk: selectedSdk, - snippetId: snippetId, - ), - ], - ), + bool _isSnippetTypeSavable() { + return snippetType != SnippetType.solution; + } + + Future _saveCode({ + required Sdk sdk, + required List snippetFiles, + required String unitId, + }) async { + saveCodeStatus = SaveCodeStatus.saving; + try { + await _unitProgressCache.saveSnippet( + sdk: sdk, + snippetFiles: snippetFiles, + snippetType: _snippetType, + unitId: unitId, ); + saveCodeStatus = SaveCodeStatus.saved; + await _unitProgressCache.loadUnitProgress(currentSdk); + _trySetSnippetType(SnippetType.saved); + } on Exception catch (e) { + print(['Could not save code: ', e]); + _saveCodeStatus = SaveCodeStatus.error; } } - // TODO(alexeyinkin): Hide the entire right pane instead. - Future _emptyPlayground() async { + void _trySetSnippetType(SnippetType snippetType) { + if (snippetType == SnippetType.saved && !isCodeSaved) { + _snippetType = SnippetType.original; + } else { + _snippetType = snippetType; + } + notifyListeners(); + } + + Future _loadSnippetByType() async { + final ExampleLoadingDescriptor descriptor; + switch (_snippetType) { + case SnippetType.original: + descriptor = _getStandardOrEmptyDescriptor( + currentSdk, + _currentUnitContent!.taskSnippetId, + ); + break; + case SnippetType.saved: + descriptor = await _unitProgressCache.getSavedDescriptor( + sdk: currentSdk, + unitId: _currentUnitContent!.id, + ); + break; + case SnippetType.solution: + descriptor = _getStandardOrEmptyDescriptor( + currentSdk, + _currentUnitContent!.solutionSnippetId, + ); + break; + } await playgroundController.examplesLoader.load( ExamplesLoadingDescriptor( descriptors: [ - EmptyExampleLoadingDescriptor(sdk: contentTreeController.sdk), + descriptor, ], ), ); + + _fillFeedbackController(); } - static PlaygroundController _createPlaygroundController(String initialSdkId) { - final exampleRepository = GetIt.instance.get(); - final codeRepository = GetIt.instance.get(); + void _fillFeedbackController() { + final controller = GetIt.instance.get(); + controller.eventSnippetContext = playgroundController.eventSnippetContext; + controller.additionalParams = _tobEventContext.toJson(); + } - final exampleCache = ExampleCache( - exampleRepository: exampleRepository, + ExampleLoadingDescriptor _getStandardOrEmptyDescriptor( + Sdk sdk, + String? snippetId, + ) { + if (snippetId == null) { + return EmptyExampleLoadingDescriptor( + sdk: currentSdk, + ); + } + return StandardExampleLoadingDescriptor( + path: snippetId, + sdk: sdk, + ); + } + + // TODO(alexeyinkin): Hide the entire right pane instead. + Future _emptyPlayground() async { + await playgroundController.examplesLoader.loadIfNew( + ExamplesLoadingDescriptor( + descriptors: [ + EmptyExampleLoadingDescriptor(sdk: contentTreeController.sdk), + ], + ), ); + } + + // Playground controller. + static PlaygroundController _createPlaygroundController(String initialSdkId) { final playgroundController = PlaygroundController( - codeRepository: codeRepository, - exampleCache: exampleCache, + codeRepository: GetIt.instance.get(), + exampleCache: ExampleCache( + exampleRepository: GetIt.instance.get(), + ), examplesLoader: ExamplesLoader(), ); unawaited( - playgroundController.examplesLoader.load( + playgroundController.examplesLoader.loadIfNew( ExamplesLoadingDescriptor( descriptors: [ EmptyExampleLoadingDescriptor(sdk: Sdk.parseOrCreate(initialSdkId)), @@ -238,7 +359,13 @@ class TourNotifier extends ChangeNotifier with PageStateMixin { _unitContentCache.removeListener(_onUnitChanged); contentTreeController.removeListener(_onUnitChanged); _appNotifier.removeListener(_onAppNotifierChanged); - _authNotifier.removeListener(_onUnitProgressChanged); + _authNotifier.removeListener(_onAuthChanged); + playgroundController.snippetEditingController + ?.removeListener(_onActiveFileControllerChanged); + // TODO(nausharipov): Use stream events https://github.com/apache/beam/issues/25185 + playgroundController + .snippetEditingController?.activeFileController?.codeController + .removeListener(_onCodeChanged); await super.dispose(); } } diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/binary_progress.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/binary_progress.dart index f562f12182523..db0b174e5daa4 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/binary_progress.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/binary_progress.dart @@ -50,7 +50,10 @@ class BinaryProgressIndicator extends StatelessWidget { ), child: SvgPicture.asset( isCompleted ? Assets.svg.unitProgress100 : Assets.svg.unitProgress0, - color: color, + colorFilter: ColorFilter.mode( + color, + BlendMode.srcIn, + ), ), ); } diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/complete_unit_button.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/complete_unit_button.dart index f29c04a56c5ae..8b13d993e0957 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/complete_unit_button.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/complete_unit_button.dart @@ -28,6 +28,17 @@ class CompleteUnitButton extends StatelessWidget { final TourNotifier tourNotifier; const CompleteUnitButton(this.tourNotifier); + Future _onPressed() async { + final unitId = tourNotifier.currentUnitId; + if (unitId == null) { + return; + } + await GetIt.instance.get().completeUnit( + tourNotifier.currentSdk.id, + unitId, + ); + } + @override Widget build(BuildContext context) { final themeData = Theme.of(context); @@ -40,9 +51,7 @@ class CompleteUnitButton extends StatelessWidget { unitProgressCache.canCompleteUnit(tourNotifier.currentUnitId); final borderColor = canComplete ? themeData.primaryColor : themeData.disabledColor; - final onPressed = canComplete - ? tourNotifier.currentUnitController?.completeUnit - : null; + final onPressed = canComplete ? _onPressed : null; return Flexible( child: OutlinedButton( diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/content_tree.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/content_tree.dart index a40f14d35c60d..caee4c9d9f3df 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/content_tree.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/content_tree.dart @@ -34,28 +34,28 @@ class ContentTreeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + return SizedBox( width: 250, - padding: const EdgeInsets.symmetric(horizontal: BeamSizes.size12), child: ContentTreeBuilder( - sdkId: controller.sdkId, + sdk: controller.sdk, builder: (context, contentTree, child) { if (contentTree == null) { return Container(); } return SingleChildScrollView( + padding: const EdgeInsets.symmetric( + horizontal: BeamSizes.size12, + ), child: Column( children: [ const ContentTreeTitleWidget(), - ...contentTree.modules - .map( - (module) => ModuleWidget( - module: module, - contentTreeController: controller, - ), - ) - .toList(growable: false), + ...contentTree.modules.map( + (module) => ModuleWidget( + module: module, + contentTreeController: controller, + ), + ), const SizedBox(height: BeamSizes.size12), ], ), diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/group.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/group.dart index fad732b105bb4..a3937e27d7a6e 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/group.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/group.dart @@ -52,7 +52,7 @@ class GroupWidget extends StatelessWidget { title: GroupTitleWidget( group: group, onTap: () { - contentTreeController.openNode(group); + contentTreeController.onNodePressed(group); }, ), child: GroupNodesWidget( diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/hints.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/hints.dart index 4ae39f99e5dff..424ce969d96fe 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/hints.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/hints.dart @@ -22,7 +22,6 @@ import 'package:flutter_svg/svg.dart'; import 'package:playground_components/playground_components.dart'; import '../../../assets/assets.gen.dart'; -import '../../../constants/sizes.dart'; import 'markdown/tob_markdown.dart'; class HintsWidget extends StatelessWidget { @@ -47,7 +46,7 @@ class HintsWidget extends StatelessWidget { } }, icon: SvgPicture.asset(Assets.svg.hint), - label: const Text('ui.hint').tr(), + label: const Text('pages.tour.hint').tr(), ); } } @@ -63,22 +62,24 @@ class _Popup extends StatelessWidget { Widget build(BuildContext context) { return OverlayBody( child: Container( - width: TobSizes.hintPopupWidth, + width: BeamSizes.popupWidth, padding: const EdgeInsets.all(BeamSizes.size16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'ui.hint', - style: Theme.of(context).textTheme.headlineLarge, - ).tr(), - const SizedBox(height: BeamSizes.size8), - TobMarkdown( - padding: EdgeInsets.zero, - data: hint, - ), - ], + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'pages.tour.hint', + style: Theme.of(context).textTheme.headlineLarge, + ).tr(), + const SizedBox(height: BeamSizes.size8), + TobMarkdown( + padding: EdgeInsets.zero, + data: hint, + ), + ], + ), ), ), ); diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/markdown/code_builder.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/markdown/code_builder.dart index c00bfc3f830fc..2eaabc10a506c 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/markdown/code_builder.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/markdown/code_builder.dart @@ -27,13 +27,36 @@ class MarkdownCodeBuilder extends MarkdownElementBuilder { final String textContent = element.textContent; final bool isCodeBlock = textContent.contains('\n'); if (isCodeBlock) { - /// codeblockDecoration is applied - return null; + return _CodeBlock(text: textContent); } return _InlineCode(text: textContent); } } +class _CodeBlock extends StatelessWidget { + final String text; + const _CodeBlock({required this.text}); + + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + return Padding( + padding: const EdgeInsets.all(BeamSizes.size4), + child: Scrollbar( + controller: scrollController, + scrollbarOrientation: ScrollbarOrientation.bottom, + thumbVisibility: true, + child: SingleChildScrollView( + controller: scrollController, + padding: const EdgeInsets.all(BeamSizes.size10), + scrollDirection: Axis.horizontal, + child: Text(text), + ), + ), + ); + } +} + class _InlineCode extends StatelessWidget { final String text; const _InlineCode({required this.text}); diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/markdown/tob_markdown.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/markdown/tob_markdown.dart index 0c8e741849981..dcbf11d8efa29 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/markdown/tob_markdown.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/markdown/tob_markdown.dart @@ -19,6 +19,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:playground_components/playground_components.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'code_builder.dart'; @@ -42,6 +43,11 @@ class TobMarkdown extends StatelessWidget { builders: { 'code': MarkdownCodeBuilder(), }, + onTapLink: (text, url, title) async { + if (url != null) { + await launchUrl(Uri.parse(url)); + } + }, padding: padding, selectable: true, shrinkWrap: shrinkWrap, diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/module.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/module.dart index b01987bf0a7ca..383fa54925ad1 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/module.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/module.dart @@ -39,7 +39,7 @@ class ModuleWidget extends StatelessWidget { children: [ ModuleTitleWidget( module: module, - onTap: () => contentTreeController.openNode(module), + onTap: () => contentTreeController.onNodePressed(module), ), ...module.nodes .map( diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit.dart index b9ca640f7d930..c8ee6627a2528 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit.dart @@ -44,7 +44,7 @@ class UnitWidget extends StatelessWidget { final isSelected = contentTreeController.currentNode?.id == unit.id; return ClickableWidget( - onTap: () => contentTreeController.openNode(unit), + onTap: () => contentTreeController.onNodePressed(unit), child: Container( decoration: BoxDecoration( color: isSelected ? Theme.of(context).selectedRowColor : null, diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit_content.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit_content.dart index 314c01aa6532d..3b47937a8d437 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit_content.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit_content.dart @@ -16,16 +16,19 @@ * limitations under the License. */ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; + import 'package:playground_components/playground_components.dart'; import '../../../constants/sizes.dart'; +import '../../../enums/save_code_status.dart'; +import '../../../enums/snippet_type.dart'; import '../../../models/unit_content.dart'; import '../state.dart'; import 'complete_unit_button.dart'; import 'hints.dart'; import 'markdown/tob_markdown.dart'; -import 'solution_button.dart'; class UnitContentWidget extends StatelessWidget { final TourNotifier tourNotifier; @@ -82,23 +85,25 @@ class _Content extends StatelessWidget { @override Widget build(BuildContext context) { final content = unitContent; - if (content == null) { return Container(); } - if (content.isChallenge) { - return _ChallengeContent( - tourNotifier: tourNotifier, - unitContent: content, - ); - } + return ListView( children: [ - _Title(title: content.title), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Title(title: content.title), + _Buttons( + unitContent: content, + tourNotifier: tourNotifier, + ), + ], + ), TobMarkdown( - padding: const EdgeInsets.all( - BeamSizes.size12, - ), + padding: const EdgeInsets.all(BeamSizes.size12), data: content.description, ), ], @@ -130,74 +135,135 @@ class _Title extends StatelessWidget { } } -class _ChallengeContent extends StatelessWidget { +class _Buttons extends StatelessWidget { final TourNotifier tourNotifier; final UnitContentModel unitContent; - const _ChallengeContent({ - required this.unitContent, + const _Buttons({ required this.tourNotifier, + required this.unitContent, }); @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _Title(title: unitContent.title), - _ChallengeButtons( - unitContent: unitContent, - tourNotifier: tourNotifier, + final hints = unitContent.hints; + + return Padding( + padding: const EdgeInsets.all(BeamSizes.size10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (hints.isNotEmpty) + HintsWidget( + hints: hints, ), - ], - ), - TobMarkdown( - padding: const EdgeInsets.all(BeamSizes.size12), - data: unitContent.description, - ), - ], + _SnippetTypeSwitcher( + tourNotifier: tourNotifier, + unitContent: unitContent, + ), + ], + ), ); } } -class _ChallengeButtons extends StatelessWidget { +class _SnippetTypeSwitcher extends StatelessWidget { final TourNotifier tourNotifier; final UnitContentModel unitContent; - const _ChallengeButtons({ + const _SnippetTypeSwitcher({ required this.tourNotifier, required this.unitContent, }); - static const _buttonPadding = EdgeInsets.only( - top: BeamSizes.size10, - right: BeamSizes.size10, - ); + Future _setSnippetByType(SnippetType snippetType) async { + await tourNotifier.showSnippetByType(snippetType); + } @override Widget build(BuildContext context) { - final hints = unitContent.hints; + return AnimatedBuilder( + animation: tourNotifier, + builder: (context, child) { + final groupValue = tourNotifier.snippetType; - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (unitContent.isChallenge) - Padding( - padding: _buttonPadding, - child: HintsWidget( - hints: hints, - ), - ), - if (tourNotifier.doesCurrentUnitHaveSolution) - Padding( - padding: _buttonPadding, - child: SolutionButton(tourNotifier: tourNotifier), - ), - ], + return Row( + children: [ + if (tourNotifier.hasSolution) + _SnippetTypeButton( + groupValue: groupValue, + title: 'pages.tour.solution'.tr(), + value: SnippetType.solution, + onChanged: () async { + await _setSnippetByType(SnippetType.solution); + }, + ), + if (tourNotifier.hasSolution || tourNotifier.isCodeSaved) + _SnippetTypeButton( + groupValue: groupValue, + title: unitContent.isChallenge + ? 'pages.tour.assignment'.tr() + : 'pages.tour.example'.tr(), + value: SnippetType.original, + onChanged: () async { + await _setSnippetByType(SnippetType.original); + }, + ), + if (tourNotifier.isCodeSaved) + _SnippetTypeButton( + groupValue: groupValue, + title: tourNotifier.saveCodeStatus == SaveCodeStatus.saving + ? 'pages.tour.saving'.tr() + : 'pages.tour.myCode'.tr(), + value: SnippetType.saved, + onChanged: () async { + await _setSnippetByType(SnippetType.saved); + }, + ), + ], + ); + }, + ); + } +} + +class _SnippetTypeButton extends StatelessWidget { + final SnippetType groupValue; + final VoidCallback onChanged; + final String title; + final SnippetType value; + + const _SnippetTypeButton({ + required this.groupValue, + required this.onChanged, + required this.value, + required this.title, + }); + + @override + Widget build(BuildContext context) { + final isSelected = value == groupValue; + final Color? bgColor; + final Color? fgColor; + final VoidCallback? onPressed; + if (isSelected) { + bgColor = Theme.of(context).splashColor; + fgColor = Theme.of(context).colorScheme.onSurface; + onPressed = null; + } else { + bgColor = null; + fgColor = null; + onPressed = onChanged; + } + + return TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(bgColor), + foregroundColor: MaterialStateProperty.all(fgColor), + ), + onPressed: onPressed, + child: Text(title), ); } } diff --git a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart index 3e9341a6ff5f5..4b6b5eb5e4b17 100644 --- a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart +++ b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart @@ -136,10 +136,10 @@ class _SdkSelection extends StatelessWidget { animation: appNotifier, builder: (context, child) => _SdkButtons( sdks: sdks, - sdkId: appNotifier.sdkId, - setSdkId: (v) => appNotifier.sdkId = v, - onStartPressed: () { - _startTour(appNotifier.sdkId); + groupValue: appNotifier.sdk, + onChanged: (v) => appNotifier.sdk = v, + onStartPressed: () async { + await _startTour(appNotifier.sdk); }, ), ); @@ -153,11 +153,11 @@ class _SdkSelection extends StatelessWidget { ); } - void _startTour(String? sdkId) { - if (sdkId == null) { + Future _startTour(Sdk? sdk) async { + if (sdk == null) { return; } - GetIt.instance.get().push(TourPage(sdkId: sdkId)); + await GetIt.instance.get().push(TourPage(sdk: sdk)); } } @@ -170,8 +170,8 @@ class _TourSummary extends StatelessWidget { return AnimatedBuilder( animation: appNotifier, builder: (context, child) { - final sdkId = appNotifier.sdkId; - if (sdkId == null) { + final sdk = appNotifier.sdk; + if (sdk == null) { return Container(); } @@ -181,7 +181,7 @@ class _TourSummary extends StatelessWidget { horizontal: 27, ), child: ContentTreeBuilder( - sdkId: sdkId, + sdk: sdk, builder: (context, contentTree, child) { if (contentTree == null) { return Container(); @@ -285,14 +285,14 @@ class _IntroTextBody extends StatelessWidget { class _SdkButtons extends StatelessWidget { final List sdks; - final String? sdkId; - final ValueChanged setSdkId; + final Sdk? groupValue; + final ValueChanged onChanged; final VoidCallback onStartPressed; const _SdkButtons({ required this.sdks, - required this.sdkId, - required this.setSdkId, + required this.groupValue, + required this.onChanged, required this.onStartPressed, }); @@ -305,15 +305,15 @@ class _SdkButtons extends StatelessWidget { .map( (sdk) => _SdkButton( title: sdk.title, - value: sdk.id, - groupValue: sdkId, - onChanged: setSdkId, + value: sdk, + groupValue: groupValue, + onChanged: onChanged, ), ) .toList(growable: false), ), ElevatedButton( - onPressed: sdkId == null ? null : onStartPressed, + onPressed: groupValue == null ? null : onStartPressed, child: const Text('pages.welcome.startTour').tr(), ), ], @@ -323,9 +323,9 @@ class _SdkButtons extends StatelessWidget { class _SdkButton extends StatelessWidget { final String title; - final String value; - final String? groupValue; - final ValueChanged onChanged; + final Sdk value; + final Sdk? groupValue; + final ValueChanged onChanged; const _SdkButton({ required this.title, @@ -391,7 +391,10 @@ class _ModuleHeader extends StatelessWidget { padding: const EdgeInsets.all(BeamSizes.size4), child: SvgPicture.asset( Assets.svg.welcomeProgress0, - color: BeamColors.grey4, + colorFilter: const ColorFilter.mode( + BeamColors.grey4, + BlendMode.srcIn, + ), ), ), const SizedBox(width: BeamSizes.size16), diff --git a/learning/tour-of-beam/frontend/lib/pages/welcome/state.dart b/learning/tour-of-beam/frontend/lib/pages/welcome/state.dart index 0b381a9957432..b743490a59caf 100644 --- a/learning/tour-of-beam/frontend/lib/pages/welcome/state.dart +++ b/learning/tour-of-beam/frontend/lib/pages/welcome/state.dart @@ -21,7 +21,6 @@ import 'package:flutter/widgets.dart'; import 'path.dart'; class WelcomeNotifier extends ChangeNotifier with PageStateMixin { - // TODO(nausharipov): remove state from Welcome? @override PagePath get path => const WelcomePath(); } diff --git a/learning/tour-of-beam/frontend/lib/repositories/client/client.dart b/learning/tour-of-beam/frontend/lib/repositories/client/client.dart index 66fd4a9963169..bbcb217c9c083 100644 --- a/learning/tour-of-beam/frontend/lib/repositories/client/client.dart +++ b/learning/tour-of-beam/frontend/lib/repositories/client/client.dart @@ -16,6 +16,8 @@ * limitations under the License. */ +import 'package:playground_components/playground_components.dart'; + import '../../models/content_tree.dart'; import '../../models/unit_content.dart'; import '../models/get_sdks_response.dart'; @@ -31,4 +33,12 @@ abstract class TobClient { Future getUserProgress(String sdkId); Future postUnitComplete(String sdkId, String id); + + Future postDeleteUserProgress(); + + Future postUserCode({ + required List snippetFiles, + required String sdkId, + required String unitId, + }); } diff --git a/learning/tour-of-beam/frontend/lib/repositories/client/cloud_functions_client.dart b/learning/tour-of-beam/frontend/lib/repositories/client/cloud_functions_client.dart index 8986de4352905..9be881abcedd9 100644 --- a/learning/tour-of-beam/frontend/lib/repositories/client/cloud_functions_client.dart +++ b/learning/tour-of-beam/frontend/lib/repositories/client/cloud_functions_client.dart @@ -21,6 +21,7 @@ import 'dart:io'; import 'package:get_it/get_it.dart'; import 'package:http/http.dart' as http; +import 'package:playground_components/playground_components.dart'; import '../../auth/notifier.dart'; import '../../config.dart'; @@ -31,42 +32,76 @@ import '../models/get_sdks_response.dart'; import '../models/get_user_progress_response.dart'; import 'client.dart'; -// TODO(nausharipov): add repository and handle exceptions +enum RequestMethod { + post, + get, +} + class CloudFunctionsTobClient extends TobClient { + Future _makeRequest({ + required String path, + required RequestMethod method, + Map queryParameters = const {}, + dynamic body, + }) async { + final token = await GetIt.instance.get().getToken(); + final uri = Uri.parse('$cloudFunctionsBaseUrl$path') + .replace(queryParameters: queryParameters); + final headers = token != null + ? {HttpHeaders.authorizationHeader: 'Bearer $token'} + : null; + + http.Response response; + switch (method) { + case RequestMethod.post: + response = await http.post( + uri, + headers: headers, + body: body, + ); + break; + case RequestMethod.get: + response = await http.get( + uri, + headers: headers, + ); + break; + } + return jsonDecode(utf8.decode(response.bodyBytes)); + } + @override Future getSdks() async { - final json = await http.get( - Uri.parse( - '$cloudFunctionsBaseUrl/getSdkList', - ), + final map = await _makeRequest( + method: RequestMethod.get, + path: 'getSdkList', ); - - final map = jsonDecode(utf8.decode(json.bodyBytes)) as Map; return GetSdksResponse.fromJson(map); } @override Future getContentTree(String sdkId) async { - final json = await http.get( - Uri.parse( - '$cloudFunctionsBaseUrl/getContentTree?sdk=$sdkId', - ), + final map = await _makeRequest( + method: RequestMethod.get, + path: 'getContentTree', + queryParameters: { + 'sdk': sdkId, + }, ); - - final map = jsonDecode(utf8.decode(json.bodyBytes)) as Map; final response = GetContentTreeResponse.fromJson(map); return ContentTreeModel.fromResponse(response); } @override Future getUnitContent(String sdkId, String unitId) async { - final json = await http.get( - Uri.parse( - '$cloudFunctionsBaseUrl/getUnitContent?sdk=$sdkId&id=$unitId', - ), + final map = await _makeRequest( + method: RequestMethod.get, + path: 'getUnitContent', + queryParameters: { + 'sdk': sdkId, + 'id': unitId, + }, ); - - final map = jsonDecode(utf8.decode(json.bodyBytes)) as Map; return UnitContentModel.fromJson(map); } @@ -76,29 +111,54 @@ class CloudFunctionsTobClient extends TobClient { if (token == null) { return null; } - final json = await http.get( - Uri.parse( - '$cloudFunctionsBaseUrl/getUserProgress?sdk=$sdkId', - ), - headers: { - HttpHeaders.authorizationHeader: 'Bearer $token', + final map = await _makeRequest( + method: RequestMethod.get, + path: 'getUserProgress', + queryParameters: { + 'sdk': sdkId, }, ); - final map = jsonDecode(utf8.decode(json.bodyBytes)) as Map; final response = GetUserProgressResponse.fromJson(map); return response; } @override Future postUnitComplete(String sdkId, String id) async { - final token = await GetIt.instance.get().getToken(); - await http.post( - Uri.parse( - '$cloudFunctionsBaseUrl/postUnitComplete?sdk=$sdkId&id=$id', - ), - headers: { - HttpHeaders.authorizationHeader: 'Bearer $token', + await _makeRequest( + method: RequestMethod.post, + path: 'postUnitComplete', + queryParameters: { + 'sdk': sdkId, + 'id': id, + }, + ); + } + + @override + Future postDeleteUserProgress() async { + await _makeRequest( + method: RequestMethod.post, + path: 'postDeleteProgress', + ); + } + + @override + Future postUserCode({ + required List snippetFiles, + required String sdkId, + required String unitId, + }) async { + await _makeRequest( + path: 'postUserCode', + method: RequestMethod.post, + queryParameters: { + 'sdk': sdkId, + 'id': unitId, }, + body: jsonEncode({ + 'files': snippetFiles.map((file) => file.toJson()).toList(), + 'pipelineOptions': '', + }), ); } } diff --git a/learning/tour-of-beam/frontend/lib/repositories/user_progress/abstract.dart b/learning/tour-of-beam/frontend/lib/repositories/user_progress/abstract.dart new file mode 100644 index 0000000000000..9d5aa33114122 --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/repositories/user_progress/abstract.dart @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:playground_components/playground_components.dart'; + +import '../../enums/snippet_type.dart'; +import '../models/get_user_progress_response.dart'; + +abstract class AbstractUserProgressRepository { + Future getUserProgress( + Sdk sdk, + ); + + Future completeUnit( + String sdkId, + String unitId, + ); + + Future saveUnitSnippet({ + required Sdk sdk, + required List snippetFiles, + required SnippetType snippetType, + required String unitId, + }); + + Future getSavedDescriptor({ + required Sdk sdk, + required String unitId, + }); + + Future deleteUserProgress(); +} diff --git a/learning/tour-of-beam/frontend/lib/repositories/user_progress/cloud.dart b/learning/tour-of-beam/frontend/lib/repositories/user_progress/cloud.dart new file mode 100644 index 0000000000000..f4722aac00eac --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/repositories/user_progress/cloud.dart @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:collection/collection.dart'; +import 'package:playground_components/playground_components.dart'; + +import '../../enums/snippet_type.dart'; +import '../client/client.dart'; +import '../models/get_user_progress_response.dart'; +import 'abstract.dart'; + +class CloudUserProgressRepository extends AbstractUserProgressRepository { + CloudUserProgressRepository({ + required this.client, + }); + final TobClient client; + + @override + Future completeUnit(String sdkId, String unitId) async { + await client.postUnitComplete(sdkId, unitId); + } + + @override + Future getSavedDescriptor({ + required Sdk sdk, + required String unitId, + }) async { + final userProgressResponse = await getUserProgress(sdk); + final unitProgress = userProgressResponse?.units.firstWhereOrNull( + (unit) => unit.id == unitId, + ); + final userSnippetId = unitProgress?.userSnippetId; + if (userSnippetId == null) { + return EmptyExampleLoadingDescriptor(sdk: sdk); + } + return UserSharedExampleLoadingDescriptor( + sdk: sdk, + snippetId: userSnippetId, + ); + } + + @override + Future getUserProgress(Sdk sdk) async { + return client.getUserProgress(sdk.id); + } + + @override + Future saveUnitSnippet({ + required Sdk sdk, + required List snippetFiles, + required String unitId, + required SnippetType? snippetType, + }) async { + await client.postUserCode( + snippetFiles: snippetFiles, + sdkId: sdk.id, + unitId: unitId, + ); + } + + @override + Future deleteUserProgress() async { + await client.postDeleteUserProgress(); + } +} diff --git a/learning/tour-of-beam/frontend/lib/repositories/user_progress/hive.dart b/learning/tour-of-beam/frontend/lib/repositories/user_progress/hive.dart new file mode 100644 index 0000000000000..65be2acab5832 --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/repositories/user_progress/hive.dart @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'dart:convert'; + +import 'package:get_it/get_it.dart'; +import 'package:hive/hive.dart'; +import 'package:playground_components/playground_components.dart'; + +import '../../cache/sdk.dart'; +import '../../constants/hive_box_names.dart'; +import '../../enums/snippet_type.dart'; +import '../../models/unit_progress.dart'; +import '../models/get_user_progress_response.dart'; +import 'abstract.dart'; + +class HiveUserProgressRepository extends AbstractUserProgressRepository { + @override + Future completeUnit(String sdkId, String unitId) { + throw UnimplementedError(); + } + + @override + Future getSavedDescriptor({ + required Sdk sdk, + required String unitId, + }) async { + try { + final unitProgressBox = await Hive.openBox( + HiveBoxNames.getSdkBoxName(sdk, HiveBoxNames.unitProgress), + ); + final unitProgress = UnitProgressModel.fromJson( + jsonDecode(unitProgressBox.get(unitId)), + ); + return HiveExampleLoadingDescriptor( + boxName: HiveBoxNames.getSdkBoxName(sdk, HiveBoxNames.snippets), + sdk: sdk, + snippetId: unitProgress.userSnippetId!, + ); + } on Exception { + return EmptyExampleLoadingDescriptor(sdk: sdk); + } + } + + @override + Future getUserProgress(Sdk sdk) async { + final sdkUnitProgressBox = await Hive.openBox( + HiveBoxNames.getSdkBoxName(sdk, HiveBoxNames.unitProgress), + ); + return GetUserProgressResponse.fromJson({ + // TODO(nausharipov): Replace lambda with tear-off when this lands: https://github.com/dart-lang/language/issues/1813 + 'units': sdkUnitProgressBox.values.map((e) => jsonDecode(e)).toList(), + }); + } + + @override + Future saveUnitSnippet({ + required Sdk sdk, + required List snippetFiles, + required SnippetType snippetType, + required String unitId, + }) async { + final snippetsBox = await Hive.openBox( + HiveBoxNames.getSdkBoxName(sdk, HiveBoxNames.snippets), + ); + final snippetId = 'local_$unitId'; + + await _saveUnitProgressIfUnsaved( + sdk: sdk, + unitId: unitId, + userSnippetId: snippetId, + ); + + await snippetsBox.put( + snippetId, + jsonEncode( + Example( + files: snippetFiles, + name: 'name', + sdk: sdk, + type: ExampleType.example, + path: 'path', + ).toJson(), + ), + ); + } + + Future _saveUnitProgressIfUnsaved({ + required Sdk sdk, + required String unitId, + required String userSnippetId, + }) async { + final unitProgressBox = await Hive.openBox( + HiveBoxNames.getSdkBoxName(sdk, HiveBoxNames.unitProgress), + ); + final unitProgressEncoded = unitProgressBox.get(unitId); + if (unitProgressEncoded == null) { + await unitProgressBox.put( + unitId, + jsonEncode( + UnitProgressModel( + id: unitId, + isCompleted: false, + userSnippetId: userSnippetId, + ).toJson(), + ), + ); + } + } + + @override + Future deleteUserProgress() async { + final sdks = GetIt.instance.get().getSdks(); + for (final sdk in sdks) { + final unitProgress = await Hive.openBox( + HiveBoxNames.getSdkBoxName(sdk, HiveBoxNames.unitProgress), + ); + final snippetsBox = await Hive.openBox( + HiveBoxNames.getSdkBoxName(sdk, HiveBoxNames.snippets), + ); + await unitProgress.clear(); + await snippetsBox.clear(); + } + } +} diff --git a/learning/tour-of-beam/frontend/lib/state.dart b/learning/tour-of-beam/frontend/lib/state.dart index c67b037d8d92b..e690e398faeae 100644 --- a/learning/tour-of-beam/frontend/lib/state.dart +++ b/learning/tour-of-beam/frontend/lib/state.dart @@ -25,34 +25,32 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'constants/storage_keys.dart'; class AppNotifier extends ChangeNotifier { - String? _sdkId; + Sdk? _sdk; AppNotifier() { - unawaited(_readSdkId()); + unawaited(_readSdk()); } - // TODO(nausharipov): remove sdkId getter and setter - String? get sdkId => _sdkId; - Sdk? get sdk => Sdk.tryParse(_sdkId); + Sdk? get sdk => _sdk; - set sdkId(String? newValue) { - _sdkId = newValue; - unawaited(_writeSdkId(newValue)); + set sdk(Sdk? newValue) { + _sdk = newValue; + unawaited(_writeSdk(newValue)); notifyListeners(); } - Future _writeSdkId(String? value) async { + Future _writeSdk(Sdk? value) async { final preferences = await SharedPreferences.getInstance(); if (value != null) { - await preferences.setString(StorageKeys.sdkId, value); + await preferences.setString(StorageKeys.sdkId, value.id); } else { await preferences.remove(StorageKeys.sdkId); } } - Future _readSdkId() async { + Future _readSdk() async { final preferences = await SharedPreferences.getInstance(); - _sdkId = preferences.getString(StorageKeys.sdkId); + _sdk = Sdk.tryParse(preferences.getString(StorageKeys.sdkId)); notifyListeners(); } } diff --git a/learning/tour-of-beam/frontend/pubspec.lock b/learning/tour-of-beam/frontend/pubspec.lock index acda06e0dc175..8f638918ce4fb 100644 --- a/learning/tour-of-beam/frontend/pubspec.lock +++ b/learning/tour-of-beam/frontend/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "3ff770dfff04a67b0863dff205a0936784de1b87a5e99b11c693fc10e66a9ce3" + sha256: "64fcb0dbca4386356386c085142fa6e79c00a3326ceaa778a2d25f5d9ba61441" url: "https://pub.dev" source: hosted - version: "1.0.12" + version: "1.0.16" aligned_dialog: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: args - sha256: b003c3098049a51720352d219b0bb5f219b60fbfb68e7a4748139a06a5676515 + sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" async: dependency: transitive description: @@ -85,18 +85,18 @@ packages: dependency: transitive description: name: build - sha256: "29a03af98de60b4eb9136acd56608a54e989f6da238a80af739415b05589d6df" + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" build_config: dependency: transitive description: name: build_config - sha256: "5b7355c14258f5e7df24bad1566f7b991de3e54aeacfb94e1a65e5233d9739c1" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" build_daemon: dependency: transitive description: @@ -109,26 +109,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: "9aae031a54ab0beebc30a888c93e900d15ae2fd8883d031dbfbd5ebdb57f5a4c" + sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.10" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "56942f8114731d1e79942cd981cfef29501937ff1bccf4dbdce0273f31f13640" + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.3" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f4d6244cc071ba842c296cb1c4ee1b31596b9f924300647ac7a1445493471a3f + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" url: "https://pub.dev" source: hosted - version: "7.2.3" + version: "7.2.7" built_collection: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: built_value - sha256: d7a9cd57c215bdf8d502772447aa6b52a8ab3f956d25d5fdea6ef1df2d2dad60 + sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" url: "https://pub.dev" source: hosted - version: "8.4.1" + version: "8.4.3" characters: dependency: transitive description: @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" clock: dependency: transitive description: @@ -181,10 +181,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "43743b95913fd28b95184eb1bed7e4bd85b802b8fad0a52522702dbeda4ee3d5" + sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.4.0" collection: dependency: "direct main" description: @@ -253,10 +253,10 @@ packages: dependency: transitive description: name: convert - sha256: "196284f26f69444b7f5c50692b55ec25da86d9e500451dc09333bf2e3ad69259" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.1.1" coverage: dependency: transitive description: @@ -285,10 +285,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "8aff82f9b26fd868992e5430335a9d773bfef01e1d852d7ba71bf4c5d9349351" + sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.4" dartx: dependency: transitive description: @@ -338,13 +338,21 @@ packages: source: hosted version: "0.0.2" enum_map: - dependency: transitive + dependency: "direct main" description: name: enum_map sha256: "0dfe18306d2e9b0e9d381f5e11aac4c8255d5f5eddc68b0ab037f7d00aa36126" url: "https://pub.dev" source: hosted version: "0.2.1" + enum_map_gen: + dependency: "direct dev" + description: + name: enum_map_gen + sha256: "5c99bdd426f4ea457ad6e928eeb44111224881791025cf8afa1921e7a55cf232" + url: "https://pub.dev" + source: hosted + version: "0.2.0" equatable: dependency: "direct main" description: @@ -381,58 +389,58 @@ packages: dependency: "direct main" description: name: firebase_auth - sha256: "721b90fe1a0966add31b47a490672954ac4fe45cfe721fd8a11ffbf4c166f611" + sha256: "9907d80446466e638dad31c195150b305dffd145dc57610fcd12c72289432143" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "4.2.9" firebase_auth_platform_interface: dependency: "direct main" description: name: firebase_auth_platform_interface - sha256: "325d934e21826b3e7030f5018ef61927e2083b4c4fb25218ddef6ffc0012b717" + sha256: c645fec50b0391aa878288f58fa4fe9762c271380c457aedf5c7c9b718604f68 url: "https://pub.dev" source: hosted - version: "6.11.7" + version: "6.11.11" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: "1a7fe4aafed9b29229aa1de6910e0631be94633b4a235e739cc2830a0f110361" + sha256: "2dcf2a36852b9091741b4a4047a02e1f2c43a62c6cacec7df573a793a6543e6d" url: "https://pub.dev" source: hosted - version: "5.1.3" + version: "5.2.8" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: c129209ba55f3d4272c89fb4a4994c15bea77fb6de63a82d45fb6bc5c94e4355 + sha256: fe30ac230f12f8836bb97e6e09197340d3c584526825b1746ea362a82e1e43f7 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.7.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "5fab93f5b354648efa62e7cc829c90efb68c8796eecf87e0888cae2d5f3accd4" + sha256: "5615b30c36f55b2777d0533771deda7e5730e769e5d3cb7fda79e9bed86cfa55" url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.3" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "18b35ce111b0a4266abf723c825bcf9d4e2519d13638cc7f06f2a8dd960c75bc" + sha256: "291fbcace608aca6c860652e1358ef89752be8cc3ef227f8bbcd1e62775b833a" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.1" fixnum: dependency: transitive description: name: fixnum - sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -442,10 +450,10 @@ packages: dependency: transitive description: name: flutter_code_editor - sha256: a220a7dcd197d793f46f2c2132551128bb41b1ccc08f7afb781223e1b5e400a1 + sha256: "73313c8235b242102af1935312933134774f62c7ed8ad8297beedb88340cc7e1" url: "https://pub.dev" source: hosted - version: "0.2.12" + version: "0.2.18" flutter_driver: dependency: transitive description: flutter @@ -518,18 +526,18 @@ packages: dependency: transitive description: name: fluttertoast - sha256: "7a738eddad04c7b27a1ecfecd12e8ecd4b188cdd2d91c252a02a4aba65838c9d" + sha256: "2f9c4d3f4836421f7067a28f8939814597b27614e021da9d63e5d3fb6e212d25" url: "https://pub.dev" source: hosted - version: "8.1.1" + version: "8.2.1" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "4f4a162323c86ffc1245765cfe138872b8f069deb42f7dbb36115fa27f31469b" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "3.2.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -547,10 +555,10 @@ packages: dependency: transitive description: name: glob - sha256: c51b4fdfee4d281f49b8c957f1add91b815473597f76bcf07377987f66a55729 + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" google_fonts: dependency: "direct main" description: @@ -559,54 +567,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.3" - google_identity_services_web: - dependency: transitive - description: - name: google_identity_services_web - sha256: "5d9af2f1fa192f2629a266d038ee9307b0abe729a4f1b454dd21b414f5e7d381" - url: "https://pub.dev" - source: hosted - version: "0.2.0" - google_sign_in: - dependency: "direct main" - description: - name: google_sign_in - sha256: "4f7177a6116738b0c54230a864f1d44d5d2bbec3e43b4d00c16735e32bb8e8da" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - google_sign_in_android: - dependency: transitive - description: - name: google_sign_in_android - sha256: "41187ee48f8f3f7588cb932a5ab3cc8c83f354d1d50c750f61b240efac1b33d2" - url: "https://pub.dev" - source: hosted - version: "6.1.4" - google_sign_in_ios: - dependency: transitive - description: - name: google_sign_in_ios - sha256: "1116aff5e87f89837b052a81abe6259be7c4dd418275786864d27b74cb2a4e70" - url: "https://pub.dev" - source: hosted - version: "5.5.1" - google_sign_in_platform_interface: - dependency: transitive - description: - name: google_sign_in_platform_interface - sha256: "61306213c76bb8170c3aa20017df296c0131c24d7f6c0cc7e2eeaeac34c9f457" - url: "https://pub.dev" - source: hosted - version: "2.3.0" - google_sign_in_web: - dependency: transitive - description: - name: google_sign_in_web - sha256: a33778787257c348f1ec8f0ab51bc680af7dc224ad7a71fb5a5d49177dca3c49 - url: "https://pub.dev" - source: hosted - version: "0.11.0" googleapis_auth: dependency: transitive description: @@ -619,18 +579,18 @@ packages: dependency: transitive description: name: graphs - sha256: ae0b3d956ff324c6f8671f08dcb2dbd71c99cdbf2aa3ca63a14190c47aa6679c + sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" grpc: dependency: transitive description: name: grpc - sha256: "3e8e04c6277059b66d67951143842097e52bbf3f2c6fca2e67d3607b48d5c3ab" + sha256: a73c16e4f6a4a819be892bb2c73cc1d0b00e36095f69b0738cc91a733e3d27ba url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.1.0" highlight: dependency: transitive description: @@ -640,13 +600,21 @@ packages: source: hosted version: "0.7.0" hive: - dependency: transitive + dependency: "direct main" description: name: hive sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" url: "https://pub.dev" source: hosted version: "2.2.3" + hive_test: + dependency: transitive + description: + name: hive_test + sha256: dd7a5cf0be7af288566a96180b5d07574023777aa947ef252b69046ec36d8eb2 + url: "https://pub.dev" + source: hosted + version: "1.0.1" http: dependency: "direct main" description: @@ -659,10 +627,10 @@ packages: dependency: transitive description: name: http2 - sha256: feb9fbe4790be90fef454eb930368c40ae56df598b3e9b9c10cc216d68f75720 + sha256: "58805ebc6513eed3b98ee0a455a8357e61d187bf2e0fdc1e53120770f78de258" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" http_multi_server: dependency: transitive description: @@ -675,10 +643,10 @@ packages: dependency: transitive description: name: http_parser - sha256: db3060f22889f3d9d55f6a217565486737037eec3609f7f3eca4d0c67ee0d8a0 + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" integration_test: dependency: "direct dev" description: flutter @@ -696,10 +664,10 @@ packages: dependency: transitive description: name: io - sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: @@ -720,12 +688,12 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: "581006a34721ff9b9cbc2ba6aab4c81ee9a9f345e9f046f9feef5732417cfe4b" + sha256: f3c2c18a7889580f71926f30c1937727c8c7d4f3a435f8f5e8b0ddd25253ef5d url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.5.4" keyed_collection_widgets: - dependency: transitive + dependency: "direct main" description: name: keyed_collection_widgets sha256: "9db2df4c4897c35fe167bdca82d307d81baa4161c3118da3f06ab4fd2d75291b" @@ -744,10 +712,10 @@ packages: dependency: transitive description: name: logging - sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.1" markdown: dependency: "direct main" description: @@ -784,10 +752,10 @@ packages: dependency: transitive description: name: mime - sha256: dab22e92b41aa1255ea90ddc4bc2feaf35544fd0728e209638cad041a6e3928a + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.4" mocktail: dependency: transitive description: @@ -816,10 +784,10 @@ packages: dependency: transitive description: name: node_preamble - sha256: "8ebdbaa3b96d5285d068f80772390d27c21e1fa10fb2df6627b1b9415043608d" + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" os_detect: dependency: transitive description: @@ -856,58 +824,50 @@ packages: dependency: transitive description: name: path_provider - sha256: "050e8e85e4b7fecdf2bb3682c1c64c4887a183720c802d323de8a5fd76d372dd" + sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.0.12" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "833c8bcb182b515cd872c113e29aaaffd29a1c720259dd2f65ab35ed5e0db748" + sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e url: "https://pub.dev" source: hosted - version: "2.0.17" - path_provider_ios: + version: "2.0.22" + path_provider_foundation: dependency: transitive description: - name: path_provider_ios - sha256: "03d639406f5343478352433f00d3c4394d52dac8df3d847869c5e2333e0bbce8" + name: path_provider_foundation + sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.1.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 - url: "https://pub.dev" - source: hosted - version: "2.1.7" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - sha256: "2a97e7fbb7ae9dcd0dfc1220a78e9ec3e71da691912e617e8715ff2a13086ae8" + sha256: "2e32f1640f07caef0d3cb993680f181c79e54a3827b997d5ee221490d131fbd9" url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.1.8" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "27dc7a224fcd07444cb5e0e60423ccacea3e13cf00fc5282ac2c918132da931d" + sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.0.5" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "999d3dc2ac03ca3f8433018efa40b73558fa4f9759bf8383a217861d120c7d74" + sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" petitparser: dependency: transitive description: @@ -967,18 +927,18 @@ packages: dependency: "direct main" description: name: provider - sha256: "8d7d4c2df46d6a6270a4e10404bfecb18a937e3e00f710c260d0a10415ce6b7b" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "6.0.5" pub_semver: dependency: transitive description: name: pub_semver - sha256: "816c1a640e952d213ddd223b3e7aafae08cd9f8e1f6864eed304cc13b0272b07" + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.3" pubspec_parse: dependency: transitive description: @@ -987,14 +947,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" - quiver: - dependency: transitive + rate_limiter: + dependency: "direct main" description: - name: quiver - sha256: "93982981971e812c94d4a6fa3a57b89f9ec12b38b6380cd3c1370c3b01e4580e" + name: rate_limiter + sha256: "2bae2e961adedf7fc2e8b0305d30e3a3619baf001d050c6907870c5c6235b559" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "1.0.0" rxdart: dependency: transitive description: @@ -1015,50 +975,42 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "76917b7d4b9526b2ba416808a7eb9fb2863c1a09cf63ec85f1453da240fa818a" + sha256: "5949029e70abe87f75cfe59d17bf5c397619c4b74a099b10116baeb34786fad9" url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.17" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "853801ce6ba7429ec4e923e37317f32a57c903de50b8c33ffcfbdb7e6f0dd39c" + sha256: "955e9736a12ba776bdd261cf030232b30eadfcd9c79b32a3250dd4a494e8c8f7" url: "https://pub.dev" source: hosted - version: "2.0.12" - shared_preferences_ios: + version: "2.0.15" + shared_preferences_foundation: dependency: transitive description: - name: shared_preferences_ios - sha256: "585a14cefec7da8c9c2fb8cd283a3bb726b4155c0952afe6a0caaa7b2272de34" + name: shared_preferences_foundation + sha256: "2b55c18636a4edc529fa5cd44c03d3f3100c00513f518c5127c951978efcccd0" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.3" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "28aefc1261746e7bad3d09799496054beb84e8c4ffcdfed7734e17b4ada459a5" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - shared_preferences_macos: - dependency: transitive - description: - name: shared_preferences_macos - sha256: fbb94bf296576f49be37a1496d5951796211a8db0aa22cc0d68c46440dad808c + sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874 url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.1.3" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "992f0fdc46d0a3c0ac2e5859f2de0e577bbe51f78a77ee8f357cbe626a2ad32d" + sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" shared_preferences_web: dependency: transitive description: @@ -1071,18 +1023,18 @@ packages: dependency: transitive description: name: shared_preferences_windows - sha256: "97f7ab9a7da96d9cf19581f5de520ceb529548498bd6b5e0ccd02d68a0d15eba" + sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.3" shelf: dependency: transitive description: name: shelf - sha256: "8ec607599dd0a78931a5114cdac7d609b6dbbf479a38acc9a6dba024b2a30ea0" + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" shelf_packages_handler: dependency: transitive description: @@ -1103,10 +1055,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "6db16374bc3497d21aa0eebe674d3db9fdf82082aac0f04dc7b44e4af5b08afc" + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" sky_engine: dependency: transitive description: flutter @@ -1116,10 +1068,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "85f8c7d6425dff95475db618404732f034c87fe23efe05478cea50520a2517a3" + sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" url: "https://pub.dev" source: hosted - version: "1.2.5" + version: "1.2.6" source_helper: dependency: transitive description: @@ -1172,10 +1124,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -1228,26 +1180,26 @@ packages: dependency: transitive description: name: time - sha256: "267028bb7b3e87bbfd66876c6389d7101e4b14eb94fe863d3e008e497ca07844" + sha256: "83427e11d9072e038364a5e4da559e85869b227cf699a541be0da74f14140124" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" timing: dependency: transitive description: name: timing - sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" total_lints: dependency: "direct dev" description: name: total_lints - sha256: "8da9ee8d6a8e7c28e5e25bc6f35fb2102eaa7151044d7fabfa11c293ad8b4281" + sha256: "5424a55034e89a9c6198518356842dfdc33b6f0b4d557071f84d6095e5a9f8ea" url: "https://pub.dev" source: hosted - version: "2.17.4" + version: "2.19.0" tuple: dependency: transitive description: @@ -1276,58 +1228,58 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "1ccd353c1bff66b49863527c02759f4d06b92744bd9777c96a00ca6a9e8e1d2f" + sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1" url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.0.23" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "6ba7dddee26c9fae27c9203c424631109d73c8fa26cfa7bc3e35e751cb87f62e" + sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815" url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.1.0" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "360fa359ab06bcb4f7c5cd3123a2a9a4d3364d4575d27c4b33468bd4497dd094" + sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: a9b3ea9043eabfaadfa3fb89de67a11210d85569086d22b3854484beab8b3978 + sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "80b860b31a11ebbcbe51b8fe887efc204f3af91522f3b51bcda4622d276d2120" + sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "15fd9dbb306d5efce57dcf62dcb1ae045fbf74079ab4464a950e099bf5800deb" + sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0" url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.0.14" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: e3c3b16d3104260c10eea3b0e34272aaa57921f83148b0619f74c2eced9b7ef1 + sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" url_strategy: dependency: "direct main" description: @@ -1388,18 +1340,18 @@ packages: dependency: transitive description: name: watcher - sha256: e42dfcc48f67618344da967b10f62de57e04bae01d9d3af4c2596f3712a88c99 + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "3a969ddcc204a3e34e863d204b29c0752716f78b6f9cc8235083208d268a4ccd" + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" webdriver: dependency: transitive description: @@ -1420,18 +1372,18 @@ packages: dependency: transitive description: name: win32 - sha256: "6b75ac2ddd42f5c226fdaf4498a2b04071c06f1f2b8f7ab1c3f77cc7f2285ff1" + sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "3.1.3" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "060b6e1c891d956f72b5ac9463466c37cce3fa962a921532fc001e86fe93438e" + sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 url: "https://pub.dev" source: hosted - version: "0.2.0+1" + version: "1.0.0" xml: dependency: transitive description: diff --git a/learning/tour-of-beam/frontend/pubspec.yaml b/learning/tour-of-beam/frontend/pubspec.yaml index 0e580d5005266..edaec367456c7 100644 --- a/learning/tour-of-beam/frontend/pubspec.yaml +++ b/learning/tour-of-beam/frontend/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: easy_localization: ^3.0.1 easy_localization_ext: ^0.1.0 easy_localization_loader: ^1.0.0 + enum_map: ^0.2.1 equatable: ^2.0.5 firebase_auth: ^4.1.1 firebase_auth_platform_interface: ^6.11.7 @@ -41,18 +42,21 @@ dependencies: flutter_svg: ^2.0.1 get_it: ^7.2.0 google_fonts: ^4.0.3 - google_sign_in: ^6.0.0 + hive: ^2.2.3 http: ^0.13.5 json_annotation: ^4.7.0 + keyed_collection_widgets: ^0.4.3 markdown: ^7.0.1 playground_components: { path: ../../../playground/frontend/playground_components } provider: ^6.0.3 + rate_limiter: ^1.0.0 shared_preferences: ^2.0.15 url_launcher: ^6.1.5 url_strategy: ^0.2.0 dev_dependencies: build_runner: ^2.2.0 + enum_map_gen: ^0.2.0 flutter_gen_runner: ^5.2.0 flutter_test: { sdk: flutter } integration_test: { sdk: flutter } diff --git a/playground/frontend/assets/translations/en.yaml b/playground/frontend/assets/translations/en.yaml index d34c40e1bb46b..19c85673df232 100644 --- a/playground/frontend/assets/translations/en.yaml +++ b/playground/frontend/assets/translations/en.yaml @@ -24,3 +24,6 @@ intents: showSuggestions: 'Show Suggestions' viewOnGithub: 'View on GitHub' usesEmulatedData: 'This examples uses emulated data' + +ui: + feedbackTitle: 'Enjoying Playground?' diff --git a/playground/frontend/integration_test/common/common_finders.dart b/playground/frontend/integration_test/common/common_finders.dart index 4e4be822544c5..051c51167dd7b 100644 --- a/playground/frontend/integration_test/common/common_finders.dart +++ b/playground/frontend/integration_test/common/common_finders.dart @@ -30,8 +30,6 @@ import 'package:playground/modules/sdk/components/sdk_selector.dart'; import 'package:playground/modules/sdk/components/sdk_selector_row.dart'; import 'package:playground/modules/shortcuts/components/shortcuts_dialog.dart'; import 'package:playground/pages/standalone_playground/widgets/editor_textarea_wrapper.dart'; -import 'package:playground/pages/standalone_playground/widgets/feedback/feedback_dropdown_content.dart'; -import 'package:playground/pages/standalone_playground/widgets/feedback/feedback_dropdown_icon_button.dart'; import 'package:playground/pages/standalone_playground/widgets/more_actions.dart'; import 'package:playground_components/playground_components.dart'; import 'package:playground_components/src/widgets/drag_handle.dart'; @@ -66,24 +64,24 @@ extension CommonFindersExtension on CommonFinders { return byType(ExampleSelector); } - Finder feedbackDropdownCancelButton() { - return find.byKey(FeedbackDropdownContent.cancelButtonKey); + Finder dismissibleOverlay() { + return find.byKey(BeamOverlay.dismissibleAreaKey); } Finder feedbackDropdownContent() { - return byType(FeedbackDropdownContent); + return byType(FeedbackDropdown); } Finder feedbackDropdownSendButton() { - return find.byKey(FeedbackDropdownContent.sendButtonKey); + return find.byKey(FeedbackDropdown.sendButtonKey); } Finder feedbackDropdownTextField() { - return find.byKey(FeedbackDropdownContent.textFieldKey); + return find.byKey(FeedbackDropdown.textFieldKey); } Finder feedbackThumb(FeedbackRating rating) { - return find.byType(FeedbackDropdownIconButton).and( + return find.byType(InkWell).and( find.byKey(Key(rating.name)), ); } diff --git a/playground/frontend/integration_test/initial_urls_test.dart b/playground/frontend/integration_test/initial_urls_test.dart index a627476208909..bb8784dcc0efe 100644 --- a/playground/frontend/integration_test/initial_urls_test.dart +++ b/playground/frontend/integration_test/initial_urls_test.dart @@ -239,7 +239,7 @@ Future _testUserSharedExampleLoader(WidgetTester wt) async { final snippetId = await exampleCache.saveSnippet( files: [SnippetFile(content: content, isMain: false, name: 'name')], sdk: Sdk.go, - pipelineOptions: 'a=b', + pipelineOptions: '--name=value', ); print('Created user-shared example ID: $snippetId'); diff --git a/playground/frontend/integration_test/miscellaneous_ui/feedback_test.dart b/playground/frontend/integration_test/miscellaneous_ui/feedback_test.dart index c29e2e02281f7..02852b4a3bab9 100644 --- a/playground/frontend/integration_test/miscellaneous_ui/feedback_test.dart +++ b/playground/frontend/integration_test/miscellaneous_ui/feedback_test.dart @@ -52,7 +52,7 @@ Future _checkFeedback( expect(find.feedbackDropdownContent(), findsOneWidget); if (!send) { - await wt.tapAndSettle(find.feedbackDropdownCancelButton()); + await wt.tapAndSettle(find.dismissibleOverlay()); } else { final text = 'This is $rating text.'; await wt.enterText(find.feedbackDropdownTextField(), text); diff --git a/playground/frontend/lib/components/dropdown_button/dropdown_button.dart b/playground/frontend/lib/components/dropdown_button/dropdown_button.dart index 0c671b1b5f433..f0e7f991b2987 100644 --- a/playground/frontend/lib/components/dropdown_button/dropdown_button.dart +++ b/playground/frontend/lib/components/dropdown_button/dropdown_button.dart @@ -179,7 +179,7 @@ class _AppDropdownButtonState extends State void _open() { animationController.forward(); dropdown = createDropdown(); - Overlay.of(context)?.insert(dropdown!); + Overlay.of(context).insert(dropdown!); setState(() { isOpen = true; }); diff --git a/playground/frontend/lib/controllers/factories.dart b/playground/frontend/lib/controllers/factories.dart index 396025c6ae484..92863c18bb739 100644 --- a/playground/frontend/lib/controllers/factories.dart +++ b/playground/frontend/lib/controllers/factories.dart @@ -54,7 +54,7 @@ Future _loadExamples( ExamplesLoadingDescriptor descriptor, ) async { try { - await controller.examplesLoader.load(descriptor); + await controller.examplesLoader.loadIfNew(descriptor); } on Exception catch (ex) { PlaygroundComponents.toastNotifier.addException(ex); diff --git a/playground/frontend/lib/modules/examples/example_selector.dart b/playground/frontend/lib/modules/examples/example_selector.dart index 8c86a38e0dcdc..9950ceaafeba0 100644 --- a/playground/frontend/lib/modules/examples/example_selector.dart +++ b/playground/frontend/lib/modules/examples/example_selector.dart @@ -68,7 +68,7 @@ class _ExampleSelectorState extends State { } else { unawaited(_loadCatalogIfNot(widget.playgroundController)); _overlayEntry = _createExamplesDropdown(); - Overlay.of(context)?.insert(_overlayEntry!); + Overlay.of(context).insert(_overlayEntry!); widget.playgroundController.exampleCache.setSelectorOpened(true); } }, diff --git a/playground/frontend/lib/modules/messages/handlers/set_content_message_handler.dart b/playground/frontend/lib/modules/messages/handlers/set_content_message_handler.dart index 6a807e2069386..ce482f07c097c 100644 --- a/playground/frontend/lib/modules/messages/handlers/set_content_message_handler.dart +++ b/playground/frontend/lib/modules/messages/handlers/set_content_message_handler.dart @@ -46,7 +46,7 @@ class SetContentMessageHandler extends AbstractMessageHandler { final descriptor = message.descriptor; try { - await playgroundController.examplesLoader.load(descriptor); + await playgroundController.examplesLoader.loadIfNew(descriptor); } on Exception catch (ex) { PlaygroundComponents.toastNotifier.addException(ex); diff --git a/playground/frontend/lib/pages/standalone_playground/widgets/feedback/feedback_dropdown_content.dart b/playground/frontend/lib/pages/standalone_playground/widgets/feedback/feedback_dropdown_content.dart deleted file mode 100644 index 93591fd637f19..0000000000000 --- a/playground/frontend/lib/pages/standalone_playground/widgets/feedback/feedback_dropdown_content.dart +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/material.dart'; -import 'package:playground_components/playground_components.dart'; - -import '../../../../constants/font_weight.dart'; -import '../../../../constants/fonts.dart'; -import '../../../../constants/sizes.dart'; -import 'feedback_dropdown_icon_button.dart'; - -const double kTextFieldWidth = 365.0; -const double kTextFieldHeight = 68.0; -const String kFeedbackTitleText = 'Feedback'; -const String kCancelButtonTitle = 'Cancel'; -const String kSendFeedbackButtonTitle = 'Send feedback'; -const String kFeedbackContentText = 'Have feedback? We\'d love to hear it,' - ' but please don\'t share sensitive information.' - '\nHave questions? Try help or support.'; - -class FeedbackDropdownContent extends StatelessWidget { - static const textFieldKey = Key('feedbackTextFieldKey'); - static const cancelButtonKey = Key('cancelButtonKey'); - static const sendButtonKey = Key('sendFeedbackButtonKey'); - - final void Function() close; - final EventSnippetContext eventSnippetContext; - final FeedbackRating feedbackRating; - final TextEditingController textController; - - const FeedbackDropdownContent({ - required this.close, - required this.eventSnippetContext, - required this.feedbackRating, - required this.textController, - }); - - @override - Widget build(BuildContext context) { - final borderColor = - Theme.of(context).extension()!.borderColor; - - final OutlineInputBorder border = OutlineInputBorder( - borderSide: BorderSide(color: borderColor), - borderRadius: BorderRadius.circular(kMdBorderRadius), - ); - - return Padding( - padding: const EdgeInsets.symmetric(vertical: kXlSpacing), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: kXlSpacing), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: kXlSpacing), - child: Text( - kFeedbackTitleText, - style: getTitleFontStyle( - textStyle: const TextStyle( - fontSize: kFeedbackTitleFontSize, - fontWeight: kBoldWeight, - ), - ), - ), - ), - MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => close(), - child: const Icon(Icons.close), - ), - ), - ], - ), - Text( - kFeedbackContentText, - style: getTitleFontStyle( - textStyle: const TextStyle( - fontSize: kFeedbackContentFontSize, - fontWeight: kNormalWeight, - ), - ), - ), - Container( - margin: const EdgeInsets.only( - top: kMdSpacing, - bottom: kXlSpacing, - ), - width: kTextFieldWidth, - height: kTextFieldHeight, - decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.circular(kMdBorderRadius), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(kMdBorderRadius), - child: TextFormField( - key: textFieldKey, - controller: textController, - decoration: InputDecoration( - focusedBorder: border, - enabledBorder: border, - contentPadding: const EdgeInsets.all(kMdSpacing), - ), - cursorColor: borderColor, - cursorWidth: kCursorSize, - onFieldSubmitted: (String filterText) {}, - maxLines: 3, - ), - ), - ), - ], - ), - ), - const BeamDivider(), - Padding( - padding: const EdgeInsets.only( - top: kXlSpacing, - left: kXlSpacing, - right: kXlSpacing, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - height: kContainerHeight, - decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.circular(kSmBorderRadius), - border: Border.all( - color: borderColor, - ), - ), - child: TextButton( - key: cancelButtonKey, - onPressed: () { - close(); - textController.clear(); - }, - child: const Text(kCancelButtonTitle), - ), - ), - Container( - margin: const EdgeInsets.only(left: kLgSpacing), - height: kContainerHeight, - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(kSmBorderRadius), - ), - child: ElevatedButton( - key: sendButtonKey, - onPressed: () { - if (textController.text.isNotEmpty) { - PlaygroundComponents.analyticsService.sendUnawaited( - FeedbackFormSentAnalyticsEvent( - rating: feedbackRating, - text: textController.text, - snippetContext: eventSnippetContext, - ), - ); - } - close(); - textController.clear(); - }, - child: const Text(kSendFeedbackButtonTitle), - ), - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/playground/frontend/lib/pages/standalone_playground/widgets/feedback/feedback_dropdown_icon_button.dart b/playground/frontend/lib/pages/standalone_playground/widgets/feedback/feedback_dropdown_icon_button.dart deleted file mode 100644 index 8fa794af76d82..0000000000000 --- a/playground/frontend/lib/pages/standalone_playground/widgets/feedback/feedback_dropdown_icon_button.dart +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:playground_components/playground_components.dart'; - -import '../../../../constants/sizes.dart'; -import '../../../../src/assets/assets.gen.dart'; -import 'feedback_dropdown_content.dart'; - -const double kFeedbackTitleFontSize = 24.0; -const double kFeedbackContentFontSize = 14.0; -const double kFeedbackDyBottomAlignment = 50.0; -const double kFeedbackDxLeftAlignment = 10.0; -const double kFeedbackDropdownWidth = 400.0; - -const int kAnimationDurationInMilliseconds = 80; -const Offset kAnimationBeginOffset = Offset(0.0, -0.02); -const Offset kAnimationEndOffset = Offset(0.0, 0.0); - -class FeedbackDropdownIconButton extends StatefulWidget { - final bool isSelected; - final FeedbackRating feedbackRating; - final void Function() onClick; - final PlaygroundController playgroundController; - - const FeedbackDropdownIconButton({ - Key? key, - required this.isSelected, - required this.feedbackRating, - required this.onClick, - required this.playgroundController, - }) : super(key: key); - - @override - State createState() => - _FeedbackDropdownIconButton(); -} - -class _FeedbackDropdownIconButton extends State - with TickerProviderStateMixin { - final GlobalKey feedbackKey = LabeledGlobalKey('FeedbackDropdown'); - final TextEditingController feedbackTextController = TextEditingController(); - late OverlayEntry? dropdown; - late AnimationController animationController; - late Animation offsetAnimation; - bool isOpen = false; - - @override - void initState() { - super.initState(); - animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: kAnimationDurationInMilliseconds), - ); - offsetAnimation = Tween( - begin: kAnimationBeginOffset, - end: kAnimationEndOffset, - ).animate(animationController); - } - - @override - void dispose() { - animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final AppLocalizations appLocale = AppLocalizations.of(context)!; - - final String tooltip; - final String icon; - final String filledIcon; - - switch (widget.feedbackRating) { - case FeedbackRating.positive: - tooltip = appLocale.enjoying; - icon = Assets.thumbUp; - filledIcon = Assets.thumbUpFilled; - break; - case FeedbackRating.negative: - tooltip = appLocale.notEnjoying; - icon = Assets.thumbDown; - filledIcon = Assets.thumbDownFilled; - break; - } - - return Semantics( - container: true, - child: IconButton( - key: feedbackKey, - padding: EdgeInsets.zero, - onPressed: () { - _changeSelectorVisibility(); - widget.onClick(); - }, - tooltip: tooltip, - icon: SvgPicture.asset( - widget.isSelected ? filledIcon : icon, - ), - ), - ); - } - - OverlayEntry createDropdown() { - return OverlayEntry( - builder: (context) { - return Stack( - children: [ - GestureDetector( - onTap: () { - _close(); - }, - child: Container( - color: Colors.transparent, - height: double.infinity, - width: double.infinity, - ), - ), - Positioned( - left: kFeedbackDxLeftAlignment, - bottom: kFeedbackDyBottomAlignment, - child: SlideTransition( - position: offsetAnimation, - child: Material( - elevation: kElevation * 2, - borderRadius: BorderRadius.circular(kMdBorderRadius), - child: Container( - width: kFeedbackDropdownWidth, - decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.circular(kMdBorderRadius), - ), - child: FeedbackDropdownContent( - close: _close, - eventSnippetContext: - widget.playgroundController.eventSnippetContext, - feedbackRating: widget.feedbackRating, - textController: feedbackTextController, - ), - ), - ), - ), - ), - ], - ); - }, - ); - } - - void _close() { - animationController.reverse(); - dropdown?.remove(); - setState(() { - isOpen = false; - }); - feedbackTextController.clear(); - } - - void _open() { - animationController.forward(); - dropdown = createDropdown(); - Overlay.of(context)?.insert(dropdown!); - setState(() { - isOpen = true; - }); - } - - void _changeSelectorVisibility() { - if (isOpen) { - _close(); - } else { - _open(); - } - } -} diff --git a/playground/frontend/lib/pages/standalone_playground/widgets/feedback/playground_feedback.dart b/playground/frontend/lib/pages/standalone_playground/widgets/feedback/playground_feedback.dart deleted file mode 100644 index a80ba4d8cd8ad..0000000000000 --- a/playground/frontend/lib/pages/standalone_playground/widgets/feedback/playground_feedback.dart +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:playground_components/playground_components.dart'; -import 'package:provider/provider.dart'; - -import '../../../../constants/font_weight.dart'; -import '../../notifiers/feedback_state.dart'; -import 'feedback_dropdown_icon_button.dart'; - -/// A status bar item for feedback. -class PlaygroundFeedback extends StatelessWidget { - const PlaygroundFeedback({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, playgroundController, child) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - AppLocalizations.of(context)!.enjoyingPlayground, - style: const TextStyle(fontWeight: kBoldWeight), - ), - FeedbackDropdownIconButton( - key: Key(FeedbackRating.positive.name), - feedbackRating: FeedbackRating.positive, - isSelected: _getFeedbackState(context, true).feedbackRating == - FeedbackRating.positive, - onClick: () => _onRated( - context, - FeedbackRating.positive, - playgroundController, - ), - playgroundController: playgroundController, - ), - FeedbackDropdownIconButton( - key: Key(FeedbackRating.negative.name), - feedbackRating: FeedbackRating.negative, - isSelected: _getFeedbackState(context, true).feedbackRating == - FeedbackRating.negative, - onClick: () => _onRated( - context, - FeedbackRating.negative, - playgroundController, - ), - playgroundController: playgroundController, - ), - ], - ), - ); - } - - void _onRated( - BuildContext context, - FeedbackRating rating, - PlaygroundController playgroundController, - ) { - _getFeedbackState(context, false).setEnjoying(rating); - - PlaygroundComponents.analyticsService.sendUnawaited( - AppRatedAnalyticsEvent( - snippetContext: playgroundController.eventSnippetContext, - rating: rating, - ), - ); - } - - FeedbackState _getFeedbackState(BuildContext context, bool listen) { - return Provider.of(context, listen: listen); - } -} diff --git a/playground/frontend/lib/pages/standalone_playground/widgets/playground_page_footer.dart b/playground/frontend/lib/pages/standalone_playground/widgets/playground_page_footer.dart index 5873499413ad1..8af1ab6eca318 100644 --- a/playground/frontend/lib/pages/standalone_playground/widgets/playground_page_footer.dart +++ b/playground/frontend/lib/pages/standalone_playground/widgets/playground_page_footer.dart @@ -16,12 +16,13 @@ * limitations under the License. */ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; import '../../../constants/sizes.dart'; -import 'feedback/playground_feedback.dart'; class PlaygroundPageFooter extends StatelessWidget { const PlaygroundPageFooter({Key? key}) : super(key: key); @@ -43,7 +44,10 @@ class PlaygroundPageFooter extends StatelessWidget { spacing: kXlSpacing, crossAxisAlignment: WrapCrossAlignment.center, children: [ - const PlaygroundFeedback(), + FeedbackWidget( + controller: GetIt.instance.get(), + title: 'ui.feedbackTitle'.tr(), + ), ReportIssueButton(playgroundController: playgroundController), const PrivacyPolicyButton(), const CopyrightWidget(), diff --git a/playground/frontend/playground_components/assets/svg/thumb_down.svg b/playground/frontend/playground_components/assets/svg/thumb_down.svg new file mode 100644 index 0000000000000..5abbc3e06fa5f --- /dev/null +++ b/playground/frontend/playground_components/assets/svg/thumb_down.svg @@ -0,0 +1,24 @@ + + + + + diff --git a/playground/frontend/playground_components/assets/svg/thumb_down_filled.svg b/playground/frontend/playground_components/assets/svg/thumb_down_filled.svg new file mode 100644 index 0000000000000..cfa675696a115 --- /dev/null +++ b/playground/frontend/playground_components/assets/svg/thumb_down_filled.svg @@ -0,0 +1,24 @@ + + + + + diff --git a/playground/frontend/playground_components/assets/svg/thumb_up.svg b/playground/frontend/playground_components/assets/svg/thumb_up.svg new file mode 100644 index 0000000000000..cf508c90c85ee --- /dev/null +++ b/playground/frontend/playground_components/assets/svg/thumb_up.svg @@ -0,0 +1,24 @@ + + + + + diff --git a/playground/frontend/playground_components/assets/svg/thumb_up_filled.svg b/playground/frontend/playground_components/assets/svg/thumb_up_filled.svg new file mode 100644 index 0000000000000..6540bceeaf4ea --- /dev/null +++ b/playground/frontend/playground_components/assets/svg/thumb_up_filled.svg @@ -0,0 +1,24 @@ + + + + + diff --git a/playground/frontend/playground_components/assets/translations/en.yaml b/playground/frontend/playground_components/assets/translations/en.yaml index afdb313aa2328..996baed408028 100644 --- a/playground/frontend/playground_components/assets/translations/en.yaml +++ b/playground/frontend/playground_components/assets/translations/en.yaml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +dialogs: + cancel: Cancel + errors: error: 'Error' failedParseOptions: > @@ -43,12 +46,18 @@ intents: reset: 'Reset Code' widgets: - codeEditor: label: 'Code Text Area' closeButton: label: 'Close' + + feedback: + hint: "Have feedback? We'd love to hear it, but please don't share sensitive information." + negative: 'Bad Experience' + positive: 'Good Experience' + send: 'Send Feedback' + title: 'Feedback' output: filter: diff --git a/playground/frontend/playground_components/lib/playground_components.dart b/playground/frontend/playground_components/lib/playground_components.dart index d292b6e7edbc5..856a1ba818333 100644 --- a/playground/frontend/playground_components/lib/playground_components.dart +++ b/playground/frontend/playground_components/lib/playground_components.dart @@ -22,8 +22,9 @@ export 'src/constants/analytics.dart'; export 'src/constants/colors.dart'; export 'src/constants/links.dart'; export 'src/constants/sizes.dart'; - +export 'src/controllers/build_metadata.dart'; export 'src/controllers/example_loaders/examples_loader.dart'; +export 'src/controllers/feedback_controller.dart'; export 'src/controllers/playground_controller.dart'; export 'src/controllers/public_notifier.dart'; export 'src/controllers/window_close_notifier/window_close_notifier.dart'; @@ -42,6 +43,7 @@ export 'src/models/example_loading_descriptors/content_example_loading_descripto export 'src/models/example_loading_descriptors/empty_example_loading_descriptor.dart'; export 'src/models/example_loading_descriptors/example_loading_descriptor.dart'; export 'src/models/example_loading_descriptors/examples_loading_descriptor.dart'; +export 'src/models/example_loading_descriptors/hive_example_loading_descriptor.dart'; export 'src/models/example_loading_descriptors/http_example_loading_descriptor.dart'; export 'src/models/example_loading_descriptors/standard_example_loading_descriptor.dart'; export 'src/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart'; @@ -102,7 +104,10 @@ export 'src/widgets/close_button.dart'; export 'src/widgets/complexity.dart'; export 'src/widgets/copyright.dart'; export 'src/widgets/dialog.dart'; +export 'src/widgets/dialogs/confirm.dart'; +export 'src/widgets/dialogs/progress.dart'; export 'src/widgets/divider.dart'; +export 'src/widgets/feedback.dart'; export 'src/widgets/header_icon_button.dart'; export 'src/widgets/loading_error.dart'; export 'src/widgets/loading_indicator.dart'; @@ -111,8 +116,8 @@ export 'src/widgets/output/output.dart'; export 'src/widgets/output/output_tab.dart'; export 'src/widgets/output/result_tab.dart'; export 'src/widgets/overlay/body.dart'; -export 'src/widgets/overlay/dismissible.dart'; export 'src/widgets/overlay/opener.dart'; +export 'src/widgets/overlay/widget.dart'; export 'src/widgets/reset_button.dart'; export 'src/widgets/run_or_cancel_button.dart'; export 'src/widgets/shortcut_tooltip.dart'; diff --git a/playground/frontend/playground_components/lib/src/assets/assets.gen.dart b/playground/frontend/playground_components/lib/src/assets/assets.gen.dart index 288b595d38ba9..981557a7e465b 100644 --- a/playground/frontend/playground_components/lib/src/assets/assets.gen.dart +++ b/playground/frontend/playground_components/lib/src/assets/assets.gen.dart @@ -60,8 +60,27 @@ class $AssetsSvgGen { /// File path: assets/svg/drag-vertical.svg String get dragVertical => 'assets/svg/drag-vertical.svg'; + /// File path: assets/svg/thumb_down.svg + String get thumbDown => 'assets/svg/thumb_down.svg'; + + /// File path: assets/svg/thumb_down_filled.svg + String get thumbDownFilled => 'assets/svg/thumb_down_filled.svg'; + + /// File path: assets/svg/thumb_up.svg + String get thumbUp => 'assets/svg/thumb_up.svg'; + + /// File path: assets/svg/thumb_up_filled.svg + String get thumbUpFilled => 'assets/svg/thumb_up_filled.svg'; + /// List of all assets - List get values => [dragHorizontal, dragVertical]; + List get values => [ + dragHorizontal, + dragVertical, + thumbDown, + thumbDownFilled, + thumbUp, + thumbUpFilled + ]; } class $AssetsSymbolsGen { diff --git a/playground/frontend/playground_components/lib/src/cache/example_cache.dart b/playground/frontend/playground_components/lib/src/cache/example_cache.dart index 4258cddd0beda..6582ba7ee1453 100644 --- a/playground/frontend/playground_components/lib/src/cache/example_cache.dart +++ b/playground/frontend/playground_components/lib/src/cache/example_cache.dart @@ -134,7 +134,8 @@ class ExampleCache extends ChangeNotifier { return Example( complexity: result.complexity, files: result.files, - name: result.files.first.name, + name: 'User Snippet', + isMultiFile: result.files.length > 1, path: id, sdk: result.sdk, pipelineOptions: result.pipelineOptions, diff --git a/playground/frontend/playground_components/lib/src/constants/sizes.dart b/playground/frontend/playground_components/lib/src/constants/sizes.dart index d98ccd0448815..8a85dfb61e233 100644 --- a/playground/frontend/playground_components/lib/src/constants/sizes.dart +++ b/playground/frontend/playground_components/lib/src/constants/sizes.dart @@ -43,6 +43,7 @@ class BeamSizes { static const double loadingIndicator = 40; static const double splitViewSeparator = BeamSizes.size8; static const double tabBarHeight = 50; + static const double popupWidth = 420; } class BeamBorderRadius { diff --git a/playground/frontend/playground_components/lib/src/controllers/example_loaders/examples_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/examples_loader.dart index 75342c3ec0fe9..5a52659caf50f 100644 --- a/playground/frontend/playground_components/lib/src/controllers/example_loaders/examples_loader.dart +++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/examples_loader.dart @@ -31,6 +31,7 @@ import 'content_example_loader.dart'; import 'empty_example_loader.dart'; import 'example_loader.dart'; import 'example_loader_factory.dart'; +import 'hive_example_loader.dart'; import 'http_example_loader.dart'; import 'standard_example_loader.dart'; import 'user_shared_example_loader.dart'; @@ -44,6 +45,7 @@ class ExamplesLoader { defaultFactory.add(CatalogDefaultExampleLoader.new); defaultFactory.add(ContentExampleLoader.new); defaultFactory.add(EmptyExampleLoader.new); + defaultFactory.add(HiveExampleLoader.new); defaultFactory.add(HttpExampleLoader.new); defaultFactory.add(StandardExampleLoader.new); defaultFactory.add(UserSharedExampleLoader.new); @@ -56,11 +58,14 @@ class ExamplesLoader { /// Loads examples from [descriptor]'s immediate list. /// /// Sets empty editor for SDKs of failed examples. - Future load(ExamplesLoadingDescriptor descriptor) async { + Future loadIfNew(ExamplesLoadingDescriptor descriptor) async { if (_descriptor == descriptor) { return; } + await load(descriptor); + } + Future load(ExamplesLoadingDescriptor descriptor) async { _descriptor = descriptor; final loaders = descriptor.descriptors.map(_createLoader).whereNotNull(); diff --git a/playground/frontend/playground_components/lib/src/controllers/example_loaders/hive_example_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/hive_example_loader.dart new file mode 100644 index 0000000000000..7f394cfa606d0 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/hive_example_loader.dart @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'dart:convert'; + +import 'package:hive/hive.dart'; + +import '../../cache/example_cache.dart'; +import '../../models/example.dart'; +import '../../models/example_loading_descriptors/hive_example_loading_descriptor.dart'; +import '../../models/sdk.dart'; +import 'example_loader.dart'; + +class HiveExampleLoader extends ExampleLoader { + @override + final HiveExampleLoadingDescriptor descriptor; + + final ExampleCache exampleCache; + + HiveExampleLoader({ + required this.descriptor, + required this.exampleCache, + }); + + @override + Sdk? get sdk => descriptor.sdk; + + @override + Future get future async { + final box = await Hive.openBox(descriptor.boxName); + final Map map = jsonDecode(box.get(descriptor.snippetId)); + return Example.fromJson(map); + } +} diff --git a/playground/frontend/playground_components/lib/src/controllers/example_loaders/standard_example_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/standard_example_loader.dart index 0afb6828f9b78..520791d7c1bed 100644 --- a/playground/frontend/playground_components/lib/src/controllers/example_loaders/standard_example_loader.dart +++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/standard_example_loader.dart @@ -19,8 +19,8 @@ import 'dart:async'; import '../../cache/example_cache.dart'; +import '../../exceptions/multiple_exceptions.dart'; import '../../models/example.dart'; -import '../../models/example_base.dart'; import '../../models/example_loading_descriptors/standard_example_loading_descriptor.dart'; import '../../models/sdk.dart'; import 'example_loader.dart'; @@ -35,10 +35,9 @@ class StandardExampleLoader extends ExampleLoader { final ExampleCache exampleCache; final _completer = Completer(); - Sdk? _sdk; @override - Sdk? get sdk => _sdk; + Sdk? get sdk => descriptor.sdk; @override Future get future => _completer.future; @@ -52,31 +51,39 @@ class StandardExampleLoader extends ExampleLoader { Future _load() async { try { - final example = await _loadExampleBase(); - - if (example == null) { - _completer.completeError(Exception('Example not found: $descriptor')); - return; - } + final exampleBase = await exampleCache.getPrecompiledObject( + descriptor.path, + descriptor.sdk, + ); _completer.complete( - exampleCache.loadExampleInfo(example), + await exampleCache.loadExampleInfo(exampleBase), + ); + } on Exception catch (ex, trace) { + await _tryLoadSharedExample( + previousExceptions: [ex], + previousStackTraces: [trace], ); - - // ignore: avoid_catches_without_on_clauses - } catch (ex, trace) { - _completer.completeError(ex, trace); - return; } } - Future _loadExampleBase() async { - _sdk = Sdk.tryParseExamplePath(descriptor.path); - - if (_sdk == null) { - return null; + Future _tryLoadSharedExample({ + required List previousExceptions, + required List previousStackTraces, + }) async { + try { + final example = await exampleCache.loadSharedExample( + descriptor.path, + viewOptions: descriptor.viewOptions, + ); + _completer.complete(example); + } on Exception catch (ex, trace) { + _completer.completeError( + MultipleExceptions( + exceptions: [...previousExceptions, ex], + stackTraces: [...previousStackTraces, trace], + ), + ); } - - return exampleCache.getPrecompiledObject(descriptor.path, _sdk!); } } diff --git a/playground/frontend/playground_components/lib/src/controllers/feedback_controller.dart b/playground/frontend/playground_components/lib/src/controllers/feedback_controller.dart new file mode 100644 index 0000000000000..d8145ffc0b5c9 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/controllers/feedback_controller.dart @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/widgets.dart'; + +import '../enums/feedback_rating.dart'; +import '../models/event_snippet_context.dart'; + +class FeedbackController extends ChangeNotifier { + EventSnippetContext? eventSnippetContext; + Map additionalParams; + final textController = TextEditingController(); + + FeedbackController({ + this.eventSnippetContext, + this.additionalParams = const {}, + }); + + FeedbackRating? _rating; + FeedbackRating? get rating => _rating; + + set rating(FeedbackRating? newValue) { + _rating = newValue; + notifyListeners(); + } +} diff --git a/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart b/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart index 612e9036c7e24..74eb34b673bba 100644 --- a/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart +++ b/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart @@ -42,6 +42,7 @@ import '../services/symbols/symbols_notifier.dart'; import '../util/logical_keyboard_key.dart'; import 'code_runner.dart'; import 'example_loaders/examples_loader.dart'; +import 'feedback_controller.dart'; import 'result_filter_controller.dart'; import 'snippet_editing_controller.dart'; @@ -207,19 +208,17 @@ class PlaygroundController with ChangeNotifier { }) { if (setCurrentSdk) { _sdk = example.sdk; - final controller = _getOrCreateSnippetEditingController( - example.sdk, - loadDefaultIfNot: false, - ); - - controller.setExample(example, descriptor: descriptor); _ensureSymbolsInitialized(); - } else { - final controller = _getOrCreateSnippetEditingController( - example.sdk, - loadDefaultIfNot: false, - ); - controller.setExample(example, descriptor: descriptor); + } + + final controller = _getOrCreateSnippetEditingController( + example.sdk, + loadDefaultIfNot: false, + ); + controller.setExample(example, descriptor: descriptor); + if (example.sdk == _sdk) { + GetIt.instance.get().eventSnippetContext = + controller.eventSnippetContext; } codeRunner.reset(); @@ -231,10 +230,12 @@ class PlaygroundController with ChangeNotifier { bool notify = true, }) { _sdk = sdk; - _getOrCreateSnippetEditingController( + final controller = _getOrCreateSnippetEditingController( sdk, loadDefaultIfNot: true, ); + GetIt.instance.get().eventSnippetContext = + controller.eventSnippetContext; _ensureSymbolsInitialized(); if (notify) { diff --git a/playground/frontend/playground_components/lib/src/exceptions/multiple_exceptions.dart b/playground/frontend/playground_components/lib/src/exceptions/multiple_exceptions.dart new file mode 100644 index 0000000000000..e3fffb8c31ffb --- /dev/null +++ b/playground/frontend/playground_components/lib/src/exceptions/multiple_exceptions.dart @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +class MultipleExceptions implements Exception { + final List exceptions; + final List stackTraces; + + MultipleExceptions({ + required this.exceptions, + required this.stackTraces, + }); + + @override + String toString() { + final buffer = StringBuffer('Exceptions (${exceptions.length}): '); + for (var i = 0; i < exceptions.length; i++) { + buffer + ..write('Exception #') + ..write(i + 1) + ..writeln(':') + ..writeln(exceptions[i]) + ..writeln('StackTrace:') + ..writeln(stackTraces[i]); + } + return buffer.toString(); + } +} diff --git a/playground/frontend/playground_components/lib/src/locator.dart b/playground/frontend/playground_components/lib/src/locator.dart index c7e28bef6e67c..f741383f31ef4 100644 --- a/playground/frontend/playground_components/lib/src/locator.dart +++ b/playground/frontend/playground_components/lib/src/locator.dart @@ -19,11 +19,13 @@ import 'package:get_it/get_it.dart'; import 'controllers/build_metadata.dart'; +import 'controllers/feedback_controller.dart'; import 'services/symbols/symbols_notifier.dart'; import 'services/toast_notifier.dart'; Future initializeServiceLocator() async { GetIt.instance.registerSingleton(BuildMetadataController()); + GetIt.instance.registerSingleton(FeedbackController()); GetIt.instance.registerSingleton(SymbolsNotifier()); GetIt.instance.registerSingleton(ToastNotifier()); } diff --git a/playground/frontend/playground_components/lib/src/models/dataset.dart b/playground/frontend/playground_components/lib/src/models/dataset.dart index 5ec79d29112e6..b51ba85ed1c6e 100644 --- a/playground/frontend/playground_components/lib/src/models/dataset.dart +++ b/playground/frontend/playground_components/lib/src/models/dataset.dart @@ -16,8 +16,13 @@ * limitations under the License. */ +import 'package:json_annotation/json_annotation.dart'; + import '../enums/emulator_type.dart'; +part 'dataset.g.dart'; + +@JsonSerializable() class Dataset { final EmulatorType? type; final Map options; @@ -28,4 +33,9 @@ class Dataset { required this.options, required this.datasetPath, }); + + factory Dataset.fromJson(Map json) => + _$DatasetFromJson(json); + + Map toJson() => _$DatasetToJson(this); } diff --git a/playground/frontend/playground_components/lib/src/models/dataset.g.dart b/playground/frontend/playground_components/lib/src/models/dataset.g.dart new file mode 100644 index 0000000000000..85256b1d5d83b --- /dev/null +++ b/playground/frontend/playground_components/lib/src/models/dataset.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'dataset.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Dataset _$DatasetFromJson(Map json) => Dataset( + type: $enumDecodeNullable(_$EmulatorTypeEnumMap, json['type']), + options: Map.from(json['options'] as Map), + datasetPath: json['datasetPath'] as String, + ); + +Map _$DatasetToJson(Dataset instance) => { + 'type': _$EmulatorTypeEnumMap[instance.type], + 'options': instance.options, + 'datasetPath': instance.datasetPath, + }; + +const _$EmulatorTypeEnumMap = { + EmulatorType.kafka: 'kafka', +}; diff --git a/playground/frontend/playground_components/lib/src/models/example.dart b/playground/frontend/playground_components/lib/src/models/example.dart index 97ad54bf8b1c9..85858e8757177 100644 --- a/playground/frontend/playground_components/lib/src/models/example.dart +++ b/playground/frontend/playground_components/lib/src/models/example.dart @@ -16,11 +16,19 @@ * limitations under the License. */ +import 'package:json_annotation/json_annotation.dart'; + +import '../enums/complexity.dart'; +import 'dataset.dart'; import 'example_base.dart'; +import 'example_view_options.dart'; import 'sdk.dart'; import 'snippet_file.dart'; +part 'example.g.dart'; + /// A [ExampleBase] that also has all large fields fetched. +@JsonSerializable() class Example extends ExampleBase { final List files; final String? graph; @@ -48,6 +56,12 @@ class Example extends ExampleBase { super.viewOptions, }); + factory Example.fromJson(Map json) => + _$ExampleFromJson(json); + + @override + Map toJson() => _$ExampleToJson(this); + Example.fromBase( ExampleBase example, { required this.files, diff --git a/playground/frontend/playground_components/lib/src/models/example.g.dart b/playground/frontend/playground_components/lib/src/models/example.g.dart new file mode 100644 index 0000000000000..f46c32c1d0d44 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/models/example.g.dart @@ -0,0 +1,72 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'example.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Example _$ExampleFromJson(Map json) => Example( + files: (json['files'] as List) + .map((e) => SnippetFile.fromJson(e as Map)) + .toList(), + name: json['name'] as String, + sdk: Sdk.fromJson(json['sdk'] as Map), + type: $enumDecode(_$ExampleTypeEnumMap, json['type']), + path: json['path'] as String, + complexity: $enumDecodeNullable(_$ComplexityEnumMap, json['complexity']), + contextLine: json['contextLine'] as int? ?? 1, + datasets: (json['datasets'] as List?) + ?.map((e) => Dataset.fromJson(e as Map)) + .toList() ?? + const [], + description: json['description'] as String? ?? '', + graph: json['graph'] as String?, + isMultiFile: json['isMultiFile'] as bool? ?? false, + logs: json['logs'] as String?, + outputs: json['outputs'] as String?, + pipelineOptions: json['pipelineOptions'] as String? ?? '', + tags: + (json['tags'] as List?)?.map((e) => e as String).toList() ?? + const [], + urlNotebook: json['urlNotebook'] as String?, + urlVcs: json['urlVcs'] as String?, + viewOptions: json['viewOptions'] == null + ? ExampleViewOptions.empty + : ExampleViewOptions.fromJson( + json['viewOptions'] as Map), + ); + +Map _$ExampleToJson(Example instance) => { + 'complexity': _$ComplexityEnumMap[instance.complexity], + 'contextLine': instance.contextLine, + 'datasets': instance.datasets, + 'description': instance.description, + 'isMultiFile': instance.isMultiFile, + 'name': instance.name, + 'path': instance.path, + 'pipelineOptions': instance.pipelineOptions, + 'sdk': instance.sdk, + 'tags': instance.tags, + 'type': _$ExampleTypeEnumMap[instance.type]!, + 'urlNotebook': instance.urlNotebook, + 'urlVcs': instance.urlVcs, + 'viewOptions': instance.viewOptions, + 'files': instance.files, + 'graph': instance.graph, + 'logs': instance.logs, + 'outputs': instance.outputs, + }; + +const _$ExampleTypeEnumMap = { + ExampleType.all: 'all', + ExampleType.example: 'example', + ExampleType.kata: 'kata', + ExampleType.test: 'test', +}; + +const _$ComplexityEnumMap = { + Complexity.basic: 'BASIC', + Complexity.medium: 'MEDIUM', + Complexity.advanced: 'ADVANCED', +}; diff --git a/playground/frontend/playground_components/lib/src/models/example_base.dart b/playground/frontend/playground_components/lib/src/models/example_base.dart index 908c02a162911..25476de300ce4 100644 --- a/playground/frontend/playground_components/lib/src/models/example_base.dart +++ b/playground/frontend/playground_components/lib/src/models/example_base.dart @@ -17,6 +17,7 @@ */ import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; import '../enums/complexity.dart'; import '../repositories/example_repository.dart'; @@ -24,6 +25,8 @@ import 'dataset.dart'; import 'example_view_options.dart'; import 'sdk.dart'; +part 'example_base.g.dart'; + enum ExampleType { all, example, @@ -49,6 +52,7 @@ extension ExampleTypeToString on ExampleType { /// An example's basic info that does not contain source code /// and other large fields. /// These objects are fetched as lists from [ExampleRepository]. +@JsonSerializable() class ExampleBase with Comparable, EquatableMixin { final bool alwaysRun; final Complexity? complexity; @@ -86,6 +90,11 @@ class ExampleBase with Comparable, EquatableMixin { this.viewOptions = ExampleViewOptions.empty, }); + factory ExampleBase.fromJson(Map json) => + _$ExampleBaseFromJson(json); + + Map toJson() => _$ExampleBaseToJson(this); + // TODO(alexeyinkin): Use all fields, https://github.com/apache/beam/issues/23979 @override List get props => [path]; diff --git a/playground/frontend/playground_components/lib/src/models/example_base.g.dart b/playground/frontend/playground_components/lib/src/models/example_base.g.dart new file mode 100644 index 0000000000000..0f18242faf2c0 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/models/example_base.g.dart @@ -0,0 +1,63 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'example_base.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ExampleBase _$ExampleBaseFromJson(Map json) => ExampleBase( + name: json['name'] as String, + path: json['path'] as String, + sdk: Sdk.fromJson(json['sdk'] as Map), + type: $enumDecode(_$ExampleTypeEnumMap, json['type']), + complexity: $enumDecodeNullable(_$ComplexityEnumMap, json['complexity']), + contextLine: json['contextLine'] as int? ?? 1, + datasets: (json['datasets'] as List?) + ?.map((e) => Dataset.fromJson(e as Map)) + .toList() ?? + const [], + description: json['description'] as String? ?? '', + isMultiFile: json['isMultiFile'] as bool? ?? false, + pipelineOptions: json['pipelineOptions'] as String? ?? '', + tags: + (json['tags'] as List?)?.map((e) => e as String).toList() ?? + const [], + urlNotebook: json['urlNotebook'] as String?, + urlVcs: json['urlVcs'] as String?, + viewOptions: json['viewOptions'] == null + ? ExampleViewOptions.empty + : ExampleViewOptions.fromJson( + json['viewOptions'] as Map), + ); + +Map _$ExampleBaseToJson(ExampleBase instance) => + { + 'complexity': _$ComplexityEnumMap[instance.complexity], + 'contextLine': instance.contextLine, + 'datasets': instance.datasets, + 'description': instance.description, + 'isMultiFile': instance.isMultiFile, + 'name': instance.name, + 'path': instance.path, + 'pipelineOptions': instance.pipelineOptions, + 'sdk': instance.sdk, + 'tags': instance.tags, + 'type': _$ExampleTypeEnumMap[instance.type]!, + 'urlNotebook': instance.urlNotebook, + 'urlVcs': instance.urlVcs, + 'viewOptions': instance.viewOptions, + }; + +const _$ExampleTypeEnumMap = { + ExampleType.all: 'all', + ExampleType.example: 'example', + ExampleType.kata: 'kata', + ExampleType.test: 'test', +}; + +const _$ComplexityEnumMap = { + Complexity.basic: 'BASIC', + Complexity.medium: 'MEDIUM', + Complexity.advanced: 'ADVANCED', +}; diff --git a/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/hive_example_loading_descriptor.dart b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/hive_example_loading_descriptor.dart new file mode 100644 index 0000000000000..f69a9e505914c --- /dev/null +++ b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/hive_example_loading_descriptor.dart @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '../example_view_options.dart'; +import '../sdk.dart'; +import 'example_loading_descriptor.dart'; + +/// Describes a loadable example saved in Hive. +class HiveExampleLoadingDescriptor extends ExampleLoadingDescriptor { + final String boxName; + final Sdk sdk; + final String snippetId; + + const HiveExampleLoadingDescriptor({ + required this.boxName, + required this.sdk, + required this.snippetId, + super.viewOptions, + }); + + @override + List get props => [ + boxName, + sdk.id, + snippetId, + viewOptions, + ]; + + @override + HiveExampleLoadingDescriptor copyWithoutViewOptions() => + HiveExampleLoadingDescriptor( + boxName: boxName, + sdk: sdk, + snippetId: snippetId, + ); + + @override + Map toJson() => { + 'boxName': boxName, + 'sdk': sdk.id, + 'shared': snippetId, + ...viewOptions.toShortMap(), + }; + + static HiveExampleLoadingDescriptor? tryParse( + Map map, + ) { + final boxName = map['boxName']; + final sdkId = map['sdk']; + final snippetId = map['shared']; + + if (sdkId == null || snippetId == null) { + return null; + } + + return HiveExampleLoadingDescriptor( + boxName: boxName, + sdk: Sdk.parseOrCreate(sdkId), + snippetId: snippetId, + viewOptions: ExampleViewOptions.fromShortMap(map), + ); + } +} diff --git a/playground/frontend/playground_components/lib/src/models/example_view_options.dart b/playground/frontend/playground_components/lib/src/models/example_view_options.dart index be1d157534170..54ced794fc207 100644 --- a/playground/frontend/playground_components/lib/src/models/example_view_options.dart +++ b/playground/frontend/playground_components/lib/src/models/example_view_options.dart @@ -17,9 +17,13 @@ */ import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; import '../util/string.dart'; +part 'example_view_options.g.dart'; + +@JsonSerializable() class ExampleViewOptions with EquatableMixin { final bool foldCommentAtLineZero; final bool foldImports; @@ -35,6 +39,16 @@ class ExampleViewOptions with EquatableMixin { this.foldImports = true, }); + /// Parses a fully normalized map. + factory ExampleViewOptions.fromJson(Map json) => + _$ExampleViewOptionsFromJson(json); + + Map toJson() => _$ExampleViewOptionsToJson(this); + + /// Parses a simplified map that comes from a URL. + /// + /// This map has CSV strings instead of JSON arrays + /// and cannot override folding parameters' defaults. factory ExampleViewOptions.fromShortMap(Map map) { return ExampleViewOptions( readOnlySectionNames: _split(map['readonly']), diff --git a/playground/frontend/playground_components/lib/src/models/example_view_options.g.dart b/playground/frontend/playground_components/lib/src/models/example_view_options.g.dart new file mode 100644 index 0000000000000..e0b8d580862b8 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/models/example_view_options.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'example_view_options.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ExampleViewOptions _$ExampleViewOptionsFromJson(Map json) => + ExampleViewOptions( + readOnlySectionNames: (json['readOnlySectionNames'] as List) + .map((e) => e as String) + .toList(), + showSectionNames: (json['showSectionNames'] as List) + .map((e) => e as String) + .toList(), + unfoldSectionNames: (json['unfoldSectionNames'] as List) + .map((e) => e as String) + .toList(), + foldCommentAtLineZero: json['foldCommentAtLineZero'] as bool? ?? true, + foldImports: json['foldImports'] as bool? ?? true, + ); + +Map _$ExampleViewOptionsToJson(ExampleViewOptions instance) => + { + 'foldCommentAtLineZero': instance.foldCommentAtLineZero, + 'foldImports': instance.foldImports, + 'readOnlySectionNames': instance.readOnlySectionNames, + 'showSectionNames': instance.showSectionNames, + 'unfoldSectionNames': instance.unfoldSectionNames, + }; diff --git a/playground/frontend/playground_components/lib/src/models/sdk.dart b/playground/frontend/playground_components/lib/src/models/sdk.dart index f05f921cb32b3..fa821059e6db4 100644 --- a/playground/frontend/playground_components/lib/src/models/sdk.dart +++ b/playground/frontend/playground_components/lib/src/models/sdk.dart @@ -27,7 +27,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'sdk.g.dart'; -@JsonSerializable(createToJson: false) +@JsonSerializable() class Sdk with EquatableMixin { final String id; final String title; @@ -117,6 +117,7 @@ class Sdk with EquatableMixin { Mode? get highlightMode => _idToHighlightMode[id]; - factory Sdk.fromJson(Map json) => - _$SdkFromJson(json); + factory Sdk.fromJson(Map json) => _$SdkFromJson(json); + + Map toJson() => _$SdkToJson(this); } diff --git a/playground/frontend/playground_components/lib/src/models/sdk.g.dart b/playground/frontend/playground_components/lib/src/models/sdk.g.dart index 63b1d0978fc67..d43ed04f63a7b 100644 --- a/playground/frontend/playground_components/lib/src/models/sdk.g.dart +++ b/playground/frontend/playground_components/lib/src/models/sdk.g.dart @@ -10,3 +10,8 @@ Sdk _$SdkFromJson(Map json) => Sdk( id: json['id'] as String, title: json['title'] as String, ); + +Map _$SdkToJson(Sdk instance) => { + 'id': instance.id, + 'title': instance.title, + }; diff --git a/playground/frontend/playground_components/lib/src/services/analytics/events/app_rated.dart b/playground/frontend/playground_components/lib/src/services/analytics/events/app_rated.dart index a8b5e36c90e8b..14461e8c8732d 100644 --- a/playground/frontend/playground_components/lib/src/services/analytics/events/app_rated.dart +++ b/playground/frontend/playground_components/lib/src/services/analytics/events/app_rated.dart @@ -25,6 +25,7 @@ class AppRatedAnalyticsEvent extends AnalyticsEventWithSnippetContext { const AppRatedAnalyticsEvent({ required this.rating, required super.snippetContext, + super.additionalParams, }) : super( name: BeamAnalyticsEvents.appRated, ); diff --git a/playground/frontend/playground_components/lib/src/services/analytics/events/feedback_form_sent.dart b/playground/frontend/playground_components/lib/src/services/analytics/events/feedback_form_sent.dart index c3ae44519b13b..65756a15d1b70 100644 --- a/playground/frontend/playground_components/lib/src/services/analytics/events/feedback_form_sent.dart +++ b/playground/frontend/playground_components/lib/src/services/analytics/events/feedback_form_sent.dart @@ -28,6 +28,7 @@ class FeedbackFormSentAnalyticsEvent extends AnalyticsEventWithSnippetContext { required this.rating, required this.text, required super.snippetContext, + super.additionalParams, }) : super( name: BeamAnalyticsEvents.feedbackFormSent, ); diff --git a/playground/frontend/playground_components/lib/src/theme/theme.dart b/playground/frontend/playground_components/lib/src/theme/theme.dart index dee3c25f25d6f..9709b21492f4f 100644 --- a/playground/frontend/playground_components/lib/src/theme/theme.dart +++ b/playground/frontend/playground_components/lib/src/theme/theme.dart @@ -162,6 +162,7 @@ class BeamThemeExtension extends ThemeExtension { final kLightTheme = ThemeData( brightness: Brightness.light, appBarTheme: _getAppBarTheme(BeamLightThemeColors.secondaryBackground), + // TODO(nausharipov): Migrate to Material 3: https://github.com/apache/beam/issues/24610 backgroundColor: BeamLightThemeColors.primaryBackground, canvasColor: BeamLightThemeColors.primaryBackground, dividerColor: BeamLightThemeColors.grey, diff --git a/playground/frontend/playground_components/lib/src/widgets/dialogs/confirm.dart b/playground/frontend/playground_components/lib/src/widgets/dialogs/confirm.dart new file mode 100644 index 0000000000000..7f5edeb3b1beb --- /dev/null +++ b/playground/frontend/playground_components/lib/src/widgets/dialogs/confirm.dart @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +import '../../../playground_components.dart'; + +class ConfirmDialog extends StatelessWidget { + final String confirmButtonText; + + final String title; + final String? subtitle; + + const ConfirmDialog({ + required this.confirmButtonText, + required this.title, + this.subtitle, + }); + + static Future show({ + required BuildContext context, + required String title, + required String confirmButtonText, + String? subtitle, + }) async { + return await showDialog( + context: context, + builder: (context) => ConfirmDialog( + confirmButtonText: confirmButtonText, + title: title, + subtitle: subtitle, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + child: OverlayBody( + child: Container( + width: BeamSizes.popupWidth, + padding: const EdgeInsets.all(BeamSizes.size16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // + Text( + title, + style: Theme.of(context).textTheme.headlineMedium, + ), + if (subtitle != null) + Padding( + padding: const EdgeInsets.only(top: BeamSizes.size8), + child: Text(subtitle!), + ), + + const SizedBox(height: BeamSizes.size8), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + Navigator.pop(context, false); + }, + child: const Text('dialogs.cancel').tr(), + ), + const SizedBox(width: BeamSizes.size8), + TextButton( + onPressed: () { + Navigator.pop(context, true); + }, + child: Text(confirmButtonText), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/solution_button.dart b/playground/frontend/playground_components/lib/src/widgets/dialogs/progress.dart similarity index 53% rename from learning/tour-of-beam/frontend/lib/pages/tour/widgets/solution_button.dart rename to playground/frontend/playground_components/lib/src/widgets/dialogs/progress.dart index 5318e6514288f..4f513a783bd43 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/solution_button.dart +++ b/playground/frontend/playground_components/lib/src/widgets/dialogs/progress.dart @@ -16,35 +16,43 @@ * limitations under the License. */ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; +import 'dart:async'; -import '../../../assets/assets.gen.dart'; -import '../state.dart'; +import 'package:flutter/material.dart'; -class SolutionButton extends StatelessWidget { - final TourNotifier tourNotifier; +class ProgressDialog extends StatelessWidget { + const ProgressDialog(); - const SolutionButton({ - required this.tourNotifier, - }); + /// Shows a dialog with [CircularProgressIndicator] until [future] completes. + static void show({ + required Future future, + required GlobalKey navigatorKey, + }) { + var shown = true; + unawaited( + showDialog( + barrierDismissible: false, + context: navigatorKey.currentContext!, + builder: (_) => const ProgressDialog(), + ).whenComplete(() { + shown = false; + }), + ); + unawaited( + future.whenComplete(() { + if (shown) { + navigatorKey.currentState!.pop(); + } + }), + ); + } @override Widget build(BuildContext context) { - return AnimatedBuilder( - animation: tourNotifier, - builder: (context, child) => TextButton.icon( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( - tourNotifier.isShowingSolution - ? Theme.of(context).splashColor - : null, - ), - ), - onPressed: tourNotifier.toggleShowingSolution, - icon: SvgPicture.asset(Assets.svg.solution), - label: const Text('ui.solution').tr(), + return const Dialog( + backgroundColor: Colors.transparent, + child: Center( + child: CircularProgressIndicator(), ), ); } diff --git a/playground/frontend/playground_components/lib/src/widgets/feedback.dart b/playground/frontend/playground_components/lib/src/widgets/feedback.dart new file mode 100644 index 0000000000000..b4d83c2ad7d7b --- /dev/null +++ b/playground/frontend/playground_components/lib/src/widgets/feedback.dart @@ -0,0 +1,223 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../../playground_components.dart'; +import '../assets/assets.gen.dart'; + +class FeedbackWidget extends StatelessWidget { + static const positiveRatingButtonKey = Key('positive'); + static const negativeRatingButtonKey = Key('negative'); + + final FeedbackController controller; + final String title; + + const FeedbackWidget({ + required this.controller, + required this.title, + }); + + void _onRatingChanged(BuildContext context, FeedbackRating rating) { + controller.rating = rating; + + PlaygroundComponents.analyticsService.sendUnawaited( + AppRatedAnalyticsEvent( + rating: rating, + snippetContext: controller.eventSnippetContext, + additionalParams: controller.additionalParams, + ), + ); + + final closeNotifier = PublicNotifier(); + showOverlay( + context: context, + closeNotifier: closeNotifier, + positioned: Positioned( + bottom: 50, + left: 20, + child: OverlayBody( + child: FeedbackDropdown( + close: closeNotifier.notifyPublic, + controller: controller, + rating: rating, + title: 'widgets.feedback.title'.tr(), + subtitle: 'widgets.feedback.hint'.tr(), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: controller, + builder: (context, child) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(width: BeamSizes.size6), + Tooltip( + message: 'widgets.feedback.positive'.tr(), + child: InkWell( + key: positiveRatingButtonKey, + onTap: () { + _onRatingChanged(context, FeedbackRating.positive); + }, + child: _RatingIcon( + groupValue: controller.rating, + value: FeedbackRating.positive, + ), + ), + ), + const SizedBox(width: BeamSizes.size6), + Tooltip( + message: 'widgets.feedback.negative'.tr(), + child: InkWell( + key: negativeRatingButtonKey, + onTap: () { + _onRatingChanged(context, FeedbackRating.negative); + }, + child: _RatingIcon( + groupValue: controller.rating, + value: FeedbackRating.negative, + ), + ), + ), + ], + ), + ); + } +} + +class _RatingIcon extends StatelessWidget { + final FeedbackRating? groupValue; + final FeedbackRating value; + const _RatingIcon({ + required this.groupValue, + required this.value, + }); + + String _getAsset() { + final isSelected = value == groupValue; + switch (value) { + case FeedbackRating.positive: + return isSelected ? Assets.svg.thumbUpFilled : Assets.svg.thumbUp; + case FeedbackRating.negative: + return isSelected ? Assets.svg.thumbDownFilled : Assets.svg.thumbDown; + } + } + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + _getAsset(), + package: PlaygroundComponents.packageName, + ); + } +} + +class FeedbackDropdown extends StatelessWidget { + static const sendButtonKey = Key('sendFeedbackButtonKey'); + static const textFieldKey = Key('feedbackTextFieldKey'); + + final FeedbackController controller; + final VoidCallback close; + final FeedbackRating rating; + final String title; + final String subtitle; + + const FeedbackDropdown({ + required this.controller, + required this.title, + required this.rating, + required this.close, + required this.subtitle, + }); + + void _sendFeedback() { + PlaygroundComponents.analyticsService.sendUnawaited( + FeedbackFormSentAnalyticsEvent( + rating: rating, + text: controller.textController.text, + snippetContext: controller.eventSnippetContext, + additionalParams: controller.additionalParams, + ), + ); + controller.textController.clear(); + close(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: controller.textController, + builder: (context, child) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.all(16), + width: 400, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: Theme.of(context).textTheme.headlineLarge, + ), + const SizedBox(height: BeamSizes.size8), + Text( + subtitle, + ), + const SizedBox(height: BeamSizes.size8), + TextField( + key: textFieldKey, + controller: controller.textController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.multiline, + maxLines: 5, + minLines: 3, + ), + const SizedBox(height: BeamSizes.size8), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + key: sendButtonKey, + onPressed: controller.textController.text.isEmpty + ? null + : _sendFeedback, + child: const Text('widgets.feedback.send').tr(), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/playground/frontend/playground_components/lib/src/widgets/overlay/opener.dart b/playground/frontend/playground_components/lib/src/widgets/overlay/opener.dart index cb4e107f5f02a..a7910261ae90c 100644 --- a/playground/frontend/playground_components/lib/src/widgets/overlay/opener.dart +++ b/playground/frontend/playground_components/lib/src/widgets/overlay/opener.dart @@ -19,21 +19,23 @@ import 'package:flutter/material.dart'; import '../../controllers/public_notifier.dart'; -import 'dismissible.dart'; +import 'widget.dart'; -void openOverlay({ +void showOverlay({ required BuildContext context, required PublicNotifier closeNotifier, required Positioned positioned, + bool barrierDismissible = true, }) { final overlay = OverlayEntry( builder: (context) { - return DismissibleOverlay( + return BeamOverlay( close: closeNotifier.notifyPublic, + isDismissible: barrierDismissible, child: positioned, ); }, ); closeNotifier.addListener(overlay.remove); - Overlay.of(context)?.insert(overlay); + Overlay.of(context).insert(overlay); } diff --git a/playground/frontend/playground_components/lib/src/widgets/overlay/dismissible.dart b/playground/frontend/playground_components/lib/src/widgets/overlay/widget.dart similarity index 74% rename from playground/frontend/playground_components/lib/src/widgets/overlay/dismissible.dart rename to playground/frontend/playground_components/lib/src/widgets/overlay/widget.dart index e32e55c56a716..b4af931aed5ba 100644 --- a/playground/frontend/playground_components/lib/src/widgets/overlay/dismissible.dart +++ b/playground/frontend/playground_components/lib/src/widgets/overlay/widget.dart @@ -18,12 +18,16 @@ import 'package:flutter/material.dart'; -class DismissibleOverlay extends StatelessWidget { +class BeamOverlay extends StatelessWidget { + static const dismissibleAreaKey = Key('overlayDismissibleAreaKey'); + final VoidCallback close; + final bool isDismissible; final Positioned child; - const DismissibleOverlay({ + const BeamOverlay({ required this.close, + required this.isDismissible, required this.child, }); @@ -31,11 +35,13 @@ class DismissibleOverlay extends StatelessWidget { Widget build(BuildContext context) { return Stack( children: [ - Positioned.fill( - child: GestureDetector( - onTap: close, + if (isDismissible) + Positioned.fill( + child: GestureDetector( + key: dismissibleAreaKey, + onTap: close, + ), ), - ), child, ], ); diff --git a/playground/frontend/playground_components/lib/src/widgets/run_or_cancel_button.dart b/playground/frontend/playground_components/lib/src/widgets/run_or_cancel_button.dart index baf8dcfd1d572..444a33e743635 100644 --- a/playground/frontend/playground_components/lib/src/widgets/run_or_cancel_button.dart +++ b/playground/frontend/playground_components/lib/src/widgets/run_or_cancel_button.dart @@ -43,7 +43,6 @@ class RunOrCancelButton extends StatelessWidget { Widget build(BuildContext context) { return RunButton( playgroundController: playgroundController, - isEnabled: !(playgroundController.selectedExample?.isMultiFile ?? false), cancelRun: () async { beforeCancel?.call(playgroundController.codeRunner); await playgroundController.codeRunner.cancelRun().catchError( diff --git a/playground/frontend/playground_components/pubspec.yaml b/playground/frontend/playground_components/pubspec.yaml index f82a0803cc33d..52a45c64de1f1 100644 --- a/playground/frontend/playground_components/pubspec.yaml +++ b/playground/frontend/playground_components/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: enum_map: ^0.2.1 equatable: ^2.0.5 flutter: { sdk: flutter } - flutter_code_editor: ^0.2.13 + flutter_code_editor: ^0.2.14 flutter_markdown: ^0.6.12 flutter_svg: ^2.0.1 fluttertoast: ^8.1.1 @@ -44,6 +44,8 @@ dependencies: google_fonts: ^4.0.3 grpc: ^3.0.2 highlight: ^0.7.0 + hive: ^2.2.3 + hive_test: ^1.0.1 http: ^0.13.5 json_annotation: ^4.7.0 keyed_collection_widgets: ^0.4.3 diff --git a/playground/frontend/playground_components/test/src/controllers/example_loaders/examples_loader_test.dart b/playground/frontend/playground_components/test/src/controllers/example_loaders/examples_loader_test.dart index 705d2961946f2..67f20cf13fb0e 100644 --- a/playground/frontend/playground_components/test/src/controllers/example_loaders/examples_loader_test.dart +++ b/playground/frontend/playground_components/test/src/controllers/example_loaders/examples_loader_test.dart @@ -95,7 +95,7 @@ void main() async { }, ); - await examplesLoader.load(descriptor); + await examplesLoader.loadIfNew(descriptor); expect(setExampleTrue, [Sdk.go.id, Sdk.python.id]); expect(setExampleFalse, []); @@ -113,7 +113,7 @@ void main() async { initialSdk: Sdk.python, ); - await examplesLoader.load(descriptor); + await examplesLoader.loadIfNew(descriptor); expect(setExampleTrue, [Sdk.python.id]); expect(setExampleFalse, [Sdk.go.id]); @@ -132,7 +132,7 @@ void main() async { ); try { - await examplesLoader.load(descriptor); + await examplesLoader.loadIfNew(descriptor); } on ExampleLoadingException catch (ex) { thrown = ex; } diff --git a/playground/frontend/playground_components/test/src/controllers/example_loaders/hive_example_loader_test.dart b/playground/frontend/playground_components/test/src/controllers/example_loaders/hive_example_loader_test.dart new file mode 100644 index 0000000000000..7975bda8d594d --- /dev/null +++ b/playground/frontend/playground_components/test/src/controllers/example_loaders/hive_example_loader_test.dart @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive/hive.dart'; +import 'package:hive_test/hive_test.dart'; +import 'package:playground_components/playground_components.dart'; +import 'package:playground_components/src/controllers/example_loaders/hive_example_loader.dart'; + +import '../../common/example_cache.mocks.dart'; + +const _example = Example( + files: [], + name: 'name', + sdk: Sdk.go, + type: ExampleType.example, + path: 'path', +); + +void main() { + test('HiveExampleLoader loads locally stored example', () async { + await setUpTestHive(); + + const descriptor = HiveExampleLoadingDescriptor( + sdk: Sdk.go, + boxName: 'boxName', + snippetId: 'snippetId', + ); + + final box = await Hive.openBox(descriptor.boxName); + await box.put(descriptor.snippetId, jsonEncode(_example.toJson())); + + final loader = HiveExampleLoader( + descriptor: descriptor, + exampleCache: MockExampleCache(), + ); + + final exampleFromHive = await loader.future; + + expect(exampleFromHive.name, _example.name); + expect(exampleFromHive.sdk.id, _example.sdk.id); + + await tearDownTestHive(); + }); +} diff --git a/playground/frontend/playground_components/test/src/controllers/playground_controller_test.dart b/playground/frontend/playground_components/test/src/controllers/playground_controller_test.dart index 44d1a3bb05eb4..696b887d7f14f 100644 --- a/playground/frontend/playground_components/test/src/controllers/playground_controller_test.dart +++ b/playground/frontend/playground_components/test/src/controllers/playground_controller_test.dart @@ -33,7 +33,7 @@ Future main() async { late PlaygroundController controller; final mockExamplesLoader = MockExamplesLoader(); - when(mockExamplesLoader.load(any)).thenAnswer((_) async => 1); + when(mockExamplesLoader.loadIfNew(any)).thenAnswer((_) async => 1); setUp(() { controller = PlaygroundController( diff --git a/playground/frontend/playground_components/test/src/controllers/playground_controller_test.mocks.dart b/playground/frontend/playground_components/test/src/controllers/playground_controller_test.mocks.dart index 6ab8b1e9dbb75..ae867f7c18d81 100644 --- a/playground/frontend/playground_components/test/src/controllers/playground_controller_test.mocks.dart +++ b/playground/frontend/playground_components/test/src/controllers/playground_controller_test.mocks.dart @@ -94,6 +94,16 @@ class MockExamplesLoader extends _i1.Mock implements _i5.ExamplesLoader { returnValueForMissingStub: null, ); @override + _i7.Future loadIfNew(_i8.ExamplesLoadingDescriptor? descriptor) => + (super.noSuchMethod( + Invocation.method( + #loadIfNew, + [descriptor], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override _i7.Future load(_i8.ExamplesLoadingDescriptor? descriptor) => (super.noSuchMethod( Invocation.method( diff --git a/playground/frontend/playground_components/test/src/models/example_loading_descriptors/hive_example_loading_descriptor_test.dart b/playground/frontend/playground_components/test/src/models/example_loading_descriptors/hive_example_loading_descriptor_test.dart new file mode 100644 index 0000000000000..fdb9863fd2dd1 --- /dev/null +++ b/playground/frontend/playground_components/test/src/models/example_loading_descriptors/hive_example_loading_descriptor_test.dart @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter_test/flutter_test.dart'; +import 'package:playground_components/src/models/example_loading_descriptors/hive_example_loading_descriptor.dart'; +import 'package:playground_components/src/models/sdk.dart'; + +import 'common.dart'; + +void main() { + group('HiveExampleLoadingDescriptor', () { + const descriptor = HiveExampleLoadingDescriptor( + boxName: 'boxName', + sdk: Sdk.go, + snippetId: 'snippetId', + viewOptions: viewOptions, + ); + + test('toJson -> tryParse', () { + final map = descriptor.toJson(); + final parsed = HiveExampleLoadingDescriptor.tryParse(map); + + expect(parsed, descriptor); + }); + + test('copyWithoutViewOptions', () { + expect( + descriptor.copyWithoutViewOptions(), + HiveExampleLoadingDescriptor( + boxName: descriptor.boxName, + sdk: descriptor.sdk, + snippetId: descriptor.snippetId, + ), + ); + }); + }); +} diff --git a/playground/frontend/pubspec.lock b/playground/frontend/pubspec.lock index dc3febfcbbd2d..2891b4dc33098 100644 --- a/playground/frontend/pubspec.lock +++ b/playground/frontend/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201" + sha256: a36ec4843dc30ea6bf652bf25e3448db6c5e8bcf4aa55f063a5d1dad216d8214 url: "https://pub.dev" source: hosted - version: "52.0.0" + version: "58.0.0" akvelon_flutter_issue_106664_workaround: dependency: "direct main" description: @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: analyzer - sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4 + sha256: cc4242565347e98424ce9945c819c192ec0838cb9d1f6aa4a97cc96becbc5b27 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.10.0" app_state: dependency: "direct main" description: @@ -101,18 +101,18 @@ packages: dependency: transitive description: name: build_daemon - sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "7c35a3a7868626257d8aee47b51c26b9dba11eaddf3431117ed2744951416aab" + sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" build_runner: dependency: "direct dev" description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: built_value - sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + sha256: "31b7c748fd4b9adf8d25d72a4c4a59ef119f12876cf414f94f8af5131d5fa2b0" url: "https://pub.dev" source: hosted - version: "8.4.3" + version: "8.4.4" characters: dependency: transitive description: @@ -285,10 +285,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + sha256: "6d691edde054969f0e0f26abb1b30834b5138b963793e56f69d3a9a4435e6352" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.0" dbus: dependency: transitive description: @@ -394,10 +394,10 @@ packages: dependency: "direct dev" description: name: flutter_code_editor - sha256: "580ead0408e27f44776ed9b12c02993f6f4fcedfbf25f8fe5ebb4625c03c8c20" + sha256: "85d0d159ce4f33f3aabedea92c0bdc1ad05c6931bba80ac46660f90c7ab6255c" url: "https://pub.dev" source: hosted - version: "0.2.13" + version: "0.2.14" flutter_driver: dependency: transitive description: flutter @@ -444,10 +444,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: b9be7260c1fdbe0090a11d9d356fc2c88e14cf33407fc0c1829d76ab13808035 + sha256: f991fdb1533c3caeee0cdc14b04f50f0c3916f0dbcbc05237ccbe4e3c6b93f3f url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.5" flutter_test: dependency: "direct dev" description: flutter @@ -462,10 +462,10 @@ packages: dependency: transitive description: name: fluttertoast - sha256: "774fa28b07f3a82c93596bc137be33189fec578ed3447a93a5a11c93435de394" + sha256: "2f9c4d3f4836421f7067a28f8939814597b27614e021da9d63e5d3fb6e212d25" url: "https://pub.dev" source: hosted - version: "8.1.3" + version: "8.2.1" frontend_server_client: dependency: transitive description: @@ -507,10 +507,10 @@ packages: dependency: transitive description: name: googleapis_auth - sha256: "127b1bbd32170ab8312f503bd57f1d654d8e4039ddfbc63c027d3f7ade0eff74" + sha256: f59be210ede1e22718962e4ef48a08ae783eb67fe19b0f4c16b204e20df0869c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" graphs: dependency: transitive description: @@ -543,14 +543,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.3" + hive_test: + dependency: transitive + description: + name: hive_test + sha256: dd7a5cf0be7af288566a96180b5d07574023777aa947ef252b69046ec36d8eb2 + url: "https://pub.dev" + source: hosted + version: "1.0.1" html: dependency: transitive description: name: html - sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269 + sha256: "79d498e6d6761925a34ee5ea8fa6dfef38607781d2fa91e37523474282af55cb" url: "https://pub.dev" source: hosted - version: "0.15.1" + version: "0.15.2" http: dependency: transitive description: @@ -656,10 +664,10 @@ packages: dependency: transitive description: name: markdown - sha256: "4ed544d2ce84975b2ab5cbd4268f2d31f47858553ae2295c92fdf5d6e431a927" + sha256: d95a9d12954aafc97f984ca29baaa7690ed4d9ec4140a23ad40580bcdb6c87f5 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.2" matcher: dependency: transitive description: @@ -696,10 +704,10 @@ packages: dependency: "direct dev" description: name: mockito - sha256: "2a8a17b82b1bde04d514e75d90d634a0ac23f6cb4991f6098009dd56836aeafe" + sha256: dd61809f04da1838a680926de50a9e87385c1de91c6579629c3d1723946e8059 url: "https://pub.dev" source: hosted - version: "5.3.2" + version: "5.4.0" mocktail: dependency: transitive description: @@ -728,10 +736,10 @@ packages: dependency: transitive description: name: node_preamble - sha256: "8ebdbaa3b96d5285d068f80772390d27c21e1fa10fb2df6627b1b9415043608d" + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" onmessage: dependency: "direct main" description: @@ -776,50 +784,50 @@ packages: dependency: transitive description: name: path_provider - sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4 url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.0.14" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + sha256: da97262be945a72270513700a92b39dd2f4a54dad55d061687e2e37a6390366a url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.0.25" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.2" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: "2e32f1640f07caef0d3cb993680f181c79e54a3827b997d5ee221490d131fbd9" + sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" url: "https://pub.dev" source: hosted - version: "2.1.8" + version: "2.1.10" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.6" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.5" petitparser: dependency: transitive description: @@ -854,10 +862,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pool: dependency: transitive description: @@ -902,10 +910,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" rxdart: dependency: transitive description: @@ -926,58 +934,58 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "5949029e70abe87f75cfe59d17bf5c397619c4b74a099b10116baeb34786fad9" + sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.1.0" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "955e9736a12ba776bdd261cf030232b30eadfcd9c79b32a3250dd4a494e8c8f7" + sha256: "7fa90471a6875d26ad78c7e4a675874b2043874586891128dc5899662c97db46" url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.1.2" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "2b55c18636a4edc529fa5cd44c03d3f3100c00513f518c5127c951978efcccd0" + sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874 + sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3 + sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: a4b5bc37fe1b368bbc81f953197d55e12f49d0296e7e412dfe2d2d77d6929958 + sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.1.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9" + sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.0" shelf: dependency: transitive description: @@ -1035,10 +1043,10 @@ packages: dependency: transitive description: name: source_maps - sha256: "490098075234dcedb83c5d949b4c93dad5e6b7702748de000be2b57b8e6b2427" + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" url: "https://pub.dev" source: hosted - version: "0.10.11" + version: "0.10.12" source_span: dependency: transitive description: @@ -1155,18 +1163,18 @@ packages: dependency: transitive description: name: universal_html - sha256: "5ff50b7c14d201421cf5230ec389a0591c4deb5c817c9d7ccca3b26fe5f31e34" + sha256: ed4f24120c9b1b4721d44e439f7a47d09d9f1b7b868bc84c9d6d373a4a8732af url: "https://pub.dev" source: hosted - version: "2.0.8" + version: "2.2.1" universal_io: dependency: transitive description: name: universal_io - sha256: "79f78ddad839ee3aae3ec7c01eb4575faf0d5c860f8e5223bc9f9c17f7f03cef" + sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d" url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.2.0" url_launcher: dependency: "direct main" description: @@ -1179,58 +1187,58 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1" + sha256: a52628068d282d01a07cd86e6ba99e497aa45ce8c91159015b2416907d78e411 url: "https://pub.dev" source: hosted - version: "6.0.23" + version: "6.0.27" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815" + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.1.4" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc" + sha256: "206fb8334a700ef7754d6a9ed119e7349bc830448098f21a69bf1b4ed038cabc" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.4" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094" + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.5" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6" + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0" + sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" url: "https://pub.dev" source: hosted - version: "2.0.14" + version: "2.0.16" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615 + sha256: a83ba3607a507758669cfafb03f9de09bf6e6280c14d9b9cb18f013e406dcacd url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" url_strategy: dependency: "direct main" description: @@ -1243,26 +1251,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "09562ef5f47aa84f6567495adb6b9cb2a3192b82c352623b8bd00b300d62603b" + sha256: ea8d3fc7b2e0f35de38a7465063ecfcf03d8217f7962aa2a6717132cb5d43a79 url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.5" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "886e57742644ebed024dc3ade29712e37eea1b03d294fb314c0a3386243fe5a6" + sha256: a5eaa5d19e123ad4f61c3718ca1ed921c4e6254238d9145f82aa214955d9aced url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.5" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "5d9010c4a292766c55395b2288532579a85673f8148460d1e233d98ffe10d24e" + sha256: "15edc42f7eaa478ce854eaf1fbb9062a899c0e4e56e775dd73b7f4709c97c4ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.5" vector_math: dependency: transitive description: @@ -1307,10 +1315,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" webdriver: dependency: transitive description: @@ -1331,10 +1339,10 @@ packages: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" xdg_directories: dependency: transitive description: diff --git a/playground/frontend/test/pages/playground/states/example_selector_state_test.mocks.dart b/playground/frontend/test/pages/playground/states/example_selector_state_test.mocks.dart index d341e3c0566cd..60b85e0fa4b83 100644 --- a/playground/frontend/test/pages/playground/states/example_selector_state_test.mocks.dart +++ b/playground/frontend/test/pages/playground/states/example_selector_state_test.mocks.dart @@ -64,6 +64,16 @@ class MockExamplesLoader extends _i1.Mock implements _i3.ExamplesLoader { returnValueForMissingStub: null, ); @override + _i5.Future loadIfNew(_i6.ExamplesLoadingDescriptor? descriptor) => + (super.noSuchMethod( + Invocation.method( + #loadIfNew, + [descriptor], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override _i5.Future load(_i6.ExamplesLoadingDescriptor? descriptor) => (super.noSuchMethod( Invocation.method(