update recipe viewer

This commit is contained in:
Kenta420 2024-02-27 13:43:26 +07:00
parent cf5b11b267
commit a12121fca6
19 changed files with 677 additions and 363 deletions

View file

@ -7,7 +7,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import UploadPage from './pages/upload'
import { type MenuList } from './components/sidebar'
import { FileTextIcon, RocketIcon, UploadIcon } from '@radix-ui/react-icons'
import RecipesTablePage from './pages/recipes/recipe-table'
import RecipePage from './pages/recipes/recipe'
const sideBarMenuList: MenuList = [
{
@ -39,7 +39,7 @@ function router() {
},
{
path: 'recipes',
element: <RecipesTablePage />
element: <RecipePage />
},
{
path: 'android',

View file

@ -1,16 +1,33 @@
<svg width="136" height="102" viewBox="0 0 136 102" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.4261 38.3364V65.1097L12.4745 64.2196L51.5736 50.3544L39.4004 35.8294H14.3981L12.4261 38.3364Z" fill="#513C2F"/>
<path d="M55.2988 49.0333L68.1106 44.4899L80.4911 48.958L92.8475 34.2L79.9953 17.86H55.7355L42.8833 34.2L55.2988 49.0333Z" fill="#513C2F"/>
<path d="M84.2021 50.2973L122.827 64.2368V37.7289L121.333 35.8294H96.3304L84.2021 50.2973Z" fill="#513C2F"/>
<path d="M122.306 31.3333C119.887 16.9571 109.272 5.34644 95.4069 1.42597C95.5207 1.45815 95.6343 1.49091 95.7477 1.52414L83.8292 16.677L96.3304 32.5707H121.333L122.306 31.3333Z" fill="#513C2F"/>
<path d="M91.3327 0.514628C89.2959 0.176112 87.2039 0 85.0707 0H50.1821C47.9895 0 45.8406 0.186103 43.7502 0.543322C43.8166 0.531938 43.8831 0.520785 43.9496 0.509749L55.2144 14.8314H80.0718L91.3327 0.514628Z" fill="#513C2F"/>
<path d="M39.7766 1.44572C31.3983 3.83206 24.2116 9.02795 19.3103 15.9444L19.117 16.1782L19.1464 16.1777C16.1371 20.493 14.0124 25.466 13.0338 30.8363L14.3981 32.5707H39.4004L51.8292 16.7691L39.7766 1.44572Z" fill="#513C2F"/>
<path d="M83.0358 85.7588C85.6669 83.2025 87.6539 79.9968 88.7326 76.4033H77.0626L67.6264 69.3867L58.1902 76.4033H46.5203C47.5989 79.9968 49.5859 83.2025 52.2171 85.7588H83.0358Z" fill="#513C2F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M89.4214 73.2848C89.5683 72.2663 89.6443 71.2251 89.6443 70.1663C89.6443 58.1103 79.7865 48.3368 67.6264 48.3368C55.4663 48.3368 45.6085 58.1103 45.6085 70.1663C45.6085 71.2251 45.6846 72.2663 45.8315 73.2848H56.6174L67.6264 65.4885L78.6354 73.2848H89.4214ZM78.6354 67.0478C80.3725 67.0478 81.7807 65.6516 81.7807 63.9293C81.7807 62.2071 80.3725 60.8108 78.6354 60.8108C76.8982 60.8108 75.49 62.2071 75.49 63.9293C75.49 65.6516 76.8982 67.0478 78.6354 67.0478ZM59.7628 63.9293C59.7628 65.6516 58.3546 67.0478 56.6174 67.0478C54.8803 67.0478 53.4721 65.6516 53.4721 63.9293C53.4721 62.2071 54.8803 60.8108 56.6174 60.8108C58.3546 60.8108 59.7628 62.2071 59.7628 63.9293Z" fill="#513C2F"/>
<path d="M88.1113 101.351L100.693 88.8774H69.1992V101.351H88.1113Z" fill="#513C2F"/>
<path d="M66.0537 88.8774H35.2509L47.8326 101.351H66.0537V88.8774Z" fill="#513C2F"/>
<path d="M92.5596 101.351H104.526C109.061 101.351 113.375 99.4104 116.362 96.0266L122.671 88.8774H105.141L92.5596 101.351Z" fill="#513C2F"/>
<path d="M30.8027 88.8774H12.5817L19.0344 96.0986C22.0193 99.439 26.3049 101.351 30.8062 101.351H43.3844L30.8027 88.8774Z" fill="#513C2F"/>
<path d="M31.4541 66.4402H3.14537C2.2768 66.4402 1.57274 67.1383 1.57274 67.9995C1.57274 68.8606 2.2768 69.5587 3.14537 69.5587H6.29085V71.118H3.14537C1.40822 71.118 0 72.1651 0 73.4569C0 74.7486 1.40822 75.7957 3.14537 75.7957H6.29085V77.355H3.14537C1.40822 77.355 0 78.4021 0 79.6939C0 80.9856 1.40822 82.0327 3.14537 82.0327H6.29085V83.592H3.14537C2.2768 83.592 1.57274 84.2901 1.57274 85.1512C1.57274 86.0124 2.2768 86.7105 3.14537 86.7105H31.4541V86.681C31.593 86.6914 31.7327 86.6991 31.8731 86.7041C31.9951 86.7083 32.1175 86.7105 32.2405 86.7105C37.8863 86.7105 42.4631 82.1728 42.4631 76.5754C42.4631 70.9779 37.8863 66.4402 32.2405 66.4402C32.1849 66.4402 32.1295 66.4406 32.0742 66.4415C32.025 66.4423 31.9758 66.4435 31.9268 66.4449L31.86 66.4471L31.8096 66.4491C31.7471 66.4516 31.6849 66.4548 31.6229 66.4584C31.5665 66.4618 31.5103 66.4655 31.4541 66.4698V66.4402Z" fill="#513C2F"/>
<path d="M132.107 87.3181H103.799V87.2885C103.539 87.3081 103.277 87.3181 103.012 87.3181C97.3665 87.3181 92.7897 82.7804 92.7897 77.1829C92.7897 71.5854 97.3665 67.0478 103.012 67.0478C103.277 67.0478 103.539 67.0578 103.799 67.0773V67.0478H132.107C132.976 67.0478 133.68 67.7459 133.68 68.607C133.68 69.4682 132.976 70.1663 132.107 70.1663H128.962V71.7255H132.107C133.845 71.7255 135.253 72.7727 135.253 74.0644C135.253 75.3562 133.845 76.4033 132.107 76.4033H128.962V77.9626H132.107C133.845 77.9626 135.253 79.0097 135.253 80.3014C135.253 81.5932 133.845 82.6403 132.107 82.6403H128.962V84.1996H132.107C132.976 84.1996 133.68 84.8977 133.68 85.7588C133.68 86.62 132.976 87.3181 132.107 87.3181Z" fill="#513C2F"/>
<svg width="200" height="225" viewBox="0 0 200 225" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.5" y="1.5" width="197" height="222" rx="8.5" fill="#EAE6E1" stroke="#965A44" stroke-width="3"/>
<path d="M28.0574 170.034V142.006H22.6416V137.638H38.1546V142.006H32.7388V170.034H28.0574Z" fill="#513C2F"/>
<path d="M54.5095 158.704L52.2147 146.965H52.1229L49.8281 158.704H54.5095ZM42.9436 170.034L50.2411 137.638H54.1423L61.4399 170.034H56.7584L55.3815 163.072H49.0019L47.625 170.034H42.9436Z" fill="#513C2F"/>
<path d="M70.8132 145.282C70.8132 143.977 71.0426 142.825 71.5016 141.824C71.9606 140.823 72.5725 139.989 73.3375 139.321C74.0718 138.684 74.8979 138.199 75.8159 137.865C76.7644 137.532 77.7129 137.365 78.6614 137.365C79.61 137.365 80.5432 137.532 81.4611 137.865C82.4097 138.199 83.2664 138.684 84.0313 139.321C84.7657 139.989 85.3623 140.823 85.8213 141.824C86.2803 142.825 86.5097 143.977 86.5097 145.282V162.39C86.5097 163.755 86.2803 164.923 85.8213 165.893C85.3623 166.864 84.7657 167.668 84.0313 168.305C83.2664 168.972 82.4097 169.473 81.4611 169.806C80.5432 170.14 79.61 170.307 78.6614 170.307C77.7129 170.307 76.7644 170.14 75.8159 169.806C74.8979 169.473 74.0718 168.972 73.3375 168.305C72.5725 167.668 71.9606 166.864 71.5016 165.893C71.0426 164.923 70.8132 163.755 70.8132 162.39V145.282ZM75.4946 162.39C75.4946 163.512 75.8006 164.346 76.4125 164.892C77.0551 165.408 77.8047 165.666 78.6614 165.666C79.5182 165.666 80.2525 165.408 80.8645 164.892C81.507 164.346 81.8283 163.512 81.8283 162.39V145.282C81.8283 144.159 81.507 143.34 80.8645 142.825C80.2525 142.279 79.5182 142.006 78.6614 142.006C77.8047 142.006 77.0551 142.279 76.4125 142.825C75.8006 143.34 75.4946 144.159 75.4946 145.282V162.39Z" fill="#513C2F"/>
<path d="M113.833 170.034V137.638H120.672C122.171 137.638 123.441 137.865 124.481 138.32C125.552 138.775 126.424 139.382 127.097 140.14C127.77 140.899 128.245 141.778 128.52 142.779C128.826 143.75 128.979 144.766 128.979 145.828V147.011C128.979 147.89 128.903 148.634 128.75 149.24C128.627 149.847 128.428 150.378 128.153 150.833C127.633 151.682 126.837 152.41 125.766 153.017C126.868 153.532 127.679 154.291 128.199 155.292C128.719 156.293 128.979 157.658 128.979 159.387V161.207C128.979 164.058 128.275 166.242 126.868 167.759C125.491 169.275 123.273 170.034 120.213 170.034H113.833ZM118.515 155.019V165.393H120.534C121.483 165.393 122.217 165.256 122.737 164.983C123.288 164.71 123.701 164.331 123.976 163.846C124.252 163.36 124.42 162.784 124.481 162.117C124.542 161.449 124.573 160.721 124.573 159.933C124.573 159.114 124.527 158.401 124.435 157.794C124.343 157.188 124.16 156.672 123.885 156.247C123.579 155.823 123.165 155.519 122.645 155.337C122.125 155.125 121.437 155.019 120.58 155.019H118.515ZM118.515 142.006V150.924H120.626C122.186 150.924 123.227 150.545 123.747 149.786C124.298 148.998 124.573 147.86 124.573 146.374C124.573 144.918 124.267 143.826 123.655 143.098C123.074 142.37 122.003 142.006 120.442 142.006H118.515Z" fill="#513C2F"/>
<path d="M140.856 170.034V137.638H145.538V170.034H140.856Z" fill="#513C2F"/>
<path d="M157.246 170.034V137.638H161.744L168.812 157.157H168.904V137.638H173.585V170.034H169.179L162.019 150.56H161.927V170.034H157.246Z" fill="#513C2F"/>
<path d="M54.3364 196.092H59.4943C60.4575 196.092 61.2612 195.777 61.9052 195.148C62.5548 194.546 62.8852 193.728 62.8964 192.694C62.8964 192.071 62.7368 191.493 62.4176 190.963C62.076 190.448 61.572 190.107 60.9055 189.937V189.904C61.2584 189.74 61.5552 189.557 61.796 189.354C62.0368 189.163 62.2216 188.96 62.3504 188.747C62.6024 188.298 62.7228 187.828 62.7116 187.335C62.7116 186.373 62.4064 185.598 61.796 185.013C61.1912 184.433 60.2867 184.138 59.0827 184.127H54.3364V196.092ZM59.0491 190.782C59.7379 190.793 60.2419 190.971 60.5611 191.316C60.8803 191.666 61.04 192.087 61.04 192.579C61.04 193.061 60.8803 193.477 60.5611 193.827C60.2419 194.182 59.7379 194.366 59.0491 194.377H56.1929V190.782H59.0491ZM58.8726 185.743C59.5503 185.754 60.0487 185.916 60.3679 186.228C60.6927 186.561 60.8551 186.969 60.8551 187.45C60.8551 187.932 60.6927 188.331 60.3679 188.649C60.0487 188.993 59.5503 189.166 58.8726 189.166H56.1929V185.743H58.8726Z" fill="#513C2F"/>
<path d="M66.2133 196.092H74.034V194.377H68.0697V190.864H73.1604V189.256H68.0697V185.842H74.034V184.127H66.2133V196.092Z" fill="#513C2F"/>
<path d="M79.9298 196.092H81.3746L85.4236 184.127H83.458L80.669 193.236H80.6354L77.8549 184.127H75.8892L79.9298 196.092Z" fill="#513C2F"/>
<path d="M88.1188 196.092H95.9396V194.377H89.9753V190.864H95.066V189.256H89.9753V185.842H95.9396V184.127H88.1188V196.092Z" fill="#513C2F"/>
<path d="M100.928 185.743H103.835C104.428 185.743 104.882 185.866 105.196 186.113C105.593 186.392 105.798 186.843 105.809 187.467C105.809 187.987 105.638 188.424 105.296 188.78C104.949 189.163 104.428 189.36 103.734 189.371H100.928V185.743ZM99.0716 196.092H100.928V190.979H103.297L105.826 196.092H108.035L105.196 190.782C106.752 190.197 107.542 189.092 107.564 187.467C107.531 186.362 107.125 185.511 106.346 184.915C105.702 184.389 104.871 184.127 103.851 184.127H99.0716V196.092Z" fill="#513C2F"/>
<path d="M113.116 191.808L114.888 186.58H114.922L116.694 191.808H113.116ZM118.173 196.092H120.13L115.678 184.127H114.124L109.672 196.092H111.637L112.578 193.417H117.224L118.173 196.092Z" fill="#513C2F"/>
<path d="M126.479 191.217H128.974V191.841C128.963 192.59 128.722 193.195 128.252 193.655C127.782 194.136 127.185 194.377 126.463 194.377C126.026 194.377 125.651 194.286 125.337 194.106C125.018 193.953 124.76 193.753 124.564 193.507C124.323 193.25 124.164 192.913 124.085 192.497C123.996 192.082 123.951 191.286 123.951 190.109C123.951 188.933 123.996 188.131 124.085 187.705C124.164 187.3 124.323 186.969 124.564 186.712C124.76 186.466 125.018 186.26 125.337 186.096C125.651 185.938 126.026 185.853 126.463 185.842C127.056 185.853 127.56 186.02 127.975 186.342C128.372 186.682 128.647 187.092 128.798 187.573H130.764C130.568 186.572 130.097 185.732 129.352 185.054C128.608 184.381 127.644 184.039 126.463 184.028C125.499 184.039 124.696 184.263 124.052 184.701C123.397 185.133 122.92 185.626 122.624 186.178C122.439 186.468 122.302 186.849 122.212 187.319C122.128 187.79 122.086 188.72 122.086 190.109C122.086 191.477 122.128 192.402 122.212 192.883C122.257 193.14 122.313 193.354 122.38 193.523C122.453 193.687 122.534 193.86 122.624 194.04C122.92 194.593 123.397 195.08 124.052 195.501C124.696 195.939 125.499 196.168 126.463 196.19C127.712 196.168 128.748 195.755 129.571 194.951C130.388 194.141 130.808 193.138 130.831 191.939V189.502H126.479V191.217Z" fill="#513C2F"/>
<path d="M134.383 196.092H142.204V194.377H136.239V190.864H141.33V189.256H136.239V185.842H142.204V184.127H134.383V196.092Z" fill="#513C2F"/>
<path d="M154.128 192.136C155.341 192.136 156.325 191.159 156.325 189.953C156.325 188.748 155.341 187.77 154.128 187.77C152.915 187.77 151.932 188.748 151.932 189.953C151.932 191.159 152.915 192.136 154.128 192.136Z" fill="#513C2F"/>
<path d="M42.4115 192.136C43.6247 192.136 44.6082 191.159 44.6082 189.953C44.6082 188.748 43.6247 187.77 42.4115 187.77C41.1983 187.77 40.2148 188.748 40.2148 189.953C40.2148 191.159 41.1983 192.136 42.4115 192.136Z" fill="#513C2F"/>
<path d="M42.9129 55.8364V82.6097L42.9613 81.7196L82.0604 67.8544L69.8872 53.3294H44.8849L42.9129 55.8364Z" fill="#513C2F"/>
<path d="M85.7856 66.5333L98.5975 61.9899L110.978 66.458L123.334 51.7L110.482 35.36H86.2223L73.3701 51.7L85.7856 66.5333Z" fill="#513C2F"/>
<path d="M114.689 67.7973L153.314 81.7368V55.2289L151.82 53.3294H126.817L114.689 67.7973Z" fill="#513C2F"/>
<path d="M152.793 48.8333C150.374 34.4571 139.758 22.8464 125.894 18.926C126.008 18.9582 126.121 18.9909 126.234 19.0241L114.316 34.177L126.817 50.0707H151.82L152.793 48.8333Z" fill="#513C2F"/>
<path d="M121.82 18.0146C119.783 17.6761 117.691 17.5 115.557 17.5H80.669C78.4763 17.5 76.3274 17.6861 74.237 18.0433C74.3034 18.0319 74.3699 18.0208 74.4364 18.0097L85.7012 32.3314H110.559L121.82 18.0146Z" fill="#513C2F"/>
<path d="M70.2635 18.9457C61.8851 21.3321 54.6985 26.528 49.7972 33.4444L49.6038 33.6782L49.6332 33.6777C46.6239 37.993 44.4993 42.966 43.5207 48.3363L44.8849 50.0707H69.8872L82.316 34.2691L70.2635 18.9457Z" fill="#513C2F"/>
<path d="M113.523 103.259C116.154 100.703 118.141 97.4968 119.219 93.9033H107.549L98.1132 86.8867L88.677 93.9033H77.0072C78.0858 97.4968 80.0727 100.703 82.7039 103.259H113.523Z" fill="#513C2F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M119.908 90.7848C120.055 89.7663 120.131 88.7251 120.131 87.6663C120.131 75.6103 110.273 65.8368 98.1132 65.8368C85.9531 65.8368 76.0953 75.6103 76.0953 87.6663C76.0953 88.7251 76.1714 89.7663 76.3183 90.7848H87.1043L98.1132 82.9885L109.122 90.7848H119.908ZM109.122 84.5478C110.859 84.5478 112.268 83.1516 112.268 81.4293C112.268 79.7071 110.859 78.3108 109.122 78.3108C107.385 78.3108 105.977 79.7071 105.977 81.4293C105.977 83.1516 107.385 84.5478 109.122 84.5478ZM90.2496 81.4293C90.2496 83.1516 88.8414 84.5478 87.1043 84.5478C85.3671 84.5478 83.9589 83.1516 83.9589 81.4293C83.9589 79.7071 85.3671 78.3108 87.1043 78.3108C88.8414 78.3108 90.2496 79.7071 90.2496 81.4293Z" fill="#513C2F"/>
<path d="M118.598 118.851L131.18 106.377H99.686V118.851H118.598Z" fill="#513C2F"/>
<path d="M96.5405 106.377H65.7378L78.3195 118.851H96.5405V106.377Z" fill="#513C2F"/>
<path d="M123.046 118.851H135.013C139.548 118.851 143.862 116.91 146.848 113.527L153.158 106.377H135.628L123.046 118.851Z" fill="#513C2F"/>
<path d="M61.2895 106.377H43.0685L49.5212 113.599C52.5061 116.939 56.7917 118.851 61.293 118.851H73.8712L61.2895 106.377Z" fill="#513C2F"/>
<path d="M61.941 83.9402H33.6322C32.7636 83.9402 32.0596 84.6383 32.0596 85.4995C32.0596 86.3606 32.7636 87.0587 33.6322 87.0587H36.7777V88.618H33.6322C31.895 88.618 30.4868 89.6651 30.4868 90.9569C30.4868 92.2486 31.895 93.2957 33.6322 93.2957H36.7777V94.855H33.6322C31.895 94.855 30.4868 95.9021 30.4868 97.1939C30.4868 98.4856 31.895 99.5327 33.6322 99.5327H36.7777V101.092H33.6322C32.7636 101.092 32.0596 101.79 32.0596 102.651C32.0596 103.512 32.7636 104.21 33.6322 104.21H61.941V104.181C62.0798 104.191 62.2195 104.199 62.36 104.204C62.4819 104.208 62.6043 104.21 62.7273 104.21C68.3731 104.21 72.9499 99.6728 72.9499 94.0754C72.9499 88.4779 68.3731 83.9402 62.7273 83.9402C62.6717 83.9402 62.6163 83.9406 62.561 83.9415C62.5118 83.9423 62.4626 83.9435 62.4136 83.9449L62.3468 83.9471L62.2964 83.9491C62.2339 83.9516 62.1718 83.9548 62.1097 83.9584C62.0533 83.9618 61.9971 83.9655 61.941 83.9698V83.9402Z" fill="#513C2F"/>
<path d="M162.594 104.818H134.285V104.789C134.026 104.808 133.764 104.818 133.499 104.818C127.853 104.818 123.277 100.28 123.277 94.6829C123.277 89.0854 127.853 84.5478 133.499 84.5478C133.764 84.5478 134.026 84.5578 134.285 84.5773V84.5478H162.594C163.463 84.5478 164.167 85.2459 164.167 86.107C164.167 86.9682 163.463 87.6663 162.594 87.6663H159.449V89.2255H162.594C164.331 89.2255 165.74 90.2727 165.74 91.5644C165.74 92.8562 164.331 93.9033 162.594 93.9033H159.449V95.4626H162.594C164.331 95.4626 165.74 96.5097 165.74 97.8014C165.74 99.0932 164.331 100.14 162.594 100.14H159.449V101.7H162.594C163.463 101.7 164.167 102.398 164.167 103.259C164.167 104.12 163.463 104.818 162.594 104.818Z" fill="#513C2F"/>
</svg>

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

@ -13,7 +13,7 @@ interface SideBarProps {
const Sidebar: React.FC<SideBarProps> = ({ menuList }) => {
return (
<aside className="fixed top-0 left-0 z-40 w-64 pt-20 h-screen">
<aside className="fixed top-0 left-0 z-40 w-64 pt-[90px] h-screen">
<div className="h-full px-3 pb-4 overflow-y-auto bg-zinc-700">
<ul className="space-y-2 font-medium">
{menuList.map((item, index) => (

View file

@ -0,0 +1,343 @@
import { useEffect, useMemo, useState } from 'react'
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from './ui/dialog'
import { Button } from './ui/button'
import { CaretSortIcon, CheckIcon, PlusCircledIcon } from '@radix-ui/react-icons'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator
} from './ui/command'
import { cn } from '@/lib/utils'
import { Label } from './ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'
import useAdb from '@/hooks/useAdb'
import { useShallow } from 'zustand/react/shallow'
import type { AdbDaemonWebUsbConnection } from '@yume-chan/adb-daemon-webusb'
import { ADB_DEFAULT_DEVICE_FILTER, AdbDaemonWebUsbDevice } from '@yume-chan/adb-daemon-webusb'
import { Adb, AdbDaemonTransport } from '@yume-chan/adb'
import { toast } from './ui/use-toast'
import AdbWebCredentialStore from '@yume-chan/adb-credential-web'
type PopoverTriggerProps = React.ComponentPropsWithoutRef<typeof PopoverTrigger>
interface TeamSwitcherProps extends PopoverTriggerProps {}
const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
const [open, setOpen] = useState(false)
const [showNewDeviceDialog, setShowNewDeviceDialog] = useState(false)
const [connectedDevices, setConnectedDevices] = useState<AdbDaemonWebUsbDevice[]>([])
const [newConnectionState, setNewConnectionState] = useState<'connection' | 'connecting'>('connection')
const [selectedConnectionType, setSelectedConnectionType] = useState<string | undefined>()
const { manager, device, adb, setDevice, setAdb } = useAdb(
useShallow(state => ({
manager: state.manager,
device: state.device,
adb: state.adb,
setDevice: state.setDevice,
setAdb: state.setAdb
}))
)
useEffect(() => {
if (open) {
const getDevices = async () => {
const devices = await manager?.getDevices()
console.log(devices)
setConnectedDevices(devices || [])
}
getDevices()
}
}, [open, manager])
async function connectDeviceAdbUsb(device: AdbDaemonWebUsbDevice) {
let connection: AdbDaemonWebUsbConnection | undefined
try {
connection = await device.connect()
} catch (e) {
toast({
duration: 5000,
variant: 'destructive',
title: 'Failed to connect to device',
description: (e as Error).message
})
return
}
if (connection) {
const credentialStore: AdbWebCredentialStore = new AdbWebCredentialStore()
const transport = await AdbDaemonTransport.authenticate({
serial: device.serial,
connection: connection,
credentialStore: credentialStore
})
const adb = new Adb(transport)
setAdb(adb)
setDevice(device)
}
}
async function createNewUsbConnection() {
let selectedDevice: AdbDaemonWebUsbDevice | undefined = undefined
if (!device) {
console.log('no device')
selectedDevice = await manager?.requestDevice({
filters: [
{
...ADB_DEFAULT_DEVICE_FILTER,
serialNumber: 'd'
}
]
})
if (!selectedDevice) {
return
} else {
setDevice(selectedDevice)
}
} else {
selectedDevice = device
}
// create transport and connect to device
let adb: Adb | null = null
let connection
try {
if (selectedDevice instanceof AdbDaemonWebUsbDevice) {
connection = await selectedDevice.connect()
}
} catch (e) {
toast({
duration: 5000,
variant: 'destructive',
title: 'Failed to connect to device',
description: (e as Error).message
})
return
}
if (connection) {
const credentialStore: AdbWebCredentialStore = new AdbWebCredentialStore()
const transport = await AdbDaemonTransport.authenticate({
serial: selectedDevice.serial,
connection: connection,
credentialStore: credentialStore
})
adb = new Adb(transport)
setAdb(adb)
setNewConnectionState('connection')
setShowNewDeviceDialog(false)
}
}
// async function connectAdbDaemon() {
// if (!window.electronRuntime) {
// toast({
// duration: 5000,
// variant: 'destructive',
// title: 'Failed to connect to adb daemon',
// description: 'This feature is only available in the desktop app'
// })
// return
// }
// // create connection
// await window.ipcRenderer.invoke('adb')
// const result = await window.ipcRenderer.invoke('adb:shell', 'ls')
// console.log(result)
// }
function onDisconnect() {
device?.raw.forget()
setDevice(undefined)
adb?.close()
setAdb(undefined)
}
const selectConnectionTypeDialog = useMemo(() => {
return (
<DialogContent>
<DialogHeader>
<DialogTitle>Connect New Device</DialogTitle>
<DialogDescription>Connect a new device to manage your recipes or control your devices. </DialogDescription>
</DialogHeader>
<div>
<div className="space-y-4 py-2 pb-4">
<div className="space-y-2">
<Label htmlFor="plan">Connection type</Label>
<Select onValueChange={setSelectedConnectionType}>
<SelectTrigger>
<SelectValue placeholder="Select connection type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="usb">
<span className="font-medium">USB</span> -{' '}
<span className="text-muted-foreground">Connect device via USB</span>
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowNewDeviceDialog(false)}>
Cancel
</Button>
<Button type="submit" onClick={() => setNewConnectionState('connecting')}>
Continue
</Button>
</DialogFooter>
</DialogContent>
)
}, [selectedConnectionType])
const connectingDialog = useMemo(() => {
return (
<DialogContent>
<DialogHeader>
<DialogTitle>Connect New Device</DialogTitle>
<DialogDescription>Connect a new device to manage your recipes or control your devices. </DialogDescription>
</DialogHeader>
<div>
<div className="space-y-4 py-2 pb-4">
<div className="space-y-2">
{/* <Label htmlFor="plan">Connection type</Label>
<Select onValueChange={setSelectedConnectionType}>
<SelectTrigger>
<SelectValue placeholder="Select connection type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="usb">
<span className="font-medium">USB</span> -{' '}
<span className="text-muted-foreground">Connect device via USB</span>
</SelectItem>
</SelectContent>
</Select> */}
<span className="text-sm">Please connect your device via USB</span>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setNewConnectionState('connection')}>
Back
</Button>
<Button
type="submit"
onClick={() => {
createNewUsbConnection()
}}
>
Connect
</Button>
</DialogFooter>
</DialogContent>
)
}, [selectedConnectionType])
return (
<Dialog open={showNewDeviceDialog} onOpenChange={setShowNewDeviceDialog}>
<div className="flex space-x-5 justify-center items-center">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
aria-label="Select a Device"
className={cn('w-[400px] justify-between', className)}
>
{device ? device.name : 'Select a Device'}
<CaretSortIcon className="ml-auto h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[400px] p-0">
<Command>
<CommandList>
<CommandInput placeholder="Search device..." />
<CommandEmpty>No device found.</CommandEmpty>
<CommandGroup heading={'Devices'}>
{connectedDevices.length > 0 ? (
connectedDevices.map(device => (
<CommandItem
key={device.serial}
onSelect={() => {
connectDeviceAdbUsb(device)
setOpen(false)
}}
className="text-sm"
>
{device.name}
<CheckIcon
className={cn(
'ml-auto h-4 w-4',
device?.serial === device.serial ? 'opacity-100' : 'opacity-0'
)}
/>
</CommandItem>
))
) : (
<span className="text-sm ml-2">Not found device connected.</span>
)}
</CommandGroup>
</CommandList>
<CommandSeparator />
<CommandList>
<CommandGroup>
<DialogTrigger asChild>
<CommandItem
onSelect={() => {
setOpen(false)
setShowNewDeviceDialog(true)
}}
>
<PlusCircledIcon className="mr-2 h-5 w-5" />
Connect New Devices
</CommandItem>
</DialogTrigger>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{device && (
<Button
variant="destructive"
onClick={() => {
onDisconnect()
}}
>
Disconnect
</Button>
)}
</div>
{newConnectionState === 'connection'
? selectConnectionTypeDialog
: newConnectionState === 'connecting'
? connectingDialog
: null}
</Dialog>
)
}
export default DeviceSwitcher

View file

@ -15,6 +15,7 @@ import { Link } from 'react-router-dom'
import userAuthStore from '@/hooks/userAuth'
import { Button } from './ui/button'
import Logo from '@/assets/tao_logo.svg'
import DeviceSwitcher from './device-switcher'
const DropdownMenuUser: React.FC = () => {
return (
@ -71,8 +72,11 @@ const Navbar: React.FC = () => {
<nav className="flex justify-center items-center fixed top-0 z-50 w-full py-5 bg-zinc-700">
<div className="flex justify-between items-center mx-auto w-full max-w-screen-2xl px-6 xs:px-8 sm:px-16">
<Link to="/">
<img src={Logo} alt="logo" width={40} height={40} />
<img src={Logo} alt="logo" width={45} height={51} />
</Link>
<div className="flex justify-center items-center">
<DeviceSwitcher />
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center ms-3">
{userInfo ? (

View file

@ -15,16 +15,22 @@ interface materialDashboard {
interface RecipeDashboardHook {
selectedRecipe: string
setSelectedRecipe: (recipe: string) => void
selectedMaterial: number
setSelectedMaterial: (material: number) => void
getRecipesDashboard: (filter?: RecipeDashboardFilterQuery) => Promise<RecipeDashboard[] | []>
getMaterials: (filter?: RecipeDashboardFilterQuery) => Promise<materialDashboard[] | []>
getRecipe: (productCode: string) => Promise<RecipeDashboard | null>
}
const useRecipeDashboard = create<RecipeDashboardHook>(() => ({
selectedRecipe: '',
selectedMaterial: 0,
setSelectedRecipe: id => {
useRecipeDashboard.setState({ selectedRecipe: id })
},
selectedRecipe: '',
setSelectedMaterial: id => {
useRecipeDashboard.setState({ selectedMaterial: id })
},
async getRecipesDashboard(filter) {
return taoAxios
.get<RecipeDashboard[]>('/v2/recipes/dashboard', {

View file

@ -1,28 +1,12 @@
import { ToolBar } from './components/tool-bar'
import { ScrcpyTab } from './components/scrcpy-tab'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Toaster } from '@/components/ui/toaster'
import { ShellTab } from './components/shell-tab'
import useAdb from '@/hooks/useAdb'
import { useShallow } from 'zustand/react/shallow'
import { FileManagerTab } from './components/file-manager-tab'
const AndroidPage: React.FC = () => {
const { manager, device, adb, setDevice, setAdb } = useAdb(
useShallow(state => ({
manager: state.manager,
device: state.device,
adb: state.adb,
setDevice: state.setDevice,
setAdb: state.setAdb
}))
)
return (
<div className="flex flex-col w-full">
<div className="flex w-full py-5">
<ToolBar manager={manager} device={device} adb={adb} setAdb={setAdb} setDevice={setDevice} />
</div>
<Tabs defaultValue="scrcpy" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="scrcpy">Scrcpy</TabsTrigger>
@ -30,10 +14,10 @@ const AndroidPage: React.FC = () => {
<TabsTrigger value="file-manager">File Manager</TabsTrigger>
</TabsList>
<TabsContent value="scrcpy">
<ScrcpyTab adb={adb} />
<ScrcpyTab />
</TabsContent>
<TabsContent value="shell">
<ShellTab adb={adb} />
<ShellTab />
</TabsContent>
<TabsContent value="file-manager">
<FileManagerTab />

View file

@ -1,16 +1,14 @@
import { Button } from '@/components/ui/button'
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'
import useScrcpy from '@/hooks/scrcpy-android'
import { type Adb } from '@yume-chan/adb'
import useAdb from '@/hooks/useAdb'
import { memo, useEffect, useRef } from 'react'
import 'xterm/css/xterm.css'
import { useShallow } from 'zustand/react/shallow'
interface ScrcpyTabProps {
adb: Adb | undefined
}
export const ScrcpyTab: React.FC = memo(() => {
const adb = useAdb(state => state.adb)
export const ScrcpyTab: React.FC<ScrcpyTabProps> = memo(({ adb }) => {
const scrcpyScreenRef = useRef<HTMLDivElement>(null)
const { scrcpyClient, decoder, connectScrcpy, onHomeClick, onBackClick, disconnectScrcpy } = useScrcpy(

View file

@ -1,7 +1,7 @@
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import useShellAndroid from '@/hooks/shell-android'
import { type Adb } from '@yume-chan/adb'
import useAdb from '@/hooks/useAdb'
import { memo, useEffect, useRef } from 'react'
import { type Terminal } from 'xterm'
@ -10,11 +10,9 @@ import { FitAddon } from 'xterm-addon-fit'
import 'xterm/css/xterm.css'
import { useShallow } from 'zustand/react/shallow'
interface ShellTabProps {
adb: Adb | undefined
}
export const ShellTab: React.FC = memo(() => {
const adb = useAdb(state => state.adb)
export const ShellTab: React.FC<ShellTabProps> = memo(({ adb }) => {
const { terminal, startTerminal, killTerminal } = useShellAndroid(
useShallow(state => ({
terminal: state.terminal,

View file

@ -1,190 +0,0 @@
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from '@/components/ui/dialog'
import { toast } from '@/components/ui/use-toast'
import { Adb, AdbDaemonTransport } from '@yume-chan/adb'
import AdbWebCredentialStore from '@yume-chan/adb-credential-web'
import {
ADB_DEFAULT_DEVICE_FILTER,
AdbDaemonWebUsbDevice,
type AdbDaemonWebUsbDeviceManager
} from '@yume-chan/adb-daemon-webusb'
import { useState } from 'react'
interface ToolBarProps {
manager: AdbDaemonWebUsbDeviceManager | undefined
device: AdbDaemonWebUsbDevice | undefined
adb: Adb | undefined
setDevice: (device: AdbDaemonWebUsbDevice | undefined) => void
setAdb: (adb: Adb | undefined) => void
}
export const ToolBar: React.FC<ToolBarProps> = ({ manager, adb, device, setAdb, setDevice }) => {
const [name, setName] = useState<string>('')
const [resolution, setResolution] = useState<string>('')
const [version, setVersion] = useState<string>('')
async function createNewConnection() {
console.log(device)
let selectedDevice: AdbDaemonWebUsbDevice | undefined = undefined
if (!device) {
console.log('no device')
selectedDevice = await manager?.requestDevice({
filters: [
{
...ADB_DEFAULT_DEVICE_FILTER,
serialNumber: 'd'
}
]
})
if (!selectedDevice) {
return
} else {
setDevice(selectedDevice)
}
} else {
selectedDevice = device
}
// create transport and connect to device
let adb: Adb | null = null
let connection
try {
if (selectedDevice instanceof AdbDaemonWebUsbDevice) {
connection = await selectedDevice.connect()
}
} catch (e) {
toast({
duration: 5000,
variant: 'destructive',
title: 'Failed to connect to device',
description: (e as Error).message
})
return
}
if (connection) {
const credentialStore: AdbWebCredentialStore = new AdbWebCredentialStore()
const transport = await AdbDaemonTransport.authenticate({
serial: selectedDevice.serial,
connection: connection,
credentialStore: credentialStore
})
adb = new Adb(transport)
}
if (adb) {
const name = await adb.getProp('ro.product.model')
const version = await adb.getProp('ro.build.version.release')
setName(name)
setResolution(resolution)
setVersion(version)
setAdb(adb)
}
}
async function connectAdbDaemon() {
if (!window.electronRuntime) {
toast({
duration: 5000,
variant: 'destructive',
title: 'Failed to connect to adb daemon',
description: 'This feature is only available in the desktop app'
})
return
}
// create connection
await window.ipcRenderer.invoke('adb')
const result = await window.ipcRenderer.invoke('adb:shell', 'ls')
console.log(result)
}
function onDisconnect() {
device?.raw.forget()
setDevice(undefined)
adb?.close()
setAdb(undefined)
}
function onTerminate() {
adb?.close()
setAdb(undefined)
}
return (
<div className="flex justify-between items-center w-full p-4 shadow-lg rounded-lg">
{adb ? (
<div className="flex flex-col justify-center items-start">
<ul className="list-disc pl-4">
<li>Name: {name}</li>
<li>Version: {version}</li>
</ul>
</div>
) : (
<div className="flex flex-col justify-center items-start">
<h2>No Device Connected</h2>
</div>
)}
<div className="flex items-center space-x-4">
{adb ? (
<DisconnectConfirmDialog onDisconnect={onDisconnect} onTerminate={onTerminate} />
) : (
<Button variant={'default'} onClick={createNewConnection}>
Connect
</Button>
)}
<Button variant={'default'} onClick={connectAdbDaemon}>
Connect Adb Daemon
</Button>
</div>
</div>
)
}
interface DisconnectConfirmDialogProps {
onDisconnect: () => void
onTerminate: () => void
}
const DisconnectConfirmDialog: React.FC<DisconnectConfirmDialogProps> = ({ onDisconnect, onTerminate }) => {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant={'destructive'}>Disconnect</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Disconnect Device</DialogTitle>
<DialogDescription>
Do you want to also declaim device? if so press Disconnect else press Terminate
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="destructive" onClick={onDisconnect}>
Disconnect
</Button>
<Button variant="secondary" onClick={onTerminate}>
Terminate
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View file

@ -1,53 +0,0 @@
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import useAndroidSwitcher from '@/hooks/android-switcher'
import { cn } from '@/lib/utils'
import { useShallow } from 'zustand/react/shallow'
interface AndroidSwitcherProps {
isCollapsed?: boolean
androids: {
label: string
deviceName: string
serial: string
icon: React.ReactNode
}[]
}
const AndroidSwitcher: React.FC<AndroidSwitcherProps> = ({ androids, isCollapsed }) => {
const { selectedAndroid, setSelectedAndroid } = useAndroidSwitcher(
useShallow(state => ({
selectedAndroid: state.selectedAndroid,
setSelectedAndroid: state.setSelectedAndroid
}))
)
return (
<Select defaultValue={selectedAndroid} onValueChange={setSelectedAndroid}>
<SelectTrigger
className={cn(
'flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0',
isCollapsed && 'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden'
)}
aria-label="Select android"
>
<SelectValue placeholder="Select an android">
{androids.find(android => android.serial === selectedAndroid)?.icon}
<span className={cn('ml-2', isCollapsed && 'hidden')}>
{androids.find(android => android.serial === selectedAndroid)?.label}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
{androids.map(android => (
<SelectItem key={android.serial} value={android.serial}>
<div className="flex items-center gap-3 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0 [&_svg]:text-foreground">
{android.icon}
{android.deviceName}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
)
}
export default AndroidSwitcher

View file

@ -0,0 +1,58 @@
import { ScrollArea } from '@/components/ui/scroll-area'
import useRecipeDashboard from '@/hooks/recipe-dashboard'
import { cn } from '@/lib/utils'
import { type MaterialSetting } from '@/models/recipe/schema'
import { useShallow } from 'zustand/react/shallow'
interface MaterialListProps {
items: MaterialSetting[]
}
const MaterialList: React.FC<MaterialListProps> = ({ items }) => {
const { selectedMaterial, setSelectedMaterial } = useRecipeDashboard(
useShallow(state => ({
selectedMaterial: state.selectedMaterial,
setSelectedMaterial: state.setSelectedMaterial
}))
)
return (
<ScrollArea className="h-screen">
<div className="flex flex-col gap-2 p-4 pt-0">
{items.map(item => (
<button
key={item.id}
className={cn(
'flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent',
selectedMaterial === item.id && 'bg-muted'
)}
onClick={() => setSelectedMaterial(item.id)}
>
<div className="flex w-full flex-col gap-1">
<div className="flex items-center">
<div className="flex items-center gap-2">
<div className="font-semibold">
{item.id}: {item.materialName}
</div>
</div>
</div>
<div className="text-xs font-medium">{`${item.materialId}: ${item.materialName}`}</div>
</div>
{/* <div className="line-clamp-2 text-xs text-muted-foreground">{item.substring(0, 300)}</div>
{item.labels.length ? (
<div className="flex items-center gap-2">
{item.labels.map(label => (
<Badge key={label} variant={getBadgeVariantFromLabel(label)}>
{label}
</Badge>
))}
</div>
) : null} */}
</button>
))}
</div>
</ScrollArea>
)
}
export default MaterialList

View file

@ -143,15 +143,35 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
</div>
<Separator />
<div className="flex-1 whitespace-pre-wrap p-4 text-sm">
{/* show name and productCode of recipe */}
{recipe && (
<div>
<div className="font-semibold">{recipe.name}</div>
<div className="text-xs">{recipe.productCode}</div>
<div className="font-semibold">Product Code: {recipe.productCode}</div>
<div>
<span className="font-semibold">Name:</span> {recipe.name}
</div>
<div>
<span className="font-semibold">Other Name:</span> {recipe.otherName}
</div>
<div>
<span className="font-semibold">Description:</span> {recipe.Description}
</div>
<div>
<span className="font-semibold">Other Description:</span> {recipe.otherDescription}
</div>
<div>
{
// list all recipes
recipe.recipes
.filter(r => r.isUse)
.map((recipe, index) => (
<div key={index}>
<span className="font-semibold">Recipe {index + 1}:</span> {recipe.materialPathId}
</div>
))
}
</div>
</div>
)}
{/* show recipe description */}
{recipe && <div className="mt-4">{recipe.Description}</div>}
</div>
<Separator className="mt-auto" />
<div className="p-4">

View file

@ -4,14 +4,17 @@ import { Separator } from '@/components/ui/separator'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { TooltipProvider } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { type Recipe01, type Recipes } from '@/models/recipe/schema'
import { memo, useMemo, useState } from 'react'
import AndroidSwitcher from './android-switcher'
import { Search, CupSoda, Wheat, Dessert, Cherry, WineOff } from 'lucide-react'
import { type MaterialSetting, type Recipe01, type Recipes } from '@/models/recipe/schema'
import { memo, useEffect, useMemo, useState } from 'react'
import { Search, CupSoda, Wheat, Dessert, Cherry, WineOff, Server, Loader2 } from 'lucide-react'
import Nav from './nav'
import RecipeList from './recipe-list'
import { isBefore, isToday } from 'date-fns'
import { format, isBefore, isToday } from 'date-fns'
import RecipeDisplay from './recipe-display'
import MaterialList from './material-list'
import { Button } from '@/components/ui/button'
import { useMutation } from '@tanstack/react-query'
import taoAxios from '@/lib/taoAxios'
interface RecipeMenuProps {
recipes: Recipes
@ -20,13 +23,40 @@ interface RecipeMenuProps {
isDevBranch: boolean
}
const RecipeMenu: React.FC<RecipeMenuProps> = memo(({ recipes, recipe01, defaultSize, isDevBranch }) => {
recipe01 = useMemo(() => {
return recipe01.sort((a, b) => (a.LastChange && b.LastChange && isBefore(a.LastChange, b.LastChange) ? 1 : -1))
}, [recipe01])
const [recipeList, setRecipeList] = useState<Recipe01[]>(recipe01)
const sortedRecipe01 = useMemo(() => {
return recipeList.sort((a, b) => (a.LastChange && b.LastChange && isBefore(a.LastChange, b.LastChange) ? 1 : -1))
}, [recipeList])
const { isPending, isSuccess, isError, mutate } = useMutation({
mutationFn: async () => {
return taoAxios.post('/v2/recipes/', recipes, {
params: {
country_id: 'tha'
}
})
}
})
const [search, setSearch] = useState('')
useEffect(() => {
if (search) {
const recipesFiltered = recipe01.filter(
item =>
item.productCode.toLowerCase().includes(search.toLowerCase()) ||
(item.name && item.name.toLowerCase().includes(search.toLowerCase()))
)
setRecipeList(recipesFiltered)
} else {
setRecipeList(recipe01)
}
}, [search])
return (
<>
<ResizablePanel defaultSize={defaultSize} minSize={30}>
<ResizablePanel id="recipe-panel" defaultSize={defaultSize} minSize={30}>
<Tabs defaultValue="all">
<div className="flex items-center px-4 py-2">
<h1 className="text-xl font-bold">
@ -43,18 +73,37 @@ const RecipeMenu: React.FC<RecipeMenuProps> = memo(({ recipes, recipe01, default
</div>
<Separator />
<div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="pb-3 flex justify-end items-end">
<Button className="bg-primary text-white" onClick={() => mutate()}>
{isPending ? (
<div className="flex items-center gap-2">
<Loader2 size={20} className="animate-spin" />
<span>Updating...</span>
</div>
) : isSuccess ? (
'Updated'
) : isError ? (
'Error'
) : (
<div className="flex items-center gap-2">
<Server size={20} />
Up Recipe to Server
</div>
)}
</Button>
</div>
<form>
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input placeholder="Search" className="pl-8" />
<Input placeholder="Search" className="pl-8" value={search} onChange={e => setSearch(e.target.value)} />
</div>
</form>
</div>
<TabsContent value="all" className="m-0">
<RecipeList items={recipe01} />
<RecipeList items={sortedRecipe01} />
</TabsContent>
<TabsContent value="today" className="m-0">
<RecipeList items={recipe01.filter(item => item.LastChange && isToday(item.LastChange))} />
<RecipeList items={sortedRecipe01.filter(item => item.LastChange && isToday(item.LastChange))} />
</TabsContent>
</Tabs>
</ResizablePanel>
@ -66,13 +115,61 @@ const RecipeMenu: React.FC<RecipeMenuProps> = memo(({ recipes, recipe01, default
)
})
interface MaterialsProps {
recipes: Recipes
defaultSize?: number
isDevBranch: boolean
}
const Materials: React.FC<MaterialsProps> = memo(({ recipes, defaultSize, isDevBranch }) => {
const [materialSettingList, setMaterialSettingList] = useState<MaterialSetting[]>(recipes.MaterialSetting)
const sortedMaterialSettingList = useMemo(() => {
return materialSettingList.sort((a, b) => (a.id < b.id ? 1 : -1))
}, [materialSettingList])
const [search, setSearch] = useState('')
useEffect(() => {
if (search) {
const materialSettingsFiltered = recipes.MaterialSetting.filter(
item =>
item.materialName.toLowerCase().includes(search.toLowerCase()) ||
item.id.toString().includes(search.toLowerCase())
)
setMaterialSettingList(materialSettingsFiltered)
} else {
setMaterialSettingList(recipes.MaterialSetting)
}
}, [search])
return (
<>
<ResizablePanel id="material-panel" defaultSize={defaultSize} minSize={30}>
<div className="flex items-center px-4 py-2">
<h1 className="text-xl font-bold">
Recipe Version: {recipes.MachineSetting.configNumber} {isDevBranch ? '(Dev)' : ''}
</h1>
</div>
<Separator />
<div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<form>
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input placeholder="Search" className="pl-8" value={search} onChange={e => setSearch(e.target.value)} />
</div>
</form>
</div>
<MaterialList items={sortedMaterialSettingList} />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={defaultSize}>
<RecipeDisplay recipes={recipes} />
</ResizablePanel>
</>
)
})
interface RecipeEditorProps {
androids: {
label: string
deviceName: string
serial: string
icon: React.ReactNode
}[]
isDevBranch: boolean
recipes: Recipes
defaultLayout: number[] | undefined
@ -81,7 +178,6 @@ interface RecipeEditorProps {
}
export const RecipeEditor: React.FC<RecipeEditorProps> = ({
androids,
recipes,
isDevBranch,
defaultLayout = [265, 440, 655],
@ -106,7 +202,7 @@ export const RecipeEditor: React.FC<RecipeEditorProps> = ({
onLayout={(sizes: number[]) => {
document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}`
}}
className="h-full max-h-[800px] items-stretch"
className="h-full max-h-[900px] items-stretch"
>
<ResizablePanel
defaultSize={defaultLayout[0]}
@ -125,7 +221,9 @@ export const RecipeEditor: React.FC<RecipeEditorProps> = ({
className={cn(isCollapsed && 'min-w-[50px] transition-all duration-300 ease-in-out')}
>
<div className={cn('flex h-[52px] items-center justify-center', isCollapsed ? 'h-[52px]' : 'px-2')}>
<AndroidSwitcher isCollapsed={isCollapsed} androids={androids} />
<span className={cn('text-muted-foreground text-xs ', isCollapsed && 'hidden')}>
TimeStamp: {format(recipes.Timestamp, 'dd-MM-yyyy HH:mm:ss')}
</span>
</div>
<Separator />
<Nav
@ -189,6 +287,8 @@ export const RecipeEditor: React.FC<RecipeEditorProps> = ({
defaultSize={defaultLayout[1]}
isDevBranch={isDevBranch}
/>
) : showListIndex === 2 ? (
<Materials recipes={recipes} defaultSize={defaultLayout[1]} isDevBranch={isDevBranch} />
) : null}
</ResizablePanelGroup>
</TooltipProvider>

View file

@ -32,7 +32,7 @@ const RecipeList: React.FC<RecipeListProps> = ({ items }) => {
<div className="flex w-full flex-col gap-1">
<div className="flex items-center">
<div className="flex items-center gap-2">
<div className="font-semibold">{item.name}</div>
<div className="text-md">{item.name}</div>
{item.LastChange && isToday(item.LastChange) && (
<span className="flex h-2 w-2 rounded-full bg-blue-600" />
)}
@ -49,7 +49,9 @@ const RecipeList: React.FC<RecipeListProps> = ({ items }) => {
})}
</div>
</div>
<div className="text-xs font-medium">{`${item.productCode}: ${item.name}`}</div>
<div className="text-xs">
{item.productCode}: {item.name || 'No Name'}
</div>
</div>
{/* <div className="line-clamp-2 text-xs text-muted-foreground">{item.substring(0, 300)}</div>
{item.labels.length ? (

View file

@ -1,12 +0,0 @@
import RecipeForm from './components/recipe-edit-components/recipe-form'
const RecipeEditPage: React.FC = () => {
return (
<div>
<h1>Edit Recipe</h1>
<RecipeForm />
</div>
)
}
export default RecipeEditPage

View file

@ -1,36 +1,13 @@
import useLocalStorage from '@/hooks/localStorage'
import RecipeEditor from './components/recipe-editor-components/recipe-editor'
import { useShallow } from 'zustand/react/shallow'
import { Smartphone } from 'lucide-react'
import { type Recipes } from '@/models/recipe/schema'
import useFileManager from '@/hooks/filemanager-android'
import { useCallback, useEffect, useState } from 'react'
import useAdb from '@/hooks/useAdb'
import { isBefore } from 'date-fns'
const androids: {
label: string
deviceName: string
serial: string
icon: React.ReactNode
}[] = [
{
label: 'Test Device',
deviceName: 'Test Device',
serial: 'Test Device',
icon: <Smartphone />
}
]
const RecipesTablePage = () => {
// const recipeQuery = useLocalStorage(state => state.recipeQuery)
// const getRecipesDashboard = useRecipeDashboard(state => state.getRecipesDashboard)
// const { data: recipeDashboardList, isLoading } = useQuery({
// queryKey: ['recipe-overview'],
// queryFn: () => getRecipesDashboard(recipeQuery)
// })
const { layout, collapsed } = useLocalStorage(
useShallow(state => ({
layout: state.layout,
@ -77,18 +54,9 @@ const RecipesTablePage = () => {
}, [readData])
return (
// <div className="flex w-full flex-col gap-3">
// <section>
// <h1 className="text-3xl font-bold text-gray-900">Recipes</h1>
// </section>
// <section>
// <DataTable data={recipeDashboardList ?? []} columns={columns} isLoading={isLoading} />
// </section>
// </div>
<>
{recipes ? (
<RecipeEditor
androids={androids}
defaultLayout={layout}
navCollapsedSize={4}
recipes={recipes}

View file

@ -0,0 +1,20 @@
import useAdb from '@/hooks/useAdb'
import RecipesTablePage from './recipe-table'
const RecipePage: React.FC = () => {
const adb = useAdb(state => state.adb)
return (
<div>
{adb ? (
<RecipesTablePage />
) : (
<div className="flex w-full h-96 justify-center items-center">
<div className="text-2xl font-bold">Please connect to the device</div>
</div>
)}
</div>
)
}
export default RecipePage

View file

@ -2,10 +2,15 @@ package v2
import (
"encoding/json"
"fmt"
"net/http"
"os"
"path"
"recipe-manager/data"
"recipe-manager/models"
modelsV2 "recipe-manager/models/v2"
"recipe-manager/services/logger"
"time"
"github.com/go-chi/chi/v5"
)
@ -65,5 +70,51 @@ func (rr *recipeRouter) Route(r chi.Router) {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
var recipe models.Recipe
if err := json.NewDecoder(r.Body).Decode(&recipe); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
countryID := r.URL.Query().Get("country_id")
filename := fmt.Sprintf("coffeethai02_%d_%s.json", recipe.MachineSetting.ConfigNumber, time.Now().Format("2-1-2006_15-04-05"))
// TODO: validate recipe data
// recipeID, err := rr.data.CreateRecipe(recipe)
// if err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
// return
// }
// write file to disk
recipePath := path.Join("./cofffeemachineConfig", countryID, filename)
_, err := os.Stat(recipePath)
if os.IsNotExist(err) {
file, err := os.Create(recipePath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
if err := encoder.Encode(recipe); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
} else {
http.Error(w, "File already exists", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
})
})
}