fix: 更新诸多bug
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 412 KiB |
|
Before Width: | Height: | Size: 138 KiB |
@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" fill="none" viewBox="0 0 44 44">
|
|
||||||
<path fill="#1797FF" d="M38.565 18.206c0-6.948-4.156-12.93-10.115-15.585a17.032 17.032 0 0 0-6.991-1.487c-9.445 0-17.106 7.657-17.106 17.106 0 4.168 1.49 7.984 3.966 10.953.038.052.077.103.12.15l.047.052c.086.099.168.193.258.292l.004-.004L20.075 42.07c.125.137.27.249.425.34.71.476 1.68.373 2.278-.276l11.038-12.078.005.004c.223-.232.442-.473.653-.722l.004-.004c.03-.03.056-.065.082-.099a17.044 17.044 0 0 0 4.009-11.004v-.013c-.005-.005-.005-.009-.005-.013Z" opacity=".2"/>
|
|
||||||
<path fill="#1797FF" d="M21.5 6C14.6 6 9 11.6 9 18.5S14.6 31 21.5 31 34 25.4 34 18.5 28.4 6 21.5 6Z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 685 B |
@ -1,13 +0,0 @@
|
|||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="Frame" clip-path="url(#clip0_81_10597)">
|
|
||||||
<g id="Frame_2">
|
|
||||||
<path id="Vector" d="M17.3125 16.75H2.6875C1.75712 16.75 1 15.9929 1 15.0625V4.9375C1 4.00712 1.75712 3.25 2.6875 3.25H17.3125C18.2429 3.25 19 4.00712 19 4.9375V15.0625C19 15.9929 18.2429 16.75 17.3125 16.75ZM2.6875 4.375C2.53832 4.375 2.39524 4.43426 2.28975 4.53975C2.18426 4.64524 2.125 4.78832 2.125 4.9375V15.0625C2.125 15.2117 2.18426 15.3548 2.28975 15.4602C2.39524 15.5657 2.53832 15.625 2.6875 15.625H17.3125C17.6219 15.625 17.875 15.3719 17.875 15.0625V4.9375C17.875 4.78832 17.8157 4.64524 17.7102 4.53975C17.6048 4.43426 17.4617 4.375 17.3125 4.375H2.6875Z" fill="#ABABAB"/>
|
|
||||||
<path id="Vector_2" d="M10 10.7964C9.88906 10.7964 9.7806 10.7635 9.68837 10.7019L4.06337 6.95224C3.99954 6.91238 3.94435 6.86013 3.90107 6.79856C3.85779 6.73699 3.82731 6.66736 3.81143 6.5938C3.79555 6.52023 3.7946 6.44423 3.80862 6.37029C3.82264 6.29635 3.85136 6.22598 3.89307 6.16334C3.93478 6.1007 3.98863 6.04706 4.05144 6.00561C4.11426 5.96416 4.18475 5.93574 4.25875 5.92202C4.33274 5.9083 4.40874 5.90957 4.48223 5.92576C4.55573 5.94194 4.62523 5.97271 4.68662 6.01624L10 9.55774L15.3134 6.01624C15.4375 5.93875 15.5869 5.91267 15.73 5.94355C15.873 5.97443 15.9984 6.05983 16.0795 6.18162C16.1606 6.30342 16.191 6.45203 16.1643 6.59591C16.1377 6.73978 16.056 6.8676 15.9366 6.95224L10.3116 10.7019C10.2196 10.764 10.111 10.797 10 10.7964Z" fill="#ABABAB"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_81_10597">
|
|
||||||
<rect width="20" height="20" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,9 +0,0 @@
|
|||||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="Close-one (关闭)">
|
|
||||||
<g id="Close-one (关闭)_2">
|
|
||||||
<path id="Vector" d="M9 16.5C13.1421 16.5 16.5 13.1421 16.5 9C16.5 4.85786 13.1421 1.5 9 1.5C4.85786 1.5 1.5 4.85786 1.5 9C1.5 13.1421 4.85786 16.5 9 16.5Z" fill="#CFCFCF" stroke="#CFCFCF" stroke-width="1.125" stroke-linejoin="round"/>
|
|
||||||
<path id="Vector_2" d="M11.1213 6.87891L6.87866 11.1215" stroke="white" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path id="Vector_3" d="M6.87866 6.87891L11.1213 11.1215" stroke="white" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 751 B |
@ -1,10 +0,0 @@
|
|||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="Frame" clip-path="url(#clip0_246_5226)">
|
|
||||||
<path id="Vector" d="M9.99884 17.9997C7.59311 17.9997 3.11719 14.4141 3.11719 11.0227V4.54952C3.11719 4.39249 3.24869 4.26563 3.41152 4.26234L3.80139 4.25576C3.81628 4.25576 5.34461 4.22502 6.92013 3.60115C8.53703 2.96298 9.57919 2.22714 9.59021 2.2196L9.81976 2.05677C9.87194 2.02017 9.93413 2.00052 9.99787 2.00049C10.0612 2.00008 10.123 2.01942 10.1748 2.0558L10.4088 2.21999C10.4194 2.22753 11.4626 2.96259 13.0775 3.60154C14.6546 4.2254 16.1829 4.25615 16.1986 4.25615L16.5854 4.26273C16.7484 4.26601 16.8795 4.39288 16.8795 4.54991L16.8828 11.0231C16.8828 14.4141 12.4061 18.0001 9.99825 18.0001L9.99884 17.9997ZM15.8211 5.30334C15.2295 5.25886 13.9849 5.10608 12.6919 4.59419C11.3707 4.07205 10.3993 3.48473 9.99884 3.2254C9.59949 3.48377 8.62753 4.07127 7.30671 4.59419C6.01605 5.10492 4.77489 5.2577 4.1752 5.30334V11.0227C4.1752 13.6824 8.07484 16.9334 9.99884 16.9334C10.7445 16.9334 12.1976 16.3283 13.626 14.9995C15.0029 13.7197 15.8248 12.2328 15.8248 11.0231L15.8211 5.30334ZM9.44691 12.1105C9.39833 12.1603 9.34025 12.1999 9.2761 12.2269C9.21196 12.2538 9.14306 12.2677 9.07348 12.2676C9.00371 12.2675 8.93464 12.2537 8.87027 12.2267C8.8059 12.1998 8.74752 12.1604 8.69851 12.1107L7.10984 10.5118C7.01064 10.4114 6.95501 10.2759 6.95501 10.1347C6.95501 9.99353 7.01064 9.85804 7.10984 9.7576C7.15877 9.70799 7.21708 9.6686 7.28137 9.64171C7.34566 9.61482 7.41465 9.60097 7.48433 9.60097C7.55402 9.60097 7.62301 9.61482 7.68729 9.64171C7.75158 9.6686 7.80989 9.70799 7.85882 9.7576L9.07348 10.9792L12.1402 7.89026C12.189 7.84057 12.2473 7.8011 12.3116 7.77416C12.3758 7.74721 12.4448 7.73334 12.5145 7.73334C12.5842 7.73334 12.6532 7.74721 12.7174 7.77416C12.7817 7.8011 12.8399 7.84057 12.8888 7.89026C12.9886 7.99039 13.0447 8.12599 13.0447 8.26737C13.0447 8.40874 12.9886 8.54435 12.8888 8.64447L9.44691 12.1105Z" fill="#ABABAB"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_246_5226">
|
|
||||||
<rect width="20" height="20" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB |
@ -1,10 +0,0 @@
|
|||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="Frame" clip-path="url(#clip0_81_10600)">
|
|
||||||
<path id="Vector" d="M5.54545 7.6V6.2C5.54545 3.8802 7.53939 2 10 2C12.4606 2 14.4545 3.8792 14.4545 6.2V7.6H14.6669C15.9549 7.6 17 8.5862 17 9.8V15.8C17 17.0154 15.9566 18 14.6669 18H5.33333C4.04512 18 3 17.0138 3 15.8V9.8C3 8.5846 4.04342 7.6 5.33312 7.6H5.54545ZM6.81818 7.6H13.1818V6.2C13.1818 4.542 11.7578 3.2 10 3.2C8.24236 3.2 6.81818 4.543 6.81818 6.2V7.6ZM4.27273 9.8V15.8C4.27273 16.3512 4.7483 16.8 5.33312 16.8H14.6667C14.806 16.8002 14.944 16.7744 15.0727 16.7242C15.2015 16.674 15.3185 16.6003 15.417 16.5075C15.5155 16.4146 15.5936 16.3043 15.6469 16.1829C15.7001 16.0615 15.7274 15.9314 15.7273 15.8V9.8C15.7273 9.2488 15.2517 8.8 14.6669 8.8H5.33333C5.19401 8.79984 5.05601 8.8256 4.92726 8.8758C4.7985 8.926 4.68152 8.99965 4.583 9.09254C4.48448 9.18543 4.40636 9.29573 4.35312 9.41713C4.29988 9.53853 4.27256 9.66863 4.27273 9.8Z" fill="#ABABAB"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_81_10600">
|
|
||||||
<rect width="20" height="20" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,10 +0,0 @@
|
|||||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_408_56)">
|
|
||||||
<path d="M47.1582 14.4283C47.1582 13.8368 47.4684 13.2886 47.9757 12.9842L61.4494 4.89891C62.5721 4.22529 64.0003 5.03389 64.0003 6.34306V20.2103C64.0003 21.1405 63.2461 21.8945 62.3161 21.8945H48.8424C47.9124 21.8945 47.1582 21.1405 47.1582 20.2103V14.4283Z" fill="#455FE9"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.3966 26.4593C23.8893 26.7637 23.5789 27.3118 23.5789 27.9034V43.7921L23.5789 45.4751L23.5789 45.4763L23.5789 48.0001C23.5789 48.9301 22.8249 49.6001 21.8947 49.6001H18.5263C17.5962 49.6001 16.8421 48.9301 16.8421 48.0001V45.4751V34.976C16.8421 33.6669 15.4139 32.8583 14.2913 33.5319L0.817607 41.6171C0.310362 41.9216 0 42.4696 0 43.0615V60.6342C0 61.5642 0.754048 62.3184 1.68421 62.3184H15.1579C16.0881 62.3184 16.8421 61.5642 16.8421 60.6342V58.9488V56.8314C16.8421 55.9011 17.5962 55.3376 18.5263 55.3376H21.8947C22.8249 55.3376 23.5789 55.9011 23.5789 56.8314V58.9488L23.5789 60.6342C23.5789 61.5642 24.333 62.3184 25.2632 62.3184H38.7368C39.6669 62.3184 40.4211 61.5642 40.4211 60.6342V45.4763V43.7921V19.8182C40.4211 18.509 38.9928 17.7004 37.8701 18.374L24.3966 26.4593Z" fill="#455FE9"/>
|
|
||||||
<path d="M47.1592 28.7999V60.7999C47.1592 61.6836 47.9132 62.3999 48.8434 62.3999H62.3171C63.2472 62.3999 64.0013 61.6836 64.0013 60.7999V28.7999C64.0013 27.9162 63.2472 27.1999 62.3171 27.1999H48.8434C47.9132 27.1999 47.1592 27.9162 47.1592 28.7999Z" fill="#455FE9"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="30" height="30" viewBox="0 0 30 30"><defs><clipPath id="master_svg0_0_54108"><rect x="0" y="0" width="30" height="30" rx="0"/></clipPath><clipPath id="master_svg1_0_54109"><rect x="3" y="3" width="24" height="24" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_0_54108)"><g clip-path="url(#master_svg1_0_54109)"><g><path d="M15.560000085830689,25.78800003528595C9.824000085830688,25.78800003528595,5.1600000858306885,21.12400003528595,5.1600000858306885,15.38800003528595C5.1600000858306885,9.65200003528595,9.824000085830688,4.98800005872345,15.560000085830689,4.98800003528595C21.296000085830688,4.98800001184845,25.96000008583069,9.65200003528595,25.96000008583069,15.38800003528595C25.96000008583069,21.12400003528595,21.29200008583069,25.78800003528595,15.560000085830689,25.78800003528595ZM15.560000085830689,6.588000035285949C10.708000085830689,6.588000035285949,6.760000085830688,10.53600003528595,6.760000085830688,15.38800003528595C6.760000085830688,20.240000035285952,10.708000085830689,24.18800003528595,15.560000085830689,24.18800003528595C20.41200008583069,24.18800003528595,24.360000085830688,20.240000035285952,24.360000085830688,15.38800003528595C24.360000085830688,10.53600003528595,20.41200008583069,6.588000035285949,15.560000085830689,6.588000035285949Z" fill="#FF3C3C" fill-opacity="1"/></g><g><path d="M15.559999228881836,16.18800030517578C15.119999228881836,16.18800030517578,14.760000228881836,15.828000305175781,14.760000228881836,15.388000305175781C14.760000228881836,15.388000305175781,14.760000228881836,10.480000305175782,14.760000228881836,10.480000305175782C14.760000228881836,10.04000030517578,15.119999228881836,9.680000328613282,15.559999228881836,9.680000305175783C16.000000228881834,9.68000028173828,16.360000228881837,10.04000030517578,16.360000228881837,10.480000305175782C16.360000228881837,10.480000305175782,16.360000228881837,15.384000305175782,16.360000228881837,15.384000305175782C16.360000228881837,15.828000305175781,16.000000228881834,16.18800030517578,15.559999228881836,16.18800030517578Z" fill="#FF3C3C" fill-opacity="1"/></g><g><path d="M14.347999572753906,18.707999336242676C14.347999572753908,19.029439336242675,14.475691572753906,19.337719336242674,14.702985572753906,19.565009336242674C14.930280572753906,19.792309336242674,15.238556572753906,19.919999336242675,15.559999572753906,19.919999336242675C15.881439572753907,19.919999336242675,16.189719572753905,19.792309336242674,16.417009572753905,19.565009336242674C16.644309572753905,19.337719336242674,16.771999572753906,19.029439336242675,16.771999572753906,18.707999336242676C16.771999572753906,18.386557336242674,16.644309572753905,18.078280336242678,16.417009572753905,17.850986336242677C16.189719572753905,17.623692336242677,15.881439572753907,17.495999336242676,15.559999572753906,17.495999336242676C15.238556572753906,17.495999336242676,14.930280572753906,17.623692336242677,14.702985572753906,17.850986336242677C14.475691572753906,18.078280336242678,14.347999572753908,18.386557336242674,14.347999572753906,18.707999336242676Z" fill="#FF3C3C" fill-opacity="1"/></g></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.1 KiB |
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="40" height="40" viewBox="0 0 40 40"><defs><clipPath id="master_svg0_0_54137"><rect x="0" y="0" width="40" height="40" rx="0"/></clipPath><clipPath id="master_svg1_0_54138"><rect x="6" y="6" width="28" height="28" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_0_54137)"><g clip-path="url(#master_svg1_0_54138)"><g><path d="M28.633299713897706,7.400000095367432C28.166699713897707,7.400000095367432,27.699999713897704,7.750000095367431,27.699999713897704,8.333333095367431C27.699999713897704,8.333333095367431,27.699999713897704,12.650000095367432,27.699999713897704,12.650000095367432C25.949999713897704,10.200000095367432,23.149999713897706,8.683330095367431,19.999999713897704,8.683330095367431C14.749999713897704,8.683330095367431,10.549999713897705,13.000000095367431,10.549999713897705,18.13330009536743C10.549999713897705,18.13330009536743,10.549999713897705,23.85000009536743,10.549999713897705,23.85000009536743C10.549999713897705,25.483300095367433,11.949999713897705,26.88330009536743,13.583329713897704,26.88330009536743C15.216669713897705,26.88330009536743,16.616669713897707,25.483300095367433,16.616669713897707,23.85000009536743C16.616669713897707,23.85000009536743,16.616669713897707,21.40000009536743,16.616669713897707,21.40000009536743C16.616669713897707,19.766700095367433,15.216669713897705,18.36670009536743,13.583329713897704,18.36670009536743C13.116669713897705,18.36670009536743,12.649999713897705,18.483300095367433,12.299999713897705,18.716700095367433C12.299999713897705,18.716700095367433,12.299999713897705,18.016700095367433,12.299999713897705,18.016700095367433C12.299999713897705,13.700000095367432,15.799999713897705,10.316670095367432,19.999999713897704,10.316670095367432C24.199999713897704,10.316670095367432,27.699999713897704,13.816670095367432,27.699999713897704,18.016700095367433C27.699999713897704,18.016700095367433,27.699999713897704,18.83330009536743,27.699999713897704,18.83330009536743C27.233299713897704,18.60000009536743,26.766699713897705,18.36670009536743,26.183299713897703,18.36670009536743C24.549999713897705,18.36670009536743,23.149999713897706,19.766700095367433,23.149999713897706,21.40000009536743C23.149999713897706,21.40000009536743,23.149999713897706,23.85000009536743,23.149999713897706,23.85000009536743C23.149999713897706,25.483300095367433,24.549999713897705,26.88330009536743,26.183299713897703,26.88330009536743C26.183299713897703,26.88330009536743,26.416699713897707,26.88330009536743,26.416699713897707,26.88330009536743C25.249999713897704,28.750000095367433,23.266699713897705,30.03330009536743,20.933299713897703,30.266700095367433C20.466669713897705,30.38330009536743,20.116669713897707,30.733300095367433,20.116669713897707,31.200000095367432C20.116669713897707,31.666700095367432,20.583299713897706,32.01670009536743,20.933299713897703,32.01670009536743C20.933299713897703,32.01670009536743,21.049999713897705,32.01670009536743,21.049999713897705,32.01670009536743C25.599999713897706,31.433300095367432,28.983299713897704,27.700000095367432,29.333299713897706,23.266700095367433C29.333299713897706,23.266700095367433,29.333299713897706,8.216667095367432,29.333299713897706,8.216667095367432C29.449999713897704,7.750000095367431,29.099999713897706,7.400000095367432,28.633299713897706,7.400000095367432ZM13.583329713897704,20.11670009536743C14.283329713897706,20.11670009536743,14.866669713897705,20.700000095367432,14.866669713897705,21.40000009536743C14.866669713897705,21.40000009536743,14.866669713897705,23.85000009536743,14.866669713897705,23.85000009536743C14.866669713897705,24.55000009536743,14.283329713897706,25.13330009536743,13.583329713897704,25.13330009536743C12.883329713897705,25.13330009536743,12.299999713897705,24.55000009536743,12.299999713897705,23.85000009536743C12.299999713897705,23.85000009536743,12.299999713897705,21.516700095367433,12.299999713897705,21.516700095367433C12.299999713897705,20.700000095367432,12.883329713897705,20.11670009536743,13.583329713897704,20.11670009536743ZM27.583299713897706,23.85000009536743C27.583299713897706,24.55000009536743,26.999999713897704,25.13330009536743,26.299999713897705,25.13330009536743C25.599999713897706,25.13330009536743,25.016699713897705,24.55000009536743,25.016699713897705,23.85000009536743C25.016699713897705,23.85000009536743,25.016699713897705,21.40000009536743,25.016699713897705,21.40000009536743C25.016699713897705,20.700000095367432,25.599999713897706,20.11670009536743,26.299999713897705,20.11670009536743C26.999999713897704,20.11670009536743,27.583299713897706,20.700000095367432,27.583299713897706,21.40000009536743C27.583299713897706,21.40000009536743,27.583299713897706,23.85000009536743,27.583299713897706,23.85000009536743Z" fill="#333333" fill-opacity="1"/></g></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="26" height="26" viewBox="0 0 26 26"><defs><clipPath id="master_svg0_0_54219"><rect x="5" y="5" width="16" height="16" rx="0"/></clipPath></defs><g><g><g><rect x="0" y="0" width="26" height="26" rx="8" fill="#FF9317" fill-opacity="1"/></g><g clip-path="url(#master_svg0_0_54219)"><g><path d="M7.337497416553497,7.684375047683716C7.337497416553497,7.684375047683716,18.1484374165535,7.684375047683716,18.1484374165535,7.684375047683716C18.885937416553496,7.684375047683716,19.490637416553497,8.273437047683716,19.500037416553496,8.995315047683716C19.500037416553496,8.995315047683716,12.745317416553497,12.703125047683717,12.745317416553497,12.703125047683717C12.745317416553497,12.703125047683717,5.992187436553498,8.999995047683715,5.992187436553498,8.999995047683715C5.9984374165534975,8.275000047683715,6.598437416553497,7.684375047683716,7.337497416553497,7.684375047683716ZM5.992187436553498,10.418745047683716C5.992187436553498,10.418745047683716,5.985937416553497,16.975005047683716,5.985937416553497,16.975005047683716C5.985937416553497,17.704675047683715,6.5937504165534975,18.301575047683716,7.337497416553497,18.301575047683716C7.337497416553497,18.301575047683716,18.1484374165535,18.301575047683716,18.1484374165535,18.301575047683716C18.8922374165535,18.301575047683716,19.500037416553496,17.704675047683715,19.500037416553496,16.975005047683716C19.500037416553496,16.975005047683716,19.500037416553496,10.415625047683715,19.500037416553496,10.415625047683715C19.500037416553496,10.415625047683715,12.904687416553497,13.949995047683716,12.904687416553497,13.949995047683716C12.803127416553497,14.004685047683715,12.681247416553497,14.004685047683715,12.581247416553497,13.949995047683716C12.581247416553497,13.949995047683716,5.992187436553498,10.418745047683716,5.992187436553498,10.418745047683716Z" fill="#FFFFFF" fill-opacity="1"/></g></g></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="18" height="18" viewBox="0 0 18 18"><defs><clipPath id="master_svg0_0_54206"><rect x="0" y="0" width="18" height="18" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_0_54206)"><g><path d="M15.231050173568725,8.453999919891357C14.901050173568725,8.465999919891358,14.643050173568726,8.744999919891358,14.655050173568725,9.077999919891358C14.717950173568726,10.664999919891358,14.112050173568726,12.218999919891358,12.990050173568726,13.340999919891358C11.913000173568726,14.417999919891358,10.482000173568725,15.011999919891357,8.958000173568726,15.011999919891357C7.434000173568726,15.011999919891357,6.003000173568726,14.417999919891358,4.929000173568726,13.340999919891358C2.7060001735687256,11.117999919891357,2.7060001735687256,7.502999919891358,4.929000173568726,5.279999919891358C6.006000173568726,4.202999919891358,7.437000173568726,3.6089999198913576,8.958000173568726,3.6089999198913576C9.288000173568726,3.6089999198913576,9.558000173568725,3.3389999198913576,9.558000173568725,3.0089999198913575C9.558000173568725,2.6789999198913574,9.288000173568726,2.4089999374694573,8.958000173568726,2.4089999198913574C7.116000173568725,2.4089999198913574,5.382000173568725,3.1259999198913575,4.080000173568726,4.430999919891358C1.3890001735687256,7.1219999198913575,1.3890001735687256,11.498999919891357,4.080000173568726,14.189999919891358C5.382000173568725,15.491999919891358,7.116000173568725,16.21199991989136,8.958000173568726,16.21199991989136C10.800000173568726,16.21199991989136,12.534050173568726,15.494999919891358,13.836050173568726,14.189999919891358C14.509250173568725,13.515699919891357,15.035850173568726,12.709599919891357,15.382750173568725,11.822119919891357C15.729750173568725,10.934679919891357,15.889450173568726,9.985109919891357,15.852050173568726,9.032999919891356C15.842950173568726,8.699999919891358,15.566950173568726,8.444999919891357,15.231050173568725,8.453999919891357Z" fill="#333333" fill-opacity="1"/></g><g><path d="M15.294000695495605,2.433000087738038C15.294000695495605,2.433000087738038,12.363000695495606,2.433000087738038,12.363000695495606,2.433000087738038C12.033000695495605,2.433000087738038,11.763000695495606,2.703000087738037,11.763000695495606,3.033000087738037C11.763000695495606,3.3630000877380373,12.033000695495605,3.6330000877380373,12.363000695495606,3.6330000877380373C12.363000695495606,3.6330000877380373,13.899000695495605,3.6330000877380373,13.899000695495605,3.6330000877380373C13.899000695495605,3.6330000877380373,9.585000695495605,7.944000087738037,9.585000695495605,7.944000087738037C9.501154495495605,8.027970087738037,9.444031395495605,8.134870087738037,9.420827495495605,8.251240087738037C9.397623495495605,8.367620087738036,9.409375748495606,8.488250087738038,9.454605095495605,8.597960087738038C9.499834395495606,8.707660087738038,9.576517695495605,8.801530087738037,9.674994695495606,8.867730087738037C9.773471695495605,8.933940087738037,9.889338695495605,8.969520087738037,10.008000695495605,8.970000087738036C10.161000695495606,8.970000087738036,10.314000695495606,8.910000087738037,10.431000695495605,8.793000087738037C10.431000695495605,8.793000087738037,14.691000695495607,4.533000087738037,14.691000695495607,4.533000087738037C14.691000695495607,4.533000087738037,14.691000695495607,5.961000087738038,14.691000695495607,5.961000087738038C14.691000695495607,6.291000087738038,14.961000695495606,6.561000087738037,15.291000695495605,6.561000087738037C15.621000695495606,6.561000087738037,15.891000695495606,6.291000087738038,15.891000695495606,5.961000087738038C15.891000695495606,5.961000087738038,15.891000695495606,3.030000087738037,15.891000695495606,3.030000087738037C15.894000695495606,2.700000087738037,15.624000695495607,2.4330000701599372,15.294000695495605,2.433000087738038Z" fill="#333333" fill-opacity="1"/></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="26" height="26" viewBox="0 0 26 26"><defs><clipPath id="master_svg0_0_54267"><rect x="6" y="5" width="15" height="15" rx="0"/></clipPath></defs><g><g><rect x="0" y="0" width="26" height="26" rx="8" fill="#FF9317" fill-opacity="1"/></g><g clip-path="url(#master_svg0_0_54267)"><g><path d="M13.5,5.9375C17.37895,5.9375,20.53125,8.871410000000001,20.53125,12.5C20.53125,16.1281,17.37895,19.0625,13.50141,19.0625C12.28315,19.0664,11.08285,18.768900000000002,10.00734,18.1967C10.00734,18.1967,9.76078,18.064500000000002,9.76078,18.064500000000002C9.76078,18.064500000000002,8.25141,18.7081,8.25141,18.7081C8.17543,18.7405,8.09223,18.752299999999998,8.01025,18.7422C7.9282699999999995,18.732100000000003,7.85042,18.700499999999998,7.78457,18.6506C7.71872,18.6008,7.66721,18.534399999999998,7.63527,18.458199999999998C7.60332,18.3821,7.59207,18.2988,7.60266,18.216900000000003C7.60266,18.216900000000003,7.61203,18.1658,7.61203,18.1658C7.61203,18.1658,7.995,16.5903,7.995,16.5903C7.995,16.5903,7.83094,16.382199999999997,7.83094,16.382199999999997C7.83094,16.382199999999997,7.73719,16.2613,7.73719,16.2613C6.91125,15.16016,6.46875,13.85656,6.46875,12.5C6.46875,8.87188,9.62109,5.9375,13.5,5.9375ZM10.92187,11.5625C10.92187,11.5625,10.85437,11.56578,10.85437,11.56578C10.67465,11.583210000000001,10.508510000000001,11.66911,10.39039,11.80569C10.27228,11.94227,10.21123,12.11905,10.21991,12.29942C10.22859,12.47978,10.30632,12.64989,10.43701,12.7745C10.567689999999999,12.89911,10.741299999999999,12.96866,10.92187,12.96875C10.92187,12.96875,10.98938,12.96547,10.98938,12.96547C11.1691,12.948039999999999,11.335239999999999,12.86214,11.45336,12.72556C11.57147,12.58898,11.63252,12.4122,11.623840000000001,12.23184C11.61516,12.05147,11.53743,11.88136,11.40674,11.75675C11.276060000000001,11.63215,11.102450000000001,11.56259,10.92187,11.5625ZM16.54685,11.5625C16.54685,11.5625,16.47935,11.56578,16.47935,11.56578C16.29965,11.583210000000001,16.13351,11.66911,16.01539,11.80569C15.89728,11.94227,15.83623,12.11905,15.84491,12.29942C15.85359,12.47978,15.93132,12.64989,16.06201,12.7745C16.19269,12.89911,16.366300000000003,12.96866,16.54685,12.96875C16.54685,12.96875,16.61435,12.96547,16.61435,12.96547C16.794150000000002,12.948039999999999,16.960250000000002,12.86214,17.07835,12.72556C17.19645,12.58898,17.257550000000002,12.4122,17.248849999999997,12.23184C17.24015,12.05147,17.16245,11.88136,17.031750000000002,11.75675C16.901049999999998,11.63215,16.727449999999997,11.56259,16.54685,11.5625ZM13.73437,11.5625C13.73437,11.5625,13.666879999999999,11.56578,13.666879999999999,11.56578C13.48715,11.583210000000001,13.321010000000001,11.66911,13.20289,11.80569C13.08478,11.94227,13.02373,12.11905,13.032409999999999,12.29942C13.04109,12.47978,13.11882,12.64989,13.24951,12.7745C13.380189999999999,12.89911,13.553799999999999,12.96866,13.73437,12.96875C13.73437,12.96875,13.801870000000001,12.96547,13.801870000000001,12.96547C13.9816,12.948039999999999,14.147739999999999,12.86214,14.26586,12.72556C14.38397,12.58898,14.44502,12.4122,14.436340000000001,12.23184C14.42766,12.05147,14.34993,11.88136,14.21924,11.75675C14.088560000000001,11.63215,13.914950000000001,11.56259,13.73437,11.5625Z" fill="#FFFFFF" fill-opacity="1"/></g></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="26" height="26" viewBox="0 0 26 26">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<rect x="0" y="0" width="26" height="26" rx="8" fill="#0087CC" fill-opacity="1"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path d="M20 7L5 13L8.5 14.5L15 10L11.5 15.5L17.5 19L20 7Z" fill="#FFFFFF" fill-opacity="1"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 433 B |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 50 KiB |
@ -1,10 +0,0 @@
|
|||||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="Frame" clip-path="url(#clip0_144_248)">
|
|
||||||
<path id="Vector" d="M16.7447 20.9744C16.7447 20.4054 16.3184 19.2213 14.9916 19.2213H13.2368C12.0846 19.2213 11.4837 20.055 11.4837 20.9744C11.4837 27.2078 11.4837 26.225 11.4837 26.225H7.4392C5.75119 26.225 4.44257 24.7807 4.44257 23.3032V15.1444H2.49655C1.65706 15.1444 1.2508 14.5606 1.08642 14.1908C1.02136 14.0424 0.743452 13.2486 1.72724 12.2289L11.9506 2.61121C12.4892 2.05366 13.2105 1.74609 13.9815 1.74609C14.7525 1.74609 15.4738 2.05366 16.0125 2.61177L26.4874 12.2226C26.4892 12.2249 26.4915 12.2272 26.4937 12.2289C27.477 13.2493 27.1991 14.0419 27.134 14.1909C26.9696 14.5607 26.5645 15.1445 25.7239 15.1445H23.7722V23.3033C23.7722 24.7807 22.4077 26.2251 20.7191 26.2251H16.7447C16.7447 26.225 16.7447 26.2107 16.7447 20.9744Z" fill="#DEDEDE"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_144_248">
|
|
||||||
<rect width="28" height="28" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1017 B |
@ -1,10 +0,0 @@
|
|||||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="Frame" clip-path="url(#clip0_64_195)">
|
|
||||||
<path id="Vector" d="M16.7447 20.9744C16.7447 20.4054 16.3185 19.2213 14.9917 19.2213H13.2368C12.0847 19.2213 11.4838 20.055 11.4838 20.9744C11.4838 27.2078 11.4838 26.225 11.4838 26.225H7.43926C5.75125 26.225 4.44263 24.7807 4.44263 23.3032V15.1444H2.49661C1.65712 15.1444 1.25086 14.5606 1.08648 14.1908C1.02142 14.0424 0.743513 13.2486 1.7273 12.2289L11.9506 2.61121C12.4893 2.05366 13.2106 1.74609 13.9816 1.74609C14.7525 1.74609 15.4739 2.05366 16.0126 2.61177L26.4875 12.2226C26.4892 12.2249 26.4915 12.2272 26.4938 12.2289C27.477 13.2493 27.1991 14.0419 27.1341 14.1909C26.9697 14.5607 26.5646 15.1445 25.724 15.1445H23.7722V23.3033C23.7722 24.7807 22.4078 26.2251 20.7192 26.2251H16.7448C16.7447 26.225 16.7447 26.2107 16.7447 20.9744Z" fill="#1797FF"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_64_195">
|
|
||||||
<rect width="28" height="28" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1016 B |
@ -1,12 +0,0 @@
|
|||||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="Frame" clip-path="url(#clip0_64_190)">
|
|
||||||
<g id="Frame_2">
|
|
||||||
<path id="Vector" d="M14.8727 16.9952V25.7225C14.8727 25.8371 14.8953 25.9506 14.9392 26.0565C14.983 26.1624 15.0473 26.2586 15.1283 26.3396C15.2094 26.4207 15.3056 26.485 15.4115 26.5288C15.5174 26.5727 15.6308 26.5952 15.7455 26.5952H21.8545C22.3175 26.5952 22.7614 26.4114 23.0888 26.084C23.4161 25.7567 23.6 25.3127 23.6 24.8498V16.9952C23.6 16.8806 23.5774 16.7672 23.5336 16.6613C23.4897 16.5554 23.4254 16.4592 23.3444 16.3781C23.2633 16.2971 23.1671 16.2328 23.0612 16.189C22.9554 16.1451 22.8419 16.1225 22.7273 16.1225H15.7455C15.6308 16.1225 15.5174 16.1451 15.4115 16.189C15.3056 16.2328 15.2094 16.2971 15.1283 16.3781C15.0473 16.4592 14.983 16.5554 14.9392 16.6613C14.8953 16.7672 14.8727 16.8806 14.8727 16.9952ZM12.2545 16.1225H5.27272C5.15811 16.1225 5.04463 16.1451 4.93874 16.189C4.83286 16.2328 4.73665 16.2971 4.65561 16.3781C4.57457 16.4592 4.51029 16.5554 4.46643 16.6613C4.42257 16.7672 4.4 16.8806 4.4 16.9952V24.8498C4.4 25.3127 4.58389 25.7567 4.91123 26.084C5.23856 26.4114 5.68253 26.5952 6.14545 26.5952H12.2545C12.3692 26.5952 12.4826 26.5727 12.5885 26.5288C12.6944 26.485 12.7906 26.4207 12.8717 26.3396C12.9527 26.2586 13.017 26.1624 13.0608 26.0565C13.1047 25.9506 13.1273 25.8371 13.1273 25.7225V16.9952C13.1273 16.8806 13.1047 16.7672 13.0608 16.6613C13.017 16.5554 12.9527 16.4592 12.8717 16.3781C12.7906 16.2971 12.6944 16.2328 12.5885 16.189C12.4826 16.1451 12.3692 16.1225 12.2545 16.1225ZM23.6 7.39525H21.8698C22.2827 6.90556 22.5594 6.31573 22.6719 5.68514C22.7728 5.10782 22.7332 4.5147 22.5567 3.95586C22.3801 3.39703 22.0716 2.88889 21.6573 2.47437C21.1025 1.92075 20.3847 1.55987 19.6094 1.44487C18.8341 1.32986 18.0424 1.4668 17.3508 1.83554C16.6265 2.21823 16.0727 2.86099 15.7249 3.60281L14 7.28048L12.2607 3.57314C11.9713 2.95612 11.5389 2.4063 10.9712 2.02972C9.44611 1.01823 7.53265 1.28397 6.34269 2.47437C5.92844 2.88893 5.62001 3.39707 5.44343 3.95589C5.26686 4.51472 5.22731 5.10782 5.32814 5.68514C5.44062 6.31573 5.71725 6.90556 6.13018 7.39525H4.4C3.93707 7.39525 3.49311 7.57914 3.16577 7.90648C2.83844 8.23381 2.65454 8.67778 2.65454 9.1407V12.6316C2.65454 13.0945 2.83844 13.5385 3.16577 13.8658C3.49311 14.1932 3.93707 14.3771 4.4 14.3771H12.2545C12.3692 14.3771 12.4826 14.3545 12.5885 14.3106C12.6944 14.2668 12.7906 14.2025 12.8717 14.1214C12.9527 14.0404 13.017 13.9442 13.0608 13.8383C13.1047 13.7324 13.1273 13.6189 13.1273 13.5043V10.0418C13.1273 9.60717 13.4244 9.20397 13.8534 9.13415C13.9786 9.11304 14.1069 9.11947 14.2294 9.153C14.3519 9.18652 14.4656 9.24634 14.5627 9.32829C14.6597 9.41024 14.7377 9.51234 14.7912 9.6275C14.8448 9.74266 14.8726 9.8681 14.8727 9.9951V13.5043C14.8727 13.6189 14.8953 13.7324 14.9392 13.8383C14.983 13.9442 15.0473 14.0404 15.1283 14.1214C15.2094 14.2025 15.3056 14.2668 15.4115 14.3106C15.5174 14.3545 15.6308 14.3771 15.7455 14.3771H23.6C24.0629 14.3771 24.5069 14.1932 24.8342 13.8658C25.1616 13.5385 25.3455 13.0945 25.3455 12.6316V9.1407C25.3455 8.67778 25.1616 8.23381 24.8342 7.90648C24.5069 7.57914 24.0629 7.39525 23.6 7.39525ZM9.4256 7.39525L8.11476 6.78041C7.83665 6.65214 7.59404 6.45796 7.40797 6.2147C7.22189 5.97145 7.09798 5.68647 7.04698 5.38448C6.99231 5.08313 7.01194 4.77298 7.10417 4.48093C7.1964 4.18887 7.35845 3.92371 7.57629 3.70841C7.7913 3.49017 8.05644 3.32784 8.34858 3.23558C8.64073 3.14332 8.951 3.12393 9.25236 3.1791C9.55433 3.22999 9.83931 3.35381 10.0826 3.53981C10.3258 3.72581 10.52 3.96837 10.6483 4.24644L12.1254 7.39525H9.4256ZM20.9526 5.38448C20.9018 5.68647 20.778 5.97149 20.592 6.21477C20.406 6.45804 20.1633 6.65221 19.8852 6.78041L18.5744 7.39525H15.8742L17.3513 4.24644C17.4795 3.96828 17.6737 3.72564 17.9171 3.53962C18.1605 3.35361 18.4456 3.22985 18.7476 3.1791C19.0489 3.12408 19.3591 3.14355 19.6511 3.2358C19.9432 3.32805 20.2083 3.4903 20.4233 3.70841C20.6412 3.92362 20.8034 4.18877 20.8956 4.48086C20.9878 4.77294 21.0074 5.08313 20.9526 5.38448Z" fill="#DEDEDE"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_64_190">
|
|
||||||
<rect width="28" height="28" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.1 KiB |
@ -1,12 +0,0 @@
|
|||||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="Frame" clip-path="url(#clip0_144_244)">
|
|
||||||
<g id="Frame_2">
|
|
||||||
<path id="Vector" d="M14.8728 16.9952V25.7225C14.8728 25.8371 14.8953 25.9506 14.9392 26.0565C14.9831 26.1624 15.0473 26.2586 15.1284 26.3396C15.2094 26.4207 15.3056 26.485 15.4115 26.5288C15.5174 26.5727 15.6309 26.5952 15.7455 26.5952H21.8546C22.3175 26.5952 22.7615 26.4114 23.0888 26.084C23.4161 25.7567 23.6 25.3127 23.6 24.8498V16.9952C23.6 16.8806 23.5775 16.7672 23.5336 16.6613C23.4898 16.5554 23.4255 16.4592 23.3444 16.3781C23.2634 16.2971 23.1672 16.2328 23.0613 16.189C22.9554 16.1451 22.8419 16.1225 22.7273 16.1225H15.7455C15.6309 16.1225 15.5174 16.1451 15.4115 16.189C15.3056 16.2328 15.2094 16.2971 15.1284 16.3781C15.0473 16.4592 14.9831 16.5554 14.9392 16.6613C14.8953 16.7672 14.8728 16.8806 14.8728 16.9952ZM12.2546 16.1225H5.27277C5.15816 16.1225 5.04467 16.1451 4.93879 16.189C4.83291 16.2328 4.7367 16.2971 4.65566 16.3781C4.57462 16.4592 4.51033 16.5554 4.46647 16.6613C4.42262 16.7672 4.40004 16.8806 4.40004 16.9952V24.8498C4.40004 25.3127 4.58394 25.7567 4.91127 26.084C5.23861 26.4114 5.68257 26.5952 6.1455 26.5952H12.2546C12.3692 26.5952 12.4827 26.5727 12.5886 26.5288C12.6945 26.485 12.7907 26.4207 12.8717 26.3396C12.9527 26.2586 13.017 26.1624 13.0609 26.0565C13.1047 25.9506 13.1273 25.8371 13.1273 25.7225V16.9952C13.1273 16.8806 13.1047 16.7672 13.0609 16.6613C13.017 16.5554 12.9527 16.4592 12.8717 16.3781C12.7907 16.2971 12.6945 16.2328 12.5886 16.189C12.4827 16.1451 12.3692 16.1225 12.2546 16.1225ZM23.6 7.39525H21.8699C22.2828 6.90556 22.5594 6.31573 22.6719 5.68514C22.7728 5.10782 22.7333 4.5147 22.5567 3.95586C22.3801 3.39703 22.0717 2.88889 21.6574 2.47437C21.1026 1.92075 20.3847 1.55987 19.6095 1.44487C18.8342 1.32986 18.0425 1.4668 17.3509 1.83554C16.6265 2.21823 16.0728 2.86099 15.725 3.60281L14 7.28048L12.2607 3.57314C11.9714 2.95612 11.539 2.4063 10.9712 2.02972C9.44615 1.01823 7.5327 1.28397 6.34273 2.47437C5.92849 2.88893 5.62006 3.39707 5.44348 3.95589C5.2669 4.51472 5.22736 5.10782 5.32819 5.68514C5.44066 6.31573 5.71729 6.90556 6.13022 7.39525H4.40004C3.93712 7.39525 3.49315 7.57914 3.16582 7.90648C2.83848 8.23381 2.65459 8.67778 2.65459 9.1407V12.6316C2.65459 13.0945 2.83848 13.5385 3.16582 13.8658C3.49315 14.1932 3.93712 14.3771 4.40004 14.3771H12.2546C12.3692 14.3771 12.4827 14.3545 12.5886 14.3106C12.6945 14.2668 12.7907 14.2025 12.8717 14.1214C12.9527 14.0404 13.017 13.9442 13.0609 13.8383C13.1047 13.7324 13.1273 13.6189 13.1273 13.5043V10.0418C13.1273 9.60717 13.4245 9.20397 13.8534 9.13415C13.9787 9.11304 14.107 9.11947 14.2295 9.153C14.352 9.18652 14.4657 9.24634 14.5627 9.32829C14.6597 9.41024 14.7377 9.51234 14.7913 9.6275C14.8448 9.74266 14.8726 9.8681 14.8728 9.9951V13.5043C14.8728 13.6189 14.8953 13.7324 14.9392 13.8383C14.9831 13.9442 15.0473 14.0404 15.1284 14.1214C15.2094 14.2025 15.3056 14.2668 15.4115 14.3106C15.5174 14.3545 15.6309 14.3771 15.7455 14.3771H23.6C24.063 14.3771 24.5069 14.1932 24.8343 13.8658C25.1616 13.5385 25.3455 13.0945 25.3455 12.6316V9.1407C25.3455 8.67778 25.1616 8.23381 24.8343 7.90648C24.5069 7.57914 24.063 7.39525 23.6 7.39525ZM9.42564 7.39525L8.11481 6.78041C7.8367 6.65214 7.59408 6.45796 7.40801 6.2147C7.22194 5.97145 7.09803 5.68647 7.04702 5.38448C6.99235 5.08313 7.01198 4.77298 7.10421 4.48093C7.19645 4.18887 7.3585 3.92371 7.57633 3.70841C7.79135 3.49017 8.05648 3.32784 8.34863 3.23558C8.64077 3.14332 8.95105 3.12393 9.25241 3.1791C9.55437 3.22999 9.83936 3.35381 10.0826 3.53981C10.3259 3.72581 10.5201 3.96837 10.6483 4.24644L12.1254 7.39525H9.42564ZM20.9526 5.38448C20.9018 5.68647 20.778 5.97149 20.592 6.21477C20.406 6.45804 20.1634 6.65221 19.8853 6.78041L18.5744 7.39525H15.8742L17.3513 4.24644C17.4796 3.96828 17.6738 3.72564 17.9171 3.53962C18.1605 3.35361 18.4456 3.22985 18.7477 3.1791C19.049 3.12408 19.3591 3.14355 19.6512 3.2358C19.9432 3.32805 20.2083 3.4903 20.4233 3.70841C20.6413 3.92362 20.8034 4.18877 20.8957 4.48086C20.9879 4.77294 21.0074 5.08313 20.9526 5.38448Z" fill="#1797FF"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_144_244">
|
|
||||||
<rect width="28" height="28" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.1 KiB |
@ -1,10 +0,0 @@
|
|||||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="Frame" clip-path="url(#clip0_64_205)">
|
|
||||||
<path id="Vector" d="M25.9103 12.7915C25.8163 12.2565 25.3128 11.7119 24.7905 11.5914L24.4001 11.4998C23.4826 11.2154 22.6686 10.5936 22.1511 9.67793C21.6335 8.75736 21.5111 7.71144 21.737 6.75244L21.8594 6.37645C22.0146 5.85592 21.8123 5.13783 21.403 4.78112C21.403 4.78112 21.036 4.46304 20.0009 3.85093C18.9658 3.24365 18.5188 3.07498 18.5188 3.07498C18.0153 2.88704 17.3096 3.06536 16.9426 3.4654L16.6697 3.76422C15.9734 4.43898 15.0371 4.84864 14.002 4.84864C12.967 4.84864 12.0213 4.43422 11.3249 3.7546L11.0614 3.46537C10.6991 3.06536 9.98869 2.8871 9.48519 3.07498C9.48519 3.07498 9.03354 3.24365 7.99842 3.8509C6.96331 4.46783 6.60105 4.78591 6.60105 4.78591C6.19176 5.13777 5.98942 5.85106 6.14469 6.37642L6.2577 6.75716C6.47882 7.71623 6.36116 8.75736 5.8436 9.67783C5.32604 10.5983 4.50291 11.2251 3.58084 11.5046L3.20445 11.5914C2.68689 11.7119 2.17872 12.2517 2.08465 12.7915C2.08465 12.7915 2 13.2734 2 14.4976C2 15.7218 2.08465 16.2038 2.08465 16.2038C2.17872 16.7436 2.68218 17.2834 3.20445 17.4039L3.57148 17.4906C4.49362 17.7702 5.32169 18.3968 5.83925 19.3221C6.35681 20.2427 6.47921 21.2886 6.25335 22.2476L6.14518 22.6187C5.98994 23.1393 6.19228 23.8574 6.60151 24.2141C6.60151 24.2141 6.96854 24.5322 8.00362 25.1443C9.03871 25.7564 9.48568 25.9202 9.48568 25.9202C9.98914 26.1081 10.6948 25.9298 11.0618 25.5298L11.3206 25.2454C12.0216 24.5658 12.9626 24.1514 14.0024 24.1514C15.0423 24.1514 15.9879 24.5706 16.6843 25.2502L16.943 25.5346C17.3053 25.9346 18.0157 26.113 18.5192 25.925C18.5192 25.925 18.9708 25.7563 20.006 25.1491C21.0411 24.5369 21.4033 24.2189 21.4033 24.2189C21.8126 23.867 22.015 23.1489 21.8597 22.6235L21.7467 22.2379C21.5255 21.2837 21.6432 20.2426 22.1608 19.3268C22.6783 18.4062 23.5063 17.7749 24.4285 17.4954L24.7955 17.4086C25.3131 17.2881 25.8213 16.7483 25.9153 16.2085C25.9153 16.2085 26 15.7266 26 14.5024C25.9951 13.2733 25.9104 12.7913 25.9104 12.7913L25.9103 12.7915ZM14.0023 19.3992C11.3629 19.3992 9.21743 17.2063 9.21743 14.4976C9.21743 11.7938 11.3581 9.6008 14.0023 9.6008C16.6418 9.6008 18.7872 11.7937 18.7872 14.5024C18.7824 17.2063 16.6417 19.3992 14.0023 19.3992Z" fill="#DEDEDE"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_64_205">
|
|
||||||
<rect width="28" height="28" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB |
@ -1,10 +0,0 @@
|
|||||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="Frame" clip-path="url(#clip0_144_284)">
|
|
||||||
<path id="Vector" d="M25.9103 12.7915C25.8163 12.2565 25.3128 11.7119 24.7905 11.5914L24.4001 11.4998C23.4826 11.2154 22.6686 10.5936 22.1511 9.67793C21.6335 8.75736 21.5111 7.71144 21.737 6.75244L21.8594 6.37645C22.0146 5.85592 21.8123 5.13783 21.403 4.78112C21.403 4.78112 21.036 4.46304 20.0009 3.85093C18.9658 3.24365 18.5188 3.07498 18.5188 3.07498C18.0153 2.88704 17.3096 3.06536 16.9426 3.4654L16.6697 3.76422C15.9734 4.43898 15.0371 4.84864 14.002 4.84864C12.967 4.84864 12.0213 4.43422 11.3249 3.7546L11.0614 3.46537C10.6991 3.06536 9.98869 2.8871 9.48519 3.07498C9.48519 3.07498 9.03354 3.24365 7.99842 3.8509C6.96331 4.46783 6.60105 4.78591 6.60105 4.78591C6.19176 5.13777 5.98942 5.85106 6.14469 6.37642L6.2577 6.75716C6.47882 7.71623 6.36116 8.75736 5.8436 9.67783C5.32604 10.5983 4.50291 11.2251 3.58084 11.5046L3.20445 11.5914C2.68689 11.7119 2.17872 12.2517 2.08465 12.7915C2.08465 12.7915 2 13.2734 2 14.4976C2 15.7218 2.08465 16.2038 2.08465 16.2038C2.17872 16.7436 2.68218 17.2834 3.20445 17.4039L3.57148 17.4906C4.49362 17.7702 5.32169 18.3968 5.83925 19.3221C6.35681 20.2427 6.47921 21.2886 6.25335 22.2476L6.14518 22.6187C5.98994 23.1393 6.19228 23.8574 6.60151 24.2141C6.60151 24.2141 6.96854 24.5322 8.00362 25.1443C9.03871 25.7564 9.48568 25.9202 9.48568 25.9202C9.98914 26.1081 10.6948 25.9298 11.0618 25.5298L11.3206 25.2454C12.0216 24.5658 12.9626 24.1514 14.0024 24.1514C15.0423 24.1514 15.9879 24.5706 16.6843 25.2502L16.943 25.5346C17.3053 25.9346 18.0157 26.113 18.5192 25.925C18.5192 25.925 18.9708 25.7563 20.006 25.1491C21.0411 24.5369 21.4033 24.2189 21.4033 24.2189C21.8126 23.867 22.015 23.1489 21.8597 22.6235L21.7467 22.2379C21.5255 21.2837 21.6432 20.2426 22.1608 19.3268C22.6783 18.4062 23.5063 17.7749 24.4285 17.4954L24.7955 17.4086C25.3131 17.2881 25.8213 16.7483 25.9153 16.2085C25.9153 16.2085 26 15.7266 26 14.5024C25.9951 13.2733 25.9104 12.7913 25.9104 12.7913L25.9103 12.7915ZM14.0023 19.3992C11.3629 19.3992 9.21743 17.2063 9.21743 14.4976C9.21743 11.7938 11.3581 9.6008 14.0023 9.6008C16.6418 9.6008 18.7872 11.7937 18.7872 14.5024C18.7824 17.2063 16.6417 19.3992 14.0023 19.3992Z" fill="#1797FF"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_144_284">
|
|
||||||
<rect width="28" height="28" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB |
@ -1,11 +0,0 @@
|
|||||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="Frame" clip-path="url(#clip0_64_203)">
|
|
||||||
<path id="Vector" d="M22.4125 14.9461C23.3523 14.9461 24.1271 15.7118 23.9826 16.6408C23.583 19.208 22.2927 21.5527 20.3378 23.2635C18.3829 24.9744 15.8883 25.9423 13.2914 25.9975C10.6944 26.0527 8.16092 25.1917 6.13514 23.5655C4.10935 21.9392 2.72058 19.6515 2.21235 17.1036C1.92144 15.6403 1.92951 14.1334 2.23606 12.6733C2.54261 11.2133 3.14131 9.8304 3.99614 8.60786C4.85096 7.38531 5.94423 6.34839 7.21015 5.5595C8.47608 4.77062 9.88848 4.24607 11.3624 4.01743C12.2903 3.8728 13.0566 4.64868 13.0566 5.5879V13.2446C13.0566 13.6959 13.2359 14.1286 13.5549 14.4477C13.8739 14.7668 14.3065 14.9461 14.7577 14.9461H22.4125Z" fill="#DEDEDE"/>
|
|
||||||
<path id="Vector_2" d="M24.4043 13C25.349 13 26.1278 12.2306 25.9825 11.297C25.6206 8.96568 24.5264 6.80997 22.8582 5.14175C21.19 3.47354 19.0343 2.37935 16.703 2.01751C15.7703 1.87218 15 2.65188 15 3.59572V11.2901C15 11.7436 15.1801 12.1785 15.5008 12.4992C15.8215 12.8199 16.2564 13 16.7099 13H24.4043Z" fill="#DEDEDE"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_64_203">
|
|
||||||
<rect width="28" height="28" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,12 +0,0 @@
|
|||||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="Frame" clip-path="url(#clip0_144_265)">
|
|
||||||
<path id="Vector" opacity="0.01" d="M2 2H26V26H2V2Z" fill="#1797FF"/>
|
|
||||||
<path id="Vector_2" d="M22.4125 14.9461C23.3523 14.9461 24.1271 15.7118 23.9826 16.6408C23.583 19.208 22.2927 21.5527 20.3378 23.2635C18.3829 24.9744 15.8883 25.9423 13.2914 25.9975C10.6944 26.0527 8.16092 25.1917 6.13514 23.5655C4.10935 21.9392 2.72058 19.6515 2.21235 17.1036C1.92144 15.6403 1.92951 14.1334 2.23606 12.6733C2.54261 11.2133 3.14131 9.8304 3.99614 8.60786C4.85096 7.38531 5.94423 6.34839 7.21015 5.5595C8.47608 4.77062 9.88848 4.24607 11.3624 4.01743C12.2903 3.8728 13.0566 4.64868 13.0566 5.5879V13.2446C13.0566 13.6959 13.2359 14.1286 13.5549 14.4477C13.8739 14.7668 14.3065 14.9461 14.7577 14.9461H22.4125Z" fill="#1797FF"/>
|
|
||||||
<path id="Vector_3" d="M24.4043 13C25.349 13 26.1278 12.2306 25.9825 11.297C25.6206 8.96568 24.5264 6.80997 22.8582 5.14175C21.19 3.47354 19.0343 2.37935 16.703 2.01751C15.7703 1.87218 15 2.65188 15 3.59572V11.2901C15 11.7436 15.1801 12.1785 15.5008 12.4992C15.8215 12.8199 16.2564 13 16.7099 13H24.4043Z" fill="#1797FF"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_144_265">
|
|
||||||
<rect width="28" height="28" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1,3 +1,4 @@
|
|||||||
description: This file stores settings for Dart & Flutter DevTools.
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
extensions:
|
extensions:
|
||||||
|
- drift: true
|
||||||
@ -3,6 +3,7 @@ import '../services/api_service/kr_api.user.dart';
|
|||||||
import '../utils/kr_update_util.dart';
|
import '../utils/kr_update_util.dart';
|
||||||
import '../utils/kr_secure_storage.dart';
|
import '../utils/kr_secure_storage.dart';
|
||||||
import '../utils/kr_log_util.dart';
|
import '../utils/kr_log_util.dart';
|
||||||
|
import '../utils/kr_http_adapter_util.dart';
|
||||||
import '../services/singbox_imp/kr_sing_box_imp.dart';
|
import '../services/singbox_imp/kr_sing_box_imp.dart';
|
||||||
import '../utils/kr_init_log_collector.dart'; // 🔧 新增:导入日志收集器
|
import '../utils/kr_init_log_collector.dart'; // 🔧 新增:导入日志收集器
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
@ -31,7 +32,7 @@ class KRDomain {
|
|||||||
// static String kr_currentDomain = "apicn.bearvpn.top";
|
// static String kr_currentDomain = "apicn.bearvpn.top";
|
||||||
|
|
||||||
static List<String> kr_baseDomains = ["api.hifast.biz", "api.airovpn.tel",];
|
static List<String> kr_baseDomains = ["api.hifast.biz", "api.airovpn.tel",];
|
||||||
static String kr_currentDomain = "api.hifast.biz1";
|
static String kr_currentDomain = "api.hifast.biz";
|
||||||
|
|
||||||
// 备用域名获取地址列表
|
// 备用域名获取地址列表
|
||||||
static List<String> kr_backupDomainUrls = [
|
static List<String> kr_backupDomainUrls = [
|
||||||
@ -67,21 +68,14 @@ class KRDomain {
|
|||||||
|
|
||||||
// Dio 实例及初始化
|
// Dio 实例及初始化
|
||||||
static final Dio _dio = (() {
|
static final Dio _dio = (() {
|
||||||
final dio = Dio();
|
final dio = Dio(BaseOptions(
|
||||||
// 🔧 配置HttpClientAdapter使用sing-box的mixed代理
|
connectTimeout: const Duration(seconds: kr_domainTimeout),
|
||||||
dio.httpClientAdapter = IOHttpClientAdapter(
|
sendTimeout: const Duration(seconds: kr_domainTimeout),
|
||||||
createHttpClient: () {
|
receiveTimeout: const Duration(seconds: kr_domainTimeout),
|
||||||
final client = HttpClient();
|
));
|
||||||
client.findProxy = (url) {
|
// 🔧 使用统一的 Adapter 转换工具
|
||||||
final proxyConfig = KRSingBoxImp.instance.kr_buildProxyRule();
|
dio.httpClientAdapter = KRHttpAdapterUtil.createAdapter(
|
||||||
KRLogUtil.kr_i(
|
timeout: const Duration(seconds: kr_domainTimeout),
|
||||||
'🔍 KRDomain 请求使用代理: $proxyConfig, url: $url',
|
|
||||||
tag: 'KRDomain',
|
|
||||||
);
|
|
||||||
return proxyConfig;
|
|
||||||
};
|
|
||||||
return client;
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
return dio;
|
return dio;
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -44,6 +44,9 @@ class KRAppRunData {
|
|||||||
/// 用户邀请码(从用户信息接口获取)
|
/// 用户邀请码(从用户信息接口获取)
|
||||||
final RxString kr_referCode = ''.obs;
|
final RxString kr_referCode = ''.obs;
|
||||||
|
|
||||||
|
/// 邀请人ID(谁邀请了当前用户)
|
||||||
|
final RxInt kr_refererId = 0.obs;
|
||||||
|
|
||||||
/// 用户余额
|
/// 用户余额
|
||||||
final RxInt kr_balance = 0.obs;
|
final RxInt kr_balance = 0.obs;
|
||||||
|
|
||||||
@ -597,12 +600,14 @@ class KRAppRunData {
|
|||||||
|
|
||||||
// 保存到全局状态
|
// 保存到全局状态
|
||||||
kr_referCode.value = userInfo.referCode;
|
kr_referCode.value = userInfo.referCode;
|
||||||
|
kr_refererId.value = userInfo.refererId;
|
||||||
kr_account.value = authType == 'device' ? '9000${userInfo.id}' : authIdentifier;
|
kr_account.value = authType == 'device' ? '9000${userInfo.id}' : authIdentifier;
|
||||||
kr_balance.value = userInfo.balance;
|
kr_balance.value = userInfo.balance;
|
||||||
kr_commission.value = userInfo.commission;
|
kr_commission.value = userInfo.commission;
|
||||||
|
|
||||||
KRLogUtil.kr_i('💾 [AppRunData] 用户信息已保存到全局状态:', tag: 'AppRunData');
|
KRLogUtil.kr_i('💾 [AppRunData] 用户信息已保存到全局状态:', tag: 'AppRunData');
|
||||||
KRLogUtil.kr_i(' - kr_referCode: "${kr_referCode.value}"', tag: 'AppRunData');
|
KRLogUtil.kr_i(' - kr_referCode: "${kr_referCode.value}"', tag: 'AppRunData');
|
||||||
|
KRLogUtil.kr_i(' - kr_refererId: ${kr_refererId.value}', tag: 'AppRunData');
|
||||||
KRLogUtil.kr_i(' - kr_balance: ${kr_balance.value}', tag: 'AppRunData');
|
KRLogUtil.kr_i(' - kr_balance: ${kr_balance.value}', tag: 'AppRunData');
|
||||||
KRLogUtil.kr_i(' - kr_commission: ${kr_commission.value}', tag: 'AppRunData');
|
KRLogUtil.kr_i(' - kr_commission: ${kr_commission.value}', tag: 'AppRunData');
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
|
|
||||||
import '../controllers/kr_country_selector_controller.dart';
|
|
||||||
|
|
||||||
class KRCountrySelectorBinding extends Bindings {
|
|
||||||
@override
|
|
||||||
void dependencies() {
|
|
||||||
Get.lazyPut<KRCountrySelectorController>(
|
|
||||||
() => KRCountrySelectorController(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:kaer_with_panels/app/common/app_config.dart';
|
|
||||||
import 'package:kaer_with_panels/app/utils/kr_country_util.dart';
|
|
||||||
|
|
||||||
import '../../../services/singbox_imp/kr_sing_box_imp.dart';
|
|
||||||
|
|
||||||
class KRCountrySelectorController extends GetxController {
|
|
||||||
// 使用 KRCountry 枚举来加载国家
|
|
||||||
final RxList<KRCountry> kr_countries = <KRCountry>[].obs;
|
|
||||||
// 当前选中的国家
|
|
||||||
final Rx<KRCountry> kr_selectedCountry = KRCountry.cn.obs;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
kr_selectedCountry.value = KRCountryUtil.kr_currentCountry.value;
|
|
||||||
kr_loadCountries();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载国家数据
|
|
||||||
void kr_loadCountries() {
|
|
||||||
kr_countries.value = KRCountryUtil.kr_getSupportedCountries();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选择国家
|
|
||||||
Future<void> kr_selectCountry(KRCountry country) async {
|
|
||||||
kr_selectedCountry.value = country;
|
|
||||||
// try {
|
|
||||||
// await KRSingBoxImp().kr_updateCountry(country);
|
|
||||||
// // Get.back();
|
|
||||||
// } catch (err) {
|
|
||||||
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onClose() {
|
|
||||||
// TODO: implement onClose
|
|
||||||
super.onClose();
|
|
||||||
KRSingBoxImp().kr_updateCountry(kr_selectedCountry.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
|
||||||
|
|
||||||
import 'package:kaer_with_panels/app/utils/kr_country_util.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_country_flag.dart';
|
|
||||||
import '../controllers/kr_country_selector_controller.dart';
|
|
||||||
|
|
||||||
class KRCountrySelectorView extends GetView<KRCountrySelectorController> {
|
|
||||||
const KRCountrySelectorView({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
extendBodyBehindAppBar: true,
|
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
|
||||||
body: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [
|
|
||||||
Color.fromRGBO(23, 151, 255, 0.15), // 渐变开始颜色
|
|
||||||
Color.fromRGBO(23, 151, 255, 0.05), // 中间过渡颜色
|
|
||||||
// 非渐变色区域
|
|
||||||
],
|
|
||||||
stops: [0.0, 0.28], // 调整渐变结束位置
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
AppBar(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
elevation: 0,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.arrow_back_ios,
|
|
||||||
size: 20.r,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
AppTranslations.kr_setting.countrySelector,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Obx(
|
|
||||||
() => ListView.separated(
|
|
||||||
padding: EdgeInsets.all(16.r),
|
|
||||||
itemCount: controller.kr_countries.length,
|
|
||||||
separatorBuilder: (context, index) => SizedBox(height: 12.h),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final country = controller.kr_countries[index];
|
|
||||||
return _kr_buildCountryCard(country, context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建国家卡片
|
|
||||||
Widget _kr_buildCountryCard(KRCountry country, BuildContext context) {
|
|
||||||
return Obx(
|
|
||||||
() => InkWell(
|
|
||||||
onTap: () => controller.kr_selectCountry(country),
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.all(16.r),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(12.r),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
// 国家图标
|
|
||||||
KRCountryFlag(
|
|
||||||
countryCode: country.kr_code,
|
|
||||||
width: 24.r,
|
|
||||||
height: 24.r,
|
|
||||||
),
|
|
||||||
SizedBox(width: 12.w),
|
|
||||||
// 国家名称
|
|
||||||
Text(
|
|
||||||
KRCountryUtil.kr_getCountryName(country),
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
// 选中标记
|
|
||||||
if (controller.kr_selectedCountry.value == country)
|
|
||||||
Icon(
|
|
||||||
Icons.check_circle,
|
|
||||||
color: Colors.blue,
|
|
||||||
size: 20.r,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
|
|
||||||
import '../controllers/kr_device_management_controller.dart';
|
|
||||||
|
|
||||||
class KRDeviceManagementBinding extends Bindings {
|
|
||||||
@override
|
|
||||||
void dependencies() {
|
|
||||||
Get.lazyPut<KRDeviceManagementController>(
|
|
||||||
() => KRDeviceManagementController(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,294 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
|
||||||
import 'package:kaer_with_panels/app/services/api_service/kr_api.user.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/dialogs/kr_dialog.dart';
|
|
||||||
import 'package:kaer_with_panels/app/common/app_run_data.dart';
|
|
||||||
import 'package:kaer_with_panels/app/services/api_service/kr_auth_api.dart';
|
|
||||||
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
|
|
||||||
import 'package:kaer_with_panels/app/services/kr_device_info_service.dart';
|
|
||||||
import 'package:kaer_with_panels/app/services/kr_subscribe_service.dart';
|
|
||||||
import 'package:kaer_with_panels/app/model/enum/kr_request_type.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
class KRDeviceManagementController extends GetxController {
|
|
||||||
// 设备列表
|
|
||||||
final RxList<Map<String, dynamic>> devices = <Map<String, dynamic>>[].obs;
|
|
||||||
|
|
||||||
// 加载状态
|
|
||||||
final RxBool isLoading = true.obs;
|
|
||||||
|
|
||||||
// 当前设备ID
|
|
||||||
String? currentDeviceId;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
_initDeviceId();
|
|
||||||
loadDeviceList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 初始化当前设备ID
|
|
||||||
Future<void> _initDeviceId() async {
|
|
||||||
try {
|
|
||||||
currentDeviceId = KRDeviceInfoService().deviceId ?? 'unknown';
|
|
||||||
KRLogUtil.kr_i('当前设备ID: $currentDeviceId', tag: 'DeviceManagement');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('获取设备ID失败: $e', tag: 'DeviceManagement');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 加载设备列表
|
|
||||||
Future<void> loadDeviceList() async {
|
|
||||||
try {
|
|
||||||
isLoading.value = true;
|
|
||||||
KRLogUtil.kr_i('开始加载设备列表', tag: 'DeviceManagement');
|
|
||||||
|
|
||||||
// 调用API获取设备列表
|
|
||||||
final result = await KRUserApi().kr_getUserDevices();
|
|
||||||
|
|
||||||
result.fold(
|
|
||||||
(error) {
|
|
||||||
KRLogUtil.kr_e('加载设备列表失败: ${error.msg}', tag: 'DeviceManagement');
|
|
||||||
Get.snackbar(AppTranslations.kr_dialog.error, error.msg);
|
|
||||||
},
|
|
||||||
(deviceList) {
|
|
||||||
KRLogUtil.kr_i('获取到 ${deviceList.length} 个设备', tag: 'DeviceManagement');
|
|
||||||
|
|
||||||
// 转换设备数据格式
|
|
||||||
devices.value = deviceList.map((device) {
|
|
||||||
final identifier = device['identifier']?.toString() ?? '';
|
|
||||||
final isCurrent = identifier == currentDeviceId;
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': device['id']?.toString() ?? '',
|
|
||||||
'identifier': identifier,
|
|
||||||
'device_name': device['user_agent'] ?? '未知设备',
|
|
||||||
'ip': device['ip'] ?? '',
|
|
||||||
'last_login': device['updated_at'] ?? device['created_at'] ?? '',
|
|
||||||
'is_current': isCurrent,
|
|
||||||
'enabled': device['enabled'] ?? true,
|
|
||||||
'online': device['online'] ?? false,
|
|
||||||
};
|
|
||||||
}).toList();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
KRLogUtil.kr_e('加载设备列表异常: $e', tag: 'DeviceManagement');
|
|
||||||
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
|
|
||||||
Get.snackbar(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.loadDeviceListFailed);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 删除设备
|
|
||||||
Future<void> deleteDevice(String id) async {
|
|
||||||
try {
|
|
||||||
// 检查是否是本机设备
|
|
||||||
final device = devices.firstWhere(
|
|
||||||
(d) => d['id'] == id,
|
|
||||||
orElse: () => {},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (device.isEmpty) return;
|
|
||||||
|
|
||||||
final isCurrent = device['is_current'] ?? false;
|
|
||||||
|
|
||||||
// 使用响应式变量来接收确认结果
|
|
||||||
bool? confirmed;
|
|
||||||
|
|
||||||
// 显示确认对话框
|
|
||||||
await KRDialog.show(
|
|
||||||
title: AppTranslations.kr_deviceManagement.deleteConfirmTitle,
|
|
||||||
message: isCurrent
|
|
||||||
? AppTranslations.kr_deviceManagement.deleteCurrentDeviceMessage
|
|
||||||
: AppTranslations.kr_deviceManagement.deleteOtherDeviceMessage,
|
|
||||||
icon: Container(
|
|
||||||
width: 56,
|
|
||||||
height: 56,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.red.withOpacity(0.1),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
Icons.warning_rounded,
|
|
||||||
color: Colors.red,
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
confirmText: AppTranslations.kr_dialog.delete,
|
|
||||||
cancelText: AppTranslations.kr_dialog.kr_cancel,
|
|
||||||
onConfirm: () {
|
|
||||||
confirmed = true;
|
|
||||||
},
|
|
||||||
onCancel: () {
|
|
||||||
confirmed = false;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirmed != true) return;
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('开始解绑设备 - id: $id, isCurrent: $isCurrent', tag: 'DeviceManagement');
|
|
||||||
|
|
||||||
// 调用API解绑设备
|
|
||||||
final result = await KRUserApi().kr_unbindUserDevice(id);
|
|
||||||
|
|
||||||
result.fold(
|
|
||||||
(error) {
|
|
||||||
KRLogUtil.kr_e('删除设备失败: ${error.msg}', tag: 'DeviceManagement');
|
|
||||||
Get.snackbar(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.deleteFailed(error.msg));
|
|
||||||
},
|
|
||||||
(_) async {
|
|
||||||
KRLogUtil.kr_i('设备删除成功', tag: 'DeviceManagement');
|
|
||||||
|
|
||||||
if (isCurrent) {
|
|
||||||
// 如果删除的是本机设备,重新进行设备登录
|
|
||||||
KRLogUtil.kr_i('本机设备已删除,准备重新登录', tag: 'DeviceManagement');
|
|
||||||
|
|
||||||
// 先关闭当前设备管理页面
|
|
||||||
Get.back();
|
|
||||||
|
|
||||||
// 执行重新登录
|
|
||||||
await _reloginWithDevice();
|
|
||||||
} else {
|
|
||||||
// 删除其他设备,从列表中移除
|
|
||||||
devices.removeWhere((device) => device['id'] == id);
|
|
||||||
Get.snackbar(AppTranslations.kr_dialog.success, AppTranslations.kr_deviceManagement.deleteSuccess);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
KRLogUtil.kr_e('删除设备异常: $e', tag: 'DeviceManagement');
|
|
||||||
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
|
|
||||||
Get.snackbar(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.deleteFailed(e.toString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 重新使用设备登录
|
|
||||||
Future<void> _reloginWithDevice() async {
|
|
||||||
try {
|
|
||||||
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'DeviceManagement');
|
|
||||||
KRLogUtil.kr_i('开始重新进行设备登录', tag: 'DeviceManagement');
|
|
||||||
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'DeviceManagement');
|
|
||||||
|
|
||||||
// 先清除当前的用户信息(但不调用 kr_loginOut,避免显示登录界面)
|
|
||||||
final appRunData = KRAppRunData.getInstance();
|
|
||||||
appRunData.kr_isLogin.value = false;
|
|
||||||
appRunData.kr_token = null;
|
|
||||||
appRunData.kr_account.value = null;
|
|
||||||
appRunData.kr_userId.value = null;
|
|
||||||
|
|
||||||
// 检查是否启用设备登录
|
|
||||||
final siteConfigService = KRSiteConfigService();
|
|
||||||
final isDeviceLoginEnabled = siteConfigService.isDeviceLoginEnabled();
|
|
||||||
|
|
||||||
if (!isDeviceLoginEnabled) {
|
|
||||||
KRLogUtil.kr_w('设备登录未启用,执行完整退出登录', tag: 'DeviceManagement');
|
|
||||||
Get.snackbar(AppTranslations.kr_dialog.tip, AppTranslations.kr_deviceManagement.deviceLoginDisabled);
|
|
||||||
await appRunData.kr_loginOut();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('设备登录已启用,开始调用设备登录接口', tag: 'DeviceManagement');
|
|
||||||
|
|
||||||
// 初始化设备信息服务(如果还没初始化)
|
|
||||||
await KRDeviceInfoService().initialize();
|
|
||||||
|
|
||||||
// 调用设备登录接口
|
|
||||||
final authApi = KRAuthApi();
|
|
||||||
final result = await authApi.kr_deviceLogin();
|
|
||||||
|
|
||||||
result.fold(
|
|
||||||
(error) {
|
|
||||||
// 设备登录失败
|
|
||||||
KRLogUtil.kr_e('设备登录失败: ${error.msg}', tag: 'DeviceManagement');
|
|
||||||
Get.snackbar(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.reloginFailed(error.msg));
|
|
||||||
|
|
||||||
// 执行完整退出登录,显示登录界面
|
|
||||||
appRunData.kr_loginOut();
|
|
||||||
},
|
|
||||||
(token) async {
|
|
||||||
// 设备登录成功
|
|
||||||
KRLogUtil.kr_i('✅ 设备登录成功!', tag: 'DeviceManagement');
|
|
||||||
KRLogUtil.kr_i('🎫 Token: ${token.substring(0, min(20, token.length))}...', tag: 'DeviceManagement');
|
|
||||||
|
|
||||||
// 保存新的用户信息
|
|
||||||
final deviceId = KRDeviceInfoService().deviceId ?? 'unknown';
|
|
||||||
await appRunData.kr_saveUserInfo(
|
|
||||||
token,
|
|
||||||
'device_$deviceId',
|
|
||||||
KRLoginType.kr_email,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('✅ 设备重新登录成功,已更新用户信息', tag: 'DeviceManagement');
|
|
||||||
|
|
||||||
// 等待一小段时间,确保登录状态已经更新
|
|
||||||
await Future.delayed(const Duration(milliseconds: 300));
|
|
||||||
|
|
||||||
// 刷新订阅信息
|
|
||||||
KRLogUtil.kr_i('🔄 开始刷新订阅信息...', tag: 'DeviceManagement');
|
|
||||||
try {
|
|
||||||
await KRSubscribeService().kr_refreshAll();
|
|
||||||
KRLogUtil.kr_i('✅ 订阅信息刷新成功', tag: 'DeviceManagement');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('订阅信息刷新失败: $e', tag: 'DeviceManagement');
|
|
||||||
}
|
|
||||||
|
|
||||||
Get.snackbar(AppTranslations.kr_dialog.success, AppTranslations.kr_deviceManagement.reloginSuccess);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
KRLogUtil.kr_e('设备重新登录异常: $e', tag: 'DeviceManagement');
|
|
||||||
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
|
|
||||||
Get.snackbar(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.reloginFailedGeneric);
|
|
||||||
|
|
||||||
// 发生异常,执行完整退出登录
|
|
||||||
await KRAppRunData.getInstance().kr_loginOut();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取设备类型和图标
|
|
||||||
Map<String, dynamic> getDeviceTypeInfo(String userAgent) {
|
|
||||||
String deviceType = AppTranslations.kr_deviceManagement.deviceTypeUnknown;
|
|
||||||
String iconName = 'devices';
|
|
||||||
|
|
||||||
if (userAgent.contains('Android') || userAgent.toLowerCase().contains('android')) {
|
|
||||||
deviceType = AppTranslations.kr_deviceManagement.deviceTypeAndroid;
|
|
||||||
iconName = 'phone_android';
|
|
||||||
} else if (userAgent.contains('iOS') || userAgent.contains('iPhone') || userAgent.toLowerCase().contains('ios')) {
|
|
||||||
deviceType = AppTranslations.kr_deviceManagement.deviceTypeIos;
|
|
||||||
iconName = 'phone_iphone';
|
|
||||||
} else if (userAgent.contains('iPad')) {
|
|
||||||
deviceType = AppTranslations.kr_deviceManagement.deviceTypeIpad;
|
|
||||||
iconName = 'tablet';
|
|
||||||
} else if (userAgent.contains('macOS') || userAgent.contains('Mac') || userAgent.toLowerCase().contains('mac')) {
|
|
||||||
deviceType = AppTranslations.kr_deviceManagement.deviceTypeMacos;
|
|
||||||
iconName = 'desktop_mac';
|
|
||||||
} else if (userAgent.contains('Windows') || userAgent.toLowerCase().contains('windows')) {
|
|
||||||
deviceType = AppTranslations.kr_deviceManagement.deviceTypeWindows;
|
|
||||||
iconName = 'computer';
|
|
||||||
} else if (userAgent.contains('Linux') || userAgent.toLowerCase().contains('linux')) {
|
|
||||||
deviceType = AppTranslations.kr_deviceManagement.deviceTypeLinux;
|
|
||||||
iconName = 'computer';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'type': deviceType,
|
|
||||||
'icon': iconName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onReady() {
|
|
||||||
super.onReady();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onClose() {
|
|
||||||
super.onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,313 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
|
||||||
|
|
||||||
import '../controllers/kr_device_management_controller.dart';
|
|
||||||
|
|
||||||
class KRDeviceManagementView extends GetView<KRDeviceManagementController> {
|
|
||||||
const KRDeviceManagementView({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
extendBodyBehindAppBar: true,
|
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
|
||||||
body: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [
|
|
||||||
Color.fromRGBO(23, 151, 255, 0.15),
|
|
||||||
Color.fromRGBO(23, 151, 255, 0.05),
|
|
||||||
],
|
|
||||||
stops: [0.0, 0.28],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// 顶部导航栏
|
|
||||||
AppBar(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
elevation: 0,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.arrow_back_ios,
|
|
||||||
color: Theme.of(context).iconTheme.color,
|
|
||||||
size: 20.w,
|
|
||||||
),
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'设备管理',
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 内容区域
|
|
||||||
Expanded(
|
|
||||||
child: Obx(() {
|
|
||||||
if (controller.isLoading.value) {
|
|
||||||
return Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controller.devices.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.devices_other,
|
|
||||||
size: 64.w,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
SizedBox(height: 16.h),
|
|
||||||
Text(
|
|
||||||
'暂无登录设备',
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return RefreshIndicator(
|
|
||||||
onRefresh: () => controller.loadDeviceList(),
|
|
||||||
child: ListView.builder(
|
|
||||||
padding: EdgeInsets.all(16.w),
|
|
||||||
itemCount: controller.devices.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return _buildDeviceItem(
|
|
||||||
context,
|
|
||||||
controller.devices[index],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建设备项
|
|
||||||
Widget _buildDeviceItem(
|
|
||||||
BuildContext context, Map<String, dynamic> device) {
|
|
||||||
final id = device['id'] ?? '';
|
|
||||||
final identifier = device['identifier'] ?? '';
|
|
||||||
final userAgent = device['device_name'] ?? '未知设备';
|
|
||||||
final isCurrent = device['is_current'] ?? false;
|
|
||||||
final ip = device['ip'] ?? '';
|
|
||||||
final lastLoginRaw = device['last_login'];
|
|
||||||
final String lastLogin = lastLoginRaw?.toString() ?? '';
|
|
||||||
|
|
||||||
// 获取设备类型信息
|
|
||||||
final deviceInfo = controller.getDeviceTypeInfo(userAgent);
|
|
||||||
final deviceType = deviceInfo['type'] as String;
|
|
||||||
final iconName = deviceInfo['icon'] as String;
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.only(bottom: 12.w),
|
|
||||||
padding: EdgeInsets.all(16.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(12.w),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.03),
|
|
||||||
blurRadius: 10.w,
|
|
||||||
offset: Offset(0, 2.w),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// 设备类型和操作按钮
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
// 设备图标
|
|
||||||
Container(
|
|
||||||
width: 48.w,
|
|
||||||
height: 48.w,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFF1797FF).withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(8.w),
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
_getIconData(iconName),
|
|
||||||
color: const Color(0xFF1797FF),
|
|
||||||
size: 24.w,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 12.w),
|
|
||||||
// 设备信息
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
deviceType,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (isCurrent) ...[
|
|
||||||
SizedBox(width: 8.w),
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 8.w,
|
|
||||||
vertical: 2.h,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.green.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(4.w),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'本机',
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: Colors.green,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 4.h),
|
|
||||||
Text(
|
|
||||||
'ID: ${identifier.substring(0, identifier.length > 12 ? 12 : identifier.length)}...',
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 删除按钮
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => controller.deleteDevice(id),
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
foregroundColor: Theme.of(context).colorScheme.error,
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'删除',
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// 分隔线
|
|
||||||
if (ip.isNotEmpty || lastLogin.isNotEmpty) ...[
|
|
||||||
SizedBox(height: 12.h),
|
|
||||||
Divider(height: 1, color: Theme.of(context).dividerColor),
|
|
||||||
SizedBox(height: 12.h),
|
|
||||||
],
|
|
||||||
// 详细信息
|
|
||||||
if (ip.isNotEmpty)
|
|
||||||
_buildInfoRow(
|
|
||||||
context,
|
|
||||||
'IP地址',
|
|
||||||
ip,
|
|
||||||
),
|
|
||||||
if (ip.isNotEmpty && lastLogin.isNotEmpty) SizedBox(height: 8.h),
|
|
||||||
if (lastLogin.isNotEmpty)
|
|
||||||
_buildInfoRow(
|
|
||||||
context,
|
|
||||||
'最后登录',
|
|
||||||
_formatDateTime(lastLoginRaw),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建信息行
|
|
||||||
Widget _buildInfoRow(BuildContext context, String label, String value) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'$label: ',
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
value,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 格式化时间
|
|
||||||
String _formatDateTime(dynamic timestamp) {
|
|
||||||
if (timestamp == null) return '未知';
|
|
||||||
|
|
||||||
try {
|
|
||||||
DateTime dateTime;
|
|
||||||
if (timestamp is int) {
|
|
||||||
// 判断是秒级时间戳(10位)还是毫秒级时间戳(13位)
|
|
||||||
if (timestamp > 9999999999) {
|
|
||||||
// 毫秒级时间戳
|
|
||||||
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
|
||||||
} else {
|
|
||||||
// 秒级时间戳
|
|
||||||
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
|
||||||
}
|
|
||||||
} else if (timestamp is String) {
|
|
||||||
dateTime = DateTime.parse(timestamp);
|
|
||||||
} else {
|
|
||||||
return '未知';
|
|
||||||
}
|
|
||||||
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
|
|
||||||
} catch (e) {
|
|
||||||
return '未知';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取图标数据
|
|
||||||
IconData _getIconData(String iconName) {
|
|
||||||
switch (iconName) {
|
|
||||||
case 'phone_android':
|
|
||||||
return Icons.phone_android;
|
|
||||||
case 'phone_iphone':
|
|
||||||
return Icons.phone_iphone;
|
|
||||||
case 'tablet':
|
|
||||||
return Icons.tablet_mac;
|
|
||||||
case 'desktop_mac':
|
|
||||||
return Icons.desktop_mac;
|
|
||||||
case 'computer':
|
|
||||||
return Icons.computer;
|
|
||||||
default:
|
|
||||||
return Icons.devices;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,253 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import '../../../localization/app_translations.dart';
|
|
||||||
import '../../../widgets/kr_app_text_style.dart';
|
|
||||||
import '../../../widgets/kr_loading_animation.dart';
|
|
||||||
import '../controllers/kr_home_controller.dart';
|
|
||||||
import '../models/kr_home_views_status.dart';
|
|
||||||
import 'kr_home_connection_info_view.dart';
|
|
||||||
import 'kr_home_connection_options_view.dart';
|
|
||||||
import 'kr_home_node_list_view.dart';
|
|
||||||
import '../widgets/kr_subscription_card.dart';
|
|
||||||
import 'kr_home_trial_card.dart';
|
|
||||||
import 'kr_home_last_day_card.dart';
|
|
||||||
import '../../../utils/kr_log_util.dart';
|
|
||||||
|
|
||||||
class KRHomeBottomPanel extends GetView<KRHomeController> {
|
|
||||||
const KRHomeBottomPanel({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Obx(() {
|
|
||||||
final currentStatus = controller.kr_currentListStatus.value;
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('构建底部面板', tag: 'HomeBottomPanel');
|
|
||||||
KRLogUtil.kr_i('当前视图状态: ${controller.kr_currentViewStatus.value}',
|
|
||||||
tag: 'HomeBottomPanel');
|
|
||||||
KRLogUtil.kr_i('当前高度: ${controller.kr_bottomPanelHeight.value}',
|
|
||||||
tag: 'HomeBottomPanel');
|
|
||||||
|
|
||||||
if (controller.kr_currentListStatus.value ==
|
|
||||||
KRHomeViewsListStatus.kr_loading) {
|
|
||||||
return _kr_buildLoadingView();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controller.kr_currentListStatus.value ==
|
|
||||||
KRHomeViewsListStatus.kr_error) {
|
|
||||||
return _kr_buildErrorView(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentStatus == KRHomeViewsListStatus.kr_serverList ||
|
|
||||||
currentStatus == KRHomeViewsListStatus.kr_countrySubscribeList ||
|
|
||||||
currentStatus == KRHomeViewsListStatus.kr_serverSubscribeList ||
|
|
||||||
currentStatus == KRHomeViewsListStatus.kr_subscribeList) {
|
|
||||||
return const KRHomeNodeListView();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _kr_buildDefaultView(context);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _kr_buildDefaultView(BuildContext context) {
|
|
||||||
// 🔧 Android 15 增强:增加防御性检查,避免空指针
|
|
||||||
bool hasValidSubscription = false;
|
|
||||||
bool isTrial = false;
|
|
||||||
bool isLastDay = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
hasValidSubscription = controller.kr_subscribeService.kr_currentSubscribe.value != null;
|
|
||||||
isTrial = controller.kr_subscribeService.kr_isTrial.value;
|
|
||||||
isLastDay = controller.kr_subscribeService.kr_isLastDayOfSubscription.value;
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('获取订阅数据异常: $e', tag: 'HomeBottomPanel');
|
|
||||||
}
|
|
||||||
|
|
||||||
final isNotLoggedIn = controller.kr_currentViewStatus.value ==
|
|
||||||
KRHomeViewsStatus.kr_notLoggedIn;
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('=' * 60, tag: 'HomeBottomPanel');
|
|
||||||
KRLogUtil.kr_i('🎨 构建默认视图', tag: 'HomeBottomPanel');
|
|
||||||
KRLogUtil.kr_i('是否未登录: $isNotLoggedIn', tag: 'HomeBottomPanel');
|
|
||||||
KRLogUtil.kr_i('是否有有效订阅: $hasValidSubscription', tag: 'HomeBottomPanel');
|
|
||||||
KRLogUtil.kr_i('订阅列表数量: ${controller.kr_subscribeService.kr_availableSubscribes.length}', tag: 'HomeBottomPanel');
|
|
||||||
KRLogUtil.kr_i('当前选中订阅: ${controller.kr_subscribeService.kr_currentSubscribe.value?.name ?? "null"}', tag: 'HomeBottomPanel');
|
|
||||||
KRLogUtil.kr_i('是否试用: $isTrial', tag: 'HomeBottomPanel');
|
|
||||||
KRLogUtil.kr_i('当前高度: ${controller.kr_bottomPanelHeight.value}', tag: 'HomeBottomPanel');
|
|
||||||
|
|
||||||
// 🔧 新增:详细的 UI 渲染决策日志
|
|
||||||
if (hasValidSubscription) {
|
|
||||||
KRLogUtil.kr_i('✅ 将渲染: 连接信息卡片 (KRHomeConnectionInfoView)', tag: 'HomeBottomPanel');
|
|
||||||
} else {
|
|
||||||
KRLogUtil.kr_i('✅ 将渲染: 订阅卡片 (KRSubscriptionCard) - 开通会员界面', tag: 'HomeBottomPanel');
|
|
||||||
}
|
|
||||||
KRLogUtil.kr_i('=' * 60, tag: 'HomeBottomPanel');
|
|
||||||
|
|
||||||
// 🔧 关键修复:统一布局逻辑,确保无论登录状态如何都显示完整UI
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// 主要内容区域 - 始终使用 Expanded + ScrollView 确保内容可见
|
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// 🔧 核心修复:无论登录状态,都显示核心卡片(订阅或连接信息)
|
|
||||||
if (hasValidSubscription)
|
|
||||||
// 已订阅:显示连接信息卡片
|
|
||||||
Builder(builder: (context) {
|
|
||||||
KRLogUtil.kr_i('🔹 渲染连接信息卡片,margin top: ${12}', tag: 'HomeBottomPanel');
|
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.only(top: 12),
|
|
||||||
child: const KRHomeConnectionInfoView(),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
else
|
|
||||||
// 未订阅(包括未登录):始终显示订阅卡片
|
|
||||||
Builder(builder: (context) {
|
|
||||||
KRLogUtil.kr_i('🔹 渲染订阅卡片,margin: top=${12}, left=${12}, right=${12}', tag: 'HomeBottomPanel');
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.only(top: 12, left: 12, right: 12),
|
|
||||||
child: const KRSubscriptionCard(),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
|
|
||||||
// 2. 如果已订阅且是试用,展示试用卡片
|
|
||||||
if (hasValidSubscription && isTrial)
|
|
||||||
Container(
|
|
||||||
margin: EdgeInsets.only(top: 12),
|
|
||||||
child: const KRHomeTrialCard(),
|
|
||||||
),
|
|
||||||
|
|
||||||
// 3. 如果已订阅且是最后一天,展示最后一天卡片
|
|
||||||
if (hasValidSubscription && isLastDay && !isTrial)
|
|
||||||
Container(
|
|
||||||
margin: EdgeInsets.only(top: 12),
|
|
||||||
child: const KRHomeLastDayCard(),
|
|
||||||
),
|
|
||||||
|
|
||||||
// 4. 连接选项(分组和国家入口)- 始终显示
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
||||||
child: KRHomeConnectionOptionsView(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _kr_buildLoadingView() {
|
|
||||||
KRLogUtil.kr_i('构建加载视图', tag: 'HomeBottomPanel');
|
|
||||||
KRLogUtil.kr_i('当前高度: ${controller.kr_bottomPanelHeight.value}',
|
|
||||||
tag: 'HomeBottomPanel');
|
|
||||||
|
|
||||||
// 🔧 Android 15 紧急修复:加载时显示完整的默认内容 + 加载指示器
|
|
||||||
// 而不是只显示一个转圈圈,避免用户看到空白面板
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
// 底层:显示默认内容(半透明)
|
|
||||||
Opacity(
|
|
||||||
opacity: 0.5,
|
|
||||||
child: _kr_buildDefaultView(Get.context!),
|
|
||||||
),
|
|
||||||
// 顶层:加载指示器
|
|
||||||
Center(
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Get.context!.theme.cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
CircularProgressIndicator(
|
|
||||||
color: Colors.green,
|
|
||||||
strokeWidth: 3.0,
|
|
||||||
),
|
|
||||||
SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
'正在加载...',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Get.context!.theme.textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _kr_buildErrorView(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
height: 200,
|
|
||||||
padding: EdgeInsets.all(16),
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.error_outline,
|
|
||||||
size: 48,
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.error,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.checkNetwork,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 24),
|
|
||||||
SizedBox(
|
|
||||||
width: 200,
|
|
||||||
height: 44,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () => controller.kr_refreshAll(),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
||||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
elevation: 0,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
AppTranslations.kr_home.retry,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,260 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import '../../../widgets/kr_simple_loading.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_country_flag.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
|
||||||
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
|
||||||
import 'package:kaer_with_panels/singbox/model/singbox_status.dart';
|
|
||||||
import '../controllers/kr_home_controller.dart';
|
|
||||||
import '../models/kr_home_views_status.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
|
|
||||||
class KRHomeConnectionInfoView extends GetView<KRHomeController> {
|
|
||||||
const KRHomeConnectionInfoView({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return _buildConnectCard(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 当前连接
|
|
||||||
Widget _buildConnectCard(BuildContext context) {
|
|
||||||
return Obx(() {
|
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
width: double.infinity,
|
|
||||||
height: 116,
|
|
||||||
decoration: ShapeDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(14),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.currentConnectionTitle,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 切换节点按钮
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
controller.kr_switchListStatus(KRHomeViewsListStatus.kr_subscribeList);
|
|
||||||
},
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.switchNode,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 12,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 10),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
// 🔧 修复:使用 Obx 包裹确保国旗响应式更新
|
|
||||||
Obx(() {
|
|
||||||
final countryCode = controller.kr_getCurrentNodeCountry();
|
|
||||||
if (kDebugMode) {
|
|
||||||
print('🌍 ConnectionInfo 更新,国家代码: $countryCode');
|
|
||||||
}
|
|
||||||
return KRCountryFlag(
|
|
||||||
countryCode: countryCode,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
SizedBox(width: 10),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
controller.kr_currentNodeName.value,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 6),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Obx(() {
|
|
||||||
final delay = controller.kr_currentNodeLatency.value;
|
|
||||||
if (kDebugMode) {
|
|
||||||
print('🔵 UI延迟显示更新: delay=$delay');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取延迟颜色
|
|
||||||
Color getLatencyColor(int delay) {
|
|
||||||
if (delay == -2) {
|
|
||||||
return Colors.green;
|
|
||||||
} else if (delay == -1) {
|
|
||||||
return Theme.of(context).primaryColor;
|
|
||||||
} else if (delay < 500) {
|
|
||||||
return Colors.green;
|
|
||||||
} else if (delay < 3000) {
|
|
||||||
return Color(0xFFFFB700);
|
|
||||||
} else {
|
|
||||||
return Colors.red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取延迟文本
|
|
||||||
String getLatencyText(int delay) {
|
|
||||||
if (delay == -2) {
|
|
||||||
return '--';
|
|
||||||
} else if (delay == -1) {
|
|
||||||
return AppTranslations.kr_home.connecting;
|
|
||||||
} else if (delay == 0) {
|
|
||||||
return AppTranslations.kr_home.connected;
|
|
||||||
} else if (delay >= 3000) {
|
|
||||||
return AppTranslations.kr_home.timeout;
|
|
||||||
} else {
|
|
||||||
return '${delay}ms';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔧 修复:只有 delay == -1 时才显示 connecting 动画
|
|
||||||
if (delay == -1) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
KRSimpleLoading(
|
|
||||||
color: Colors.green,
|
|
||||||
size: 12,
|
|
||||||
duration: const Duration(milliseconds: 800),
|
|
||||||
),
|
|
||||||
SizedBox(width: 2),
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.connecting,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.green,
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.signal_cellular_alt,
|
|
||||||
size: 12,
|
|
||||||
color: getLatencyColor(delay)),
|
|
||||||
SizedBox(width: 2),
|
|
||||||
Text(
|
|
||||||
getLatencyText(delay),
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
color: getLatencyColor(delay),
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
// 只在非连接中状态显示上下行
|
|
||||||
Obx(() {
|
|
||||||
final delay = controller.kr_currentNodeLatency.value;
|
|
||||||
if (delay == -1) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
SizedBox(width: 10),
|
|
||||||
Icon(Icons.arrow_upward,
|
|
||||||
size: 12,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color),
|
|
||||||
Text(
|
|
||||||
controller.kr_formatBytes(KRSingBoxImp.instance.kr_stats.value.uplink),
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 10),
|
|
||||||
Icon(Icons.arrow_downward,
|
|
||||||
size: 12,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color),
|
|
||||||
Text(
|
|
||||||
controller.kr_formatBytes(KRSingBoxImp.instance.kr_stats.value.downlink),
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// 🔧 修复: 使用多层监听确保状态更新
|
|
||||||
Obx(() {
|
|
||||||
// 🔧 关键: 强制读取两个 observable 确保追踪
|
|
||||||
final _ = KRSingBoxImp.instance.kr_status.value; // 强制追踪
|
|
||||||
final isConnected = controller.kr_isConnected.value; // 使用 controller 的状态
|
|
||||||
|
|
||||||
// 再次读取状态用于判断
|
|
||||||
final status = KRSingBoxImp.instance.kr_status.value;
|
|
||||||
final isSwitching = status is SingboxStarting || status is SingboxStopping;
|
|
||||||
|
|
||||||
// 🔧 调试日志
|
|
||||||
if (kDebugMode) {
|
|
||||||
print('🔵 Switch UI 更新: status=${status.runtimeType}, isConnected=$isConnected, isSwitching=$isSwitching');
|
|
||||||
}
|
|
||||||
|
|
||||||
return CupertinoSwitch(
|
|
||||||
value: isConnected,
|
|
||||||
// 🔧 关键: 切换中时 onChanged 为 null,Switch 自动禁用
|
|
||||||
onChanged: isSwitching
|
|
||||||
? null
|
|
||||||
: (bool value) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print('🔵 Switch onChanged 触发: 请求=$value, 当前状态=$status');
|
|
||||||
}
|
|
||||||
controller.kr_toggleSwitch(value);
|
|
||||||
},
|
|
||||||
activeColor: Colors.blue,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
|
||||||
import 'package:kaer_with_panels/app/routes/app_pages.dart';
|
|
||||||
import 'package:kaer_with_panels/app/utils/kr_subscribe_navigation_util.dart';
|
|
||||||
import '../controllers/kr_home_controller.dart';
|
|
||||||
import '../models/kr_home_views_status.dart';
|
|
||||||
|
|
||||||
class KRHomeConnectionOptionsView extends GetView<KRHomeController> {
|
|
||||||
const KRHomeConnectionOptionsView({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
print('🔌 [ConnectionOptions] 开始构建连接选项组件');
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.connectionSectionTitle,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Theme.of(context).brightness == Brightness.dark
|
|
||||||
? Colors.white
|
|
||||||
: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildConnectionOption(
|
|
||||||
"home_ct",
|
|
||||||
AppTranslations.kr_home.countryRegion,
|
|
||||||
context,
|
|
||||||
onTap: () {
|
|
||||||
controller.kr_switchListStatus(KRHomeViewsListStatus.kr_countrySubscribeList);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildConnectionOption(String icon, String label, BuildContext context,
|
|
||||||
{VoidCallback? onTap}) {
|
|
||||||
print('🔌 [ConnectionOptions] 构建连接选项: $label');
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
print('🔌 [ConnectionOptions] 选项被点击: $label');
|
|
||||||
if (controller.kr_subscribeService.kr_currentSubscribe.value == null) {
|
|
||||||
// 未订阅状态下,使用统一的订阅导航工具
|
|
||||||
KRSubscribeNavigationUtil.navigateToPurchase(tag: 'ConnectionOptions');
|
|
||||||
} else {
|
|
||||||
// 已订阅状态下执行原有的点击事件
|
|
||||||
onTap?.call();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
KrLocalImage(
|
|
||||||
imageName: icon,
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
color: Theme.of(context).brightness == Brightness.dark
|
|
||||||
? Colors.white70
|
|
||||||
: Colors.black87,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
label,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).brightness == Brightness.dark
|
|
||||||
? Colors.white
|
|
||||||
: Colors.black87,
|
|
||||||
),
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 14,
|
|
||||||
color: Theme.of(context).brightness == Brightness.dark
|
|
||||||
? Colors.white54
|
|
||||||
: Colors.black45,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,150 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
|
||||||
import '../../../routes/app_pages.dart';
|
|
||||||
import '../../../utils/kr_subscribe_navigation_util.dart';
|
|
||||||
import '../controllers/kr_home_controller.dart';
|
|
||||||
|
|
||||||
|
|
||||||
/// 最后一天卡片组件
|
|
||||||
class KRHomeLastDayCard extends GetView<KRHomeController> {
|
|
||||||
const KRHomeLastDayCard({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
width: double.infinity,
|
|
||||||
decoration: ShapeDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(14),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// 顶部标题和订阅按钮
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.lastDaySubscriptionStatus,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => KRSubscribeNavigationUtil.navigateToPurchase(tag: 'LastDayCard'),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.subscribe,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 12,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
// 倒计时显示
|
|
||||||
SizedBox(height: 10),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.timer_outlined,
|
|
||||||
color: Colors.blue,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.lastDaySubscriptionMessage,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.blue,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Obx(() {
|
|
||||||
final isLastDay =
|
|
||||||
controller.kr_subscribeService.kr_isLastDayOfSubscription.value;
|
|
||||||
final remainingTime = controller.kr_subscribeService.kr_subscriptionRemainingTime.value;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
remainingTime,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: isLastDay
|
|
||||||
? (DateTime.now().millisecondsSinceEpoch %
|
|
||||||
2000 <
|
|
||||||
1000
|
|
||||||
? Colors.red
|
|
||||||
: Colors.blue)
|
|
||||||
: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodyMedium
|
|
||||||
?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 4.h),
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.subscriptionEndMessage,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodySmall
|
|
||||||
?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,950 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_country_flag.dart';
|
|
||||||
import '../../../model/business/kr_outbound_item.dart';
|
|
||||||
import '../../../utils/kr_log_util.dart';
|
|
||||||
import '../controllers/kr_home_controller.dart';
|
|
||||||
import '../models/kr_home_views_status.dart';
|
|
||||||
|
|
||||||
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_network_image.dart';
|
|
||||||
import '../../../widgets/kr_simple_loading.dart';
|
|
||||||
|
|
||||||
import '../../../../singbox/model/singbox_proxy_type.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
|
|
||||||
/// 节点列表视图组件
|
|
||||||
/// 用于展示所有节点相关的列表视图
|
|
||||||
class KRHomeNodeListView extends GetView<KRHomeController> {
|
|
||||||
const KRHomeNodeListView({super.key});
|
|
||||||
|
|
||||||
// 添加常量定义
|
|
||||||
static const Color krModernGreen = Color(0xFF4CAF50);
|
|
||||||
static const Color krModernGreenLight = Color(0xFF81C784);
|
|
||||||
|
|
||||||
// 🔧 修复无限刷新:添加标志位确保自动测试只触发一次
|
|
||||||
static bool _hasTriggeredAutoTest = false;
|
|
||||||
|
|
||||||
/// 获取显示的延迟值
|
|
||||||
/// ✅ 修复:始终显示真实的 TCP 测试结果
|
|
||||||
int _getDisplayDelay(KRHomeController controller, KROutboundItem item) {
|
|
||||||
// 直接返回真实的延迟测试结果
|
|
||||||
// 无论是否连接VPN,都使用 item.urlTestDelay.value
|
|
||||||
// - 已连接:通过 SingBox 代理测试的真实延迟
|
|
||||||
// - 未连接:通过 TCP Socket 直连测试的真实延迟
|
|
||||||
return item.urlTestDelay.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Obx(() {
|
|
||||||
// 根据列表状态选择不同的视图
|
|
||||||
switch (controller.kr_currentListStatus.value) {
|
|
||||||
case KRHomeViewsListStatus.kr_serverList:
|
|
||||||
return _buildServerList(context);
|
|
||||||
case KRHomeViewsListStatus.kr_subscribeList:
|
|
||||||
return _buildSubscribeList(context);
|
|
||||||
case KRHomeViewsListStatus.kr_countrySubscribeList:
|
|
||||||
return _kr_buildRegionList(context);
|
|
||||||
case KRHomeViewsListStatus.kr_serverSubscribeList:
|
|
||||||
return _kr_buildServerSubscribeList(context);
|
|
||||||
default:
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 服务器列表视图
|
|
||||||
|
|
||||||
/// 构建专用服务器列表
|
|
||||||
Widget _buildServerList(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
height: 360, // 减小高度比例
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(20),
|
|
||||||
topRight: Radius.circular(20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// 标题栏
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.serverListTitle,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
controller.kr_currentListStatus.value =
|
|
||||||
KRHomeViewsListStatus.kr_none;
|
|
||||||
},
|
|
||||||
child: Icon(
|
|
||||||
Icons.close,
|
|
||||||
size: 24,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 列表内容
|
|
||||||
Expanded(
|
|
||||||
child: Obx(() {
|
|
||||||
if (controller.kr_subscribeService.groupOutboundList.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: Text(
|
|
||||||
AppTranslations.kr_home.noServers,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return ListView.builder(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
itemCount: controller.kr_subscribeService.groupOutboundList.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final group =
|
|
||||||
controller.kr_subscribeService.groupOutboundList[index];
|
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.only(bottom: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () {
|
|
||||||
controller.kr_setCurrentGroup(group);
|
|
||||||
controller.kr_currentListStatus.value =
|
|
||||||
KRHomeViewsListStatus.kr_serverSubscribeList;
|
|
||||||
},
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(12),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
KRNetworkImage(
|
|
||||||
kr_imageUrl: group.icon,
|
|
||||||
kr_width: 32,
|
|
||||||
kr_height: 32,
|
|
||||||
kr_fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
group.tag,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 16,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 国家订阅列表视图
|
|
||||||
Widget _kr_buildRegionList(BuildContext context) {
|
|
||||||
return _kr_buildListPage(
|
|
||||||
context,
|
|
||||||
title: AppTranslations.kr_home.countryListTitle,
|
|
||||||
listContent: Obx(() {
|
|
||||||
if (controller.kr_subscribeService.groupOutboundList.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: Text(
|
|
||||||
AppTranslations.kr_home.noRegions,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return _kr_buildListContainer(
|
|
||||||
context,
|
|
||||||
child: ListView.builder(
|
|
||||||
padding: EdgeInsets.fromLTRB(16, 8, 16, 0),
|
|
||||||
itemCount:
|
|
||||||
controller.kr_subscribeService.countryOutboundList.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final country =
|
|
||||||
controller.kr_subscribeService.countryOutboundList[index];
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
// 主区域
|
|
||||||
InkWell(
|
|
||||||
onTap: () {
|
|
||||||
country.isExpand.value = !country.isExpand.value;
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 12),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
KRCountryFlag(
|
|
||||||
countryCode: country.country,
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
),
|
|
||||||
SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
controller
|
|
||||||
.kr_getCountryFullName(country.country),
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodyMedium
|
|
||||||
?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Obx(() {
|
|
||||||
return Icon(
|
|
||||||
country.isExpand.value
|
|
||||||
? Icons.keyboard_arrow_down
|
|
||||||
: Icons.arrow_forward_ios,
|
|
||||||
size: 16,
|
|
||||||
color:
|
|
||||||
Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 展开的服务器列表
|
|
||||||
Obx(() {
|
|
||||||
final isExpanded = country.isExpand.value;
|
|
||||||
if (!isExpanded) return const SizedBox();
|
|
||||||
|
|
||||||
return ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: NeverScrollableScrollPhysics(),
|
|
||||||
padding: EdgeInsets.only(left: 24),
|
|
||||||
itemCount: country.outboundList.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final server = country.outboundList[index];
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
InkWell(
|
|
||||||
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
|
||||||
onTap: () async {
|
|
||||||
try {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print('🔄 用户点击节点: ${server.tag}');
|
|
||||||
}
|
|
||||||
// 使用统一的节点切换方法,等待完成
|
|
||||||
final success = await controller
|
|
||||||
.kr_performNodeSwitch(server.tag);
|
|
||||||
|
|
||||||
// 只有切换成功才关闭列表
|
|
||||||
if (success) {
|
|
||||||
controller.kr_currentListStatus.value =
|
|
||||||
KRHomeViewsListStatus.kr_none;
|
|
||||||
if (kDebugMode) {
|
|
||||||
print('✅ 节点切换成功,关闭列表');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print('❌ 节点切换失败,列表保持打开');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print('❌ 节点切换异常: $e');
|
|
||||||
}
|
|
||||||
KRLogUtil.kr_e('节点切换异常: $e',
|
|
||||||
tag: 'NodeListView');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
vertical: 8,
|
|
||||||
horizontal: 16,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
// 添加轻微的背景色以区分点击区域
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: _kr_buildNodeListItem(
|
|
||||||
context,
|
|
||||||
item: server,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 添加分隔线
|
|
||||||
if (index < country.outboundList.length - 1)
|
|
||||||
Divider(
|
|
||||||
height: 1,
|
|
||||||
indent: 16,
|
|
||||||
endIndent: 16,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.dividerColor
|
|
||||||
.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
Divider(
|
|
||||||
height: 1,
|
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 服务器订阅列表视图
|
|
||||||
// 修改服务器订阅列表视图
|
|
||||||
Widget _kr_buildServerSubscribeList(BuildContext context) {
|
|
||||||
return _kr_buildListPage(
|
|
||||||
context,
|
|
||||||
title: controller.kr_currentGroup.value?.tag ?? '',
|
|
||||||
onBack: () => controller.kr_currentListStatus.value =
|
|
||||||
KRHomeViewsListStatus.kr_serverList,
|
|
||||||
listContent: Obx(() {
|
|
||||||
final servers = controller.kr_currentGroup.value?.outboundList ?? [];
|
|
||||||
if (servers.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: Text(
|
|
||||||
AppTranslations.kr_home.noNodes,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return _kr_buildListContainer(
|
|
||||||
context,
|
|
||||||
child: ListView.builder(
|
|
||||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
|
|
||||||
itemCount: servers.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final server = servers[index];
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
InkWell(
|
|
||||||
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
|
||||||
onTap: () async {
|
|
||||||
try {
|
|
||||||
KRLogUtil.kr_i('🔄 用户点击节点: ${server.tag}');
|
|
||||||
// 使用统一的节点切换方法,等待完成
|
|
||||||
final success = await controller
|
|
||||||
.kr_performNodeSwitch(server.tag);
|
|
||||||
|
|
||||||
// 只有切换成功才关闭列表
|
|
||||||
if (success) {
|
|
||||||
controller.kr_currentListStatus.value =
|
|
||||||
KRHomeViewsListStatus.kr_none;
|
|
||||||
KRLogUtil.kr_i('✅ 节点切换成功,关闭列表');
|
|
||||||
} else {
|
|
||||||
KRLogUtil.kr_w('❌ 节点切换失败,列表保持打开');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('❌ 节点切换异常: $e',
|
|
||||||
tag: 'NodeListView');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: _kr_buildNodeListItem(
|
|
||||||
context,
|
|
||||||
item: server,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (index < servers.length - 1)
|
|
||||||
Divider(
|
|
||||||
height: 1,
|
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Widget _kr_buildListPage(
|
|
||||||
BuildContext context, {
|
|
||||||
required String title,
|
|
||||||
VoidCallback? onBack,
|
|
||||||
required Widget listContent,
|
|
||||||
}) {
|
|
||||||
return Container(
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(20),
|
|
||||||
topRight: Radius.circular(20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_kr_buildTitleBar(
|
|
||||||
context,
|
|
||||||
title: title,
|
|
||||||
onBack: onBack,
|
|
||||||
onClose: () =>
|
|
||||||
controller.kr_currentListStatus.value = KRHomeViewsListStatus.kr_none,
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Expanded(child: listContent),
|
|
||||||
// 添加底部间距
|
|
||||||
SizedBox(height: 12),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 抽取公共的标题栏组件
|
|
||||||
Widget _kr_buildTitleBar(
|
|
||||||
BuildContext context, {
|
|
||||||
required String title,
|
|
||||||
VoidCallback? onBack,
|
|
||||||
VoidCallback? onClose,
|
|
||||||
}) {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
if (onBack != null) ...[
|
|
||||||
GestureDetector(
|
|
||||||
onTap: onBack,
|
|
||||||
child: Icon(
|
|
||||||
Icons.arrow_back_ios,
|
|
||||||
size: 20,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
],
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (onClose != null)
|
|
||||||
GestureDetector(
|
|
||||||
onTap: onClose,
|
|
||||||
child: Icon(
|
|
||||||
Icons.close,
|
|
||||||
size: 24,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// 构建列表容器
|
|
||||||
Widget _kr_buildListContainer(
|
|
||||||
BuildContext context, {
|
|
||||||
required Widget child,
|
|
||||||
}) {
|
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建节点列表项
|
|
||||||
Widget _kr_buildNodeListItem(
|
|
||||||
BuildContext context, {
|
|
||||||
required KROutboundItem item,
|
|
||||||
}) {
|
|
||||||
// 获取延迟颜色
|
|
||||||
Color getLatencyColor(int delay) {
|
|
||||||
if (delay == 0) {
|
|
||||||
return Colors.transparent;
|
|
||||||
} else if (delay < 500) {
|
|
||||||
return krModernGreen;
|
|
||||||
} else if (delay < 3000) {
|
|
||||||
return Color(0xFFFFB700); // 使用更容易看清的黄色
|
|
||||||
} else {
|
|
||||||
return Colors.red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
key: ValueKey(item.id),
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
// 🔧 修改:显示国旗代替图标
|
|
||||||
KRCountryFlag(
|
|
||||||
countryCode: item.country,
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
item.tag,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
Obx(
|
|
||||||
() => controller.kr_cutTag.value == item.tag
|
|
||||||
? Container(
|
|
||||||
margin: EdgeInsets.only(left: 4),
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 4, vertical: 1),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: krModernGreenLight.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
AppTranslations.kr_home.selected,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: krModernGreen,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 2),
|
|
||||||
Text(
|
|
||||||
item.city,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 显示延迟速度
|
|
||||||
GetBuilder<KRHomeController>(
|
|
||||||
id: item.tag,
|
|
||||||
builder: (controller) {
|
|
||||||
// 获取显示的延迟值
|
|
||||||
int displayDelay = _getDisplayDelay(controller, item);
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text(
|
|
||||||
displayDelay == 0
|
|
||||||
? ''
|
|
||||||
: displayDelay >= 3000
|
|
||||||
? AppTranslations.kr_home.timeout
|
|
||||||
: '${displayDelay}ms',
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: getLatencyColor(displayDelay),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改订阅列表视图
|
|
||||||
Widget _buildSubscribeList(BuildContext context) {
|
|
||||||
return _kr_buildListPage(
|
|
||||||
context,
|
|
||||||
title: AppTranslations.kr_home.nodeListTitle,
|
|
||||||
listContent: Obx(() {
|
|
||||||
if (controller.kr_subscribeService.allList.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: Text(
|
|
||||||
AppTranslations.kr_home.noNodes,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔧 修复无限刷新:自动触发延迟测试(仅在未连接状态下,且只触发一次)
|
|
||||||
if (!_hasTriggeredAutoTest) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (!controller.kr_isConnected.value && !controller.kr_isLatency.value && !_hasTriggeredAutoTest) {
|
|
||||||
_hasTriggeredAutoTest = true; // 标记已触发
|
|
||||||
KRLogUtil.kr_i('🔄 节点列表显示 - 自动触发延迟测试(首次)', tag: 'NodeListView');
|
|
||||||
controller.kr_urlTest();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return _kr_buildListContainer(
|
|
||||||
context,
|
|
||||||
child: ListView(
|
|
||||||
padding: EdgeInsets.fromLTRB(16, 0, 16, 0),
|
|
||||||
children: [
|
|
||||||
// 延迟测试按钮作为第一个列表项
|
|
||||||
InkWell(
|
|
||||||
onTap: () => controller.kr_urlTest(),
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 8),
|
|
||||||
margin: EdgeInsets.only(top: 8), // 添加上方间距
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: krModernGreenLight.withOpacity(0.1),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: controller.kr_isLatency.value
|
|
||||||
? KRSimpleLoading(
|
|
||||||
color: krModernGreen,
|
|
||||||
size: 24,
|
|
||||||
duration: const Duration(milliseconds: 800),
|
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
Icons.speed,
|
|
||||||
size: 24,
|
|
||||||
color: krModernGreen,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
controller.kr_isLatency.value
|
|
||||||
? AppTranslations.kr_home.testing
|
|
||||||
: AppTranslations.kr_home.testLatency,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: controller.kr_isLatency.value
|
|
||||||
? Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodySmall
|
|
||||||
?.color
|
|
||||||
: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodyMedium
|
|
||||||
?.color,
|
|
||||||
fontWeight: controller.kr_isLatency.value
|
|
||||||
? FontWeight.normal
|
|
||||||
: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!controller.kr_isLatency.value) ...[
|
|
||||||
SizedBox(height: 2),
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.refreshLatencyDesc,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodySmall
|
|
||||||
?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!controller.kr_isLatency.value)
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 12,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 分隔线
|
|
||||||
Divider(
|
|
||||||
height: 16,
|
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
// Auto 选项
|
|
||||||
InkWell(
|
|
||||||
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
|
||||||
onTap: () async {
|
|
||||||
try {
|
|
||||||
final success =
|
|
||||||
await controller.kr_performNodeSwitch('auto');
|
|
||||||
if (success) {
|
|
||||||
controller.kr_currentListStatus.value =
|
|
||||||
KRHomeViewsListStatus.kr_none;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('Auto选项切换异常: $e',
|
|
||||||
tag: 'NodeListView');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
KrLocalImage(
|
|
||||||
imageName: "home_list_location",
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
color: controller.kr_cutTag.value == 'auto'
|
|
||||||
? Colors.green
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.autoSelect,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodyMedium
|
|
||||||
?.color,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
if (controller.kr_cutTag.value == 'auto')
|
|
||||||
Container(
|
|
||||||
margin: EdgeInsets.only(left: 4),
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 4, vertical: 1),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color:
|
|
||||||
krModernGreenLight.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
AppTranslations.kr_home.selected,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: krModernGreen,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 2),
|
|
||||||
Obx(() {
|
|
||||||
// 获取当前自动选择的节点
|
|
||||||
String selectedNode =
|
|
||||||
AppTranslations.kr_home.autoSelect;
|
|
||||||
int delay = 0;
|
|
||||||
|
|
||||||
for (var group
|
|
||||||
in KRSingBoxImp.instance.kr_activeGroups) {
|
|
||||||
if (group.type == ProxyType.urltest) {
|
|
||||||
selectedNode = group.selected;
|
|
||||||
delay = controller
|
|
||||||
.kr_subscribeService.keyList[group.selected]
|
|
||||||
?.urlTestDelay.value ??
|
|
||||||
0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Text(
|
|
||||||
selectedNode,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodySmall
|
|
||||||
?.color,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Obx(() {
|
|
||||||
// 获取当前自动选择的节点
|
|
||||||
String selectedNode =
|
|
||||||
AppTranslations.kr_home.autoSelect;
|
|
||||||
int delay = 0;
|
|
||||||
|
|
||||||
for (var group
|
|
||||||
in KRSingBoxImp.instance.kr_activeGroups) {
|
|
||||||
if (group.type == ProxyType.urltest) {
|
|
||||||
selectedNode = group.selected;
|
|
||||||
delay = controller
|
|
||||||
.kr_subscribeService
|
|
||||||
.keyList[group.selected]
|
|
||||||
?.urlTestDelay
|
|
||||||
.value ??
|
|
||||||
0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return delay > 0
|
|
||||||
? Container(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text(
|
|
||||||
delay < 3000
|
|
||||||
? '${delay}ms'
|
|
||||||
: AppTranslations.kr_home.timeout,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: delay < 3000
|
|
||||||
? Colors.green
|
|
||||||
: Colors.red,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink();
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 分隔线
|
|
||||||
Divider(
|
|
||||||
height: 16,
|
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
// 节点列表
|
|
||||||
...controller.kr_subscribeService.allList
|
|
||||||
.map((node) => Column(
|
|
||||||
children: [
|
|
||||||
InkWell(
|
|
||||||
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
|
||||||
onTap: () async {
|
|
||||||
try {
|
|
||||||
KRLogUtil.kr_i(
|
|
||||||
'🔄 用户点击节点: ${node.tag}');
|
|
||||||
final success = await controller
|
|
||||||
.kr_performNodeSwitch(node.tag);
|
|
||||||
if (success) {
|
|
||||||
controller.kr_currentListStatus.value =
|
|
||||||
KRHomeViewsListStatus.kr_none;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e(
|
|
||||||
'节点切换异常: $e',
|
|
||||||
tag: 'NodeListView');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: _kr_buildNodeListItem(
|
|
||||||
context,
|
|
||||||
item: node,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (node !=
|
|
||||||
controller.kr_subscribeService.allList.last)
|
|
||||||
Divider(
|
|
||||||
height: 1,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.dividerColor
|
|
||||||
.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,223 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'dart:math';
|
|
||||||
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
|
|
||||||
import 'package:kaer_with_panels/app/common/app_run_data.dart';
|
|
||||||
import 'package:kaer_with_panels/app/model/response/kr_user_available_subscribe.dart';
|
|
||||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/dialogs/kr_dialog.dart';
|
|
||||||
|
|
||||||
class KRHomeSubscriptionView extends GetView<KRHomeController> {
|
|
||||||
const KRHomeSubscriptionView({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Obx(() {
|
|
||||||
if (!KRAppRunData().kr_isLogin.value) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
final currentSubscribe =
|
|
||||||
controller.kr_subscribeService.kr_currentSubscribe.value;
|
|
||||||
if (currentSubscribe == null) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('当前订阅名称: ${currentSubscribe.name}',
|
|
||||||
tag: 'SubscriptionView');
|
|
||||||
|
|
||||||
final totalTraffic = currentSubscribe.traffic;
|
|
||||||
final usedTraffic = currentSubscribe.download + currentSubscribe.upload;
|
|
||||||
final hasTrafficLimit = totalTraffic > 0;
|
|
||||||
var trafficPercentage =
|
|
||||||
hasTrafficLimit ? (usedTraffic / totalTraffic).clamp(0.0, 1.0) : 0.0;
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.bolt_rounded,
|
|
||||||
size: 14.w,
|
|
||||||
color: Theme.of(context).brightness == Brightness.light
|
|
||||||
? Colors.black54
|
|
||||||
: Colors.white54,
|
|
||||||
),
|
|
||||||
SizedBox(width: 4.w),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
currentSubscribe.name,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).brightness == Brightness.light
|
|
||||||
? Colors.black
|
|
||||||
: Colors.white,
|
|
||||||
fontSize: 12.sp,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 4.w),
|
|
||||||
Container(
|
|
||||||
height: 3.h,
|
|
||||||
width: 20.w,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).brightness == Brightness.light
|
|
||||||
? Colors.grey[200]
|
|
||||||
: Colors.grey[800],
|
|
||||||
borderRadius: BorderRadius.circular(1.5.r),
|
|
||||||
),
|
|
||||||
child: FractionallySizedBox(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
widthFactor: trafficPercentage,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: _getTrafficColor(trafficPercentage),
|
|
||||||
borderRadius: BorderRadius.circular(1.5.r),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 4.w),
|
|
||||||
Icon(
|
|
||||||
Icons.swap_horiz,
|
|
||||||
size: 14.w,
|
|
||||||
color: Theme.of(context).brightness == Brightness.light
|
|
||||||
? Colors.black.withOpacity(0.5)
|
|
||||||
: Colors.white.withOpacity(0.5),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildLoadingView(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
colors: [
|
|
||||||
Theme.of(context).brightness == Brightness.light
|
|
||||||
? Colors.white
|
|
||||||
: Colors.grey[900]!,
|
|
||||||
Theme.of(context).brightness == Brightness.light
|
|
||||||
? Colors.grey[50]!
|
|
||||||
: Colors.grey[800]!,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.1),
|
|
||||||
blurRadius: 15,
|
|
||||||
offset: const Offset(0, 5),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const SizedBox(
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.loading,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).brightness == Brightness.light
|
|
||||||
? Colors.black
|
|
||||||
: Colors.white,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconData _getStatusIcon(KRUserAvailableSubscribeItem subscribe) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
final expireTime = DateTime.parse(subscribe.expireTime);
|
|
||||||
final difference = expireTime.difference(now);
|
|
||||||
|
|
||||||
if (difference.isNegative) {
|
|
||||||
return Icons.error_outline_rounded;
|
|
||||||
} else if (difference.inDays <= 1) {
|
|
||||||
return Icons.warning_amber_rounded;
|
|
||||||
} else {
|
|
||||||
return Icons.check_circle_outline_rounded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _getStatusColor(
|
|
||||||
BuildContext context, KRUserAvailableSubscribeItem subscribe) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
final expireTime = DateTime.parse(subscribe.expireTime);
|
|
||||||
final difference = expireTime.difference(now);
|
|
||||||
|
|
||||||
if (difference.isNegative) {
|
|
||||||
return Colors.red;
|
|
||||||
} else if (difference.inDays <= 1) {
|
|
||||||
return Colors.orange;
|
|
||||||
} else {
|
|
||||||
return const Color(0xFF00E52B);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getStatusText(KRUserAvailableSubscribeItem subscribe) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
final expireTime = DateTime.parse(subscribe.expireTime);
|
|
||||||
final difference = expireTime.difference(now);
|
|
||||||
|
|
||||||
if (difference.isNegative) {
|
|
||||||
return '已过期';
|
|
||||||
} else if (difference.inDays <= 1) {
|
|
||||||
return '即将到期';
|
|
||||||
} else {
|
|
||||||
return '有效';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _getTrafficColor(double percentage) {
|
|
||||||
if (percentage >= 0.9) {
|
|
||||||
return Colors.red;
|
|
||||||
} else if (percentage >= 0.7) {
|
|
||||||
return Colors.orange;
|
|
||||||
} else {
|
|
||||||
return const Color(0xFF00E52B);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatTraffic(int bytes) {
|
|
||||||
if (bytes < 1024) {
|
|
||||||
return '$bytes B';
|
|
||||||
} else if (bytes < 1024 * 1024) {
|
|
||||||
return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
|
||||||
} else if (bytes < 1024 * 1024 * 1024) {
|
|
||||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
|
||||||
} else {
|
|
||||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatDate(String dateStr) {
|
|
||||||
try {
|
|
||||||
final date = DateTime.parse(dateStr);
|
|
||||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
|
||||||
} catch (e) {
|
|
||||||
return dateStr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,149 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
|
||||||
import '../../../routes/app_pages.dart';
|
|
||||||
import '../../../services/kr_subscribe_service.dart';
|
|
||||||
import '../../../utils/kr_log_util.dart';
|
|
||||||
import '../../../utils/kr_subscribe_navigation_util.dart';
|
|
||||||
import '../controllers/kr_home_controller.dart';
|
|
||||||
|
|
||||||
/// 试用卡片组件
|
|
||||||
class KRHomeTrialCard extends GetView<KRHomeController> {
|
|
||||||
const KRHomeTrialCard({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
width: double.infinity,
|
|
||||||
decoration: ShapeDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(14),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// 顶部标题和订阅按钮
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.trialStatus,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => KRSubscribeNavigationUtil.navigateToPurchase(tag: 'TrialCard'),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.subscribe,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 12,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
// 倒计时显示
|
|
||||||
SizedBox(height: 10),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.timer_outlined,
|
|
||||||
color: Colors.blue,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.trialing,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.blue,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildCountdown(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCountdown() {
|
|
||||||
return Obx(() {
|
|
||||||
final subscribeService = KRSubscribeService();
|
|
||||||
final remainingTime = subscribeService.kr_trialRemainingTime.value;
|
|
||||||
final isExpired = remainingTime.isEmpty;
|
|
||||||
|
|
||||||
return Builder(
|
|
||||||
builder: (context) => Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
remainingTime,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: isExpired
|
|
||||||
? (DateTime.now().millisecondsSinceEpoch % 2000 < 1000
|
|
||||||
? Colors.red
|
|
||||||
: Colors.blue)
|
|
||||||
: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 4.h),
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_home.trialEndMessage,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -11,9 +11,6 @@ import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
|
|||||||
import '../../../services/kr_subscribe_service.dart';
|
import '../../../services/kr_subscribe_service.dart';
|
||||||
import '../controllers/kr_home_controller.dart';
|
import '../controllers/kr_home_controller.dart';
|
||||||
import '../models/kr_home_views_status.dart';
|
import '../models/kr_home_views_status.dart';
|
||||||
import '../widgets/kr_subscribe_selector_view.dart';
|
|
||||||
import 'kr_home_bottom_panel.dart';
|
|
||||||
import 'kr_home_subscription_view.dart';
|
|
||||||
import './hi_animated_connect_button.dart';
|
import './hi_animated_connect_button.dart';
|
||||||
import 'package:kaer_with_panels/app/services/global_overlay_service.dart';
|
import 'package:kaer_with_panels/app/services/global_overlay_service.dart';
|
||||||
|
|
||||||
@ -138,18 +135,15 @@ class _KRHomeViewState extends State<KRHomeView> {
|
|||||||
style: highlightStyle,
|
style: highlightStyle,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// --- 情况2.2: 订阅有效 ---
|
// --- 情况2.2: 订阅有效 ---
|
||||||
final difference =
|
final formattedExpireTime = expireDateTime != null
|
||||||
expireDateTime?.difference(now);
|
? '${expireDateTime.year}/${expireDateTime.month.toString().padLeft(2, '0')}/${expireDateTime.day.toString().padLeft(2, '0')} ${expireDateTime.hour.toString().padLeft(2, '0')}:${expireDateTime.minute.toString().padLeft(2, '0')}'
|
||||||
final remainingDaysText =
|
: '未知';
|
||||||
(difference?.inDays ?? 0) > 0
|
// 使用换行符 \n 合并为单个 Text 组件
|
||||||
? '${difference!.inDays} 天'
|
content = Text(
|
||||||
: '不足一天';
|
'套餐到期时间:$formattedExpireTime\n${controller.kr_isConnected.value ? '当前线路:${controller.kr_getRealConnectedNodeCountry()}' : '未连接'}',
|
||||||
// 使用换行符 \n 合并为单个 Text 组件
|
style: normalStyle,
|
||||||
content = Text(
|
);
|
||||||
'套餐剩余:$remainingDaysText\n${controller.kr_isConnected.value ? '当前线路:${controller.kr_getRealConnectedNodeCountry()}' : '未连接'}',
|
|
||||||
style: normalStyle,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,7 +215,7 @@ class _KRHomeViewState extends State<KRHomeView> {
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
HIDialog.show(
|
HIDialog.show(
|
||||||
title: '*闪连功能',
|
title: '闪连功能',
|
||||||
message:
|
message:
|
||||||
'开启后,每次打开软件默认自动连接,无需点击连接按钮\n在后台关闭软件后,软件将自动断开',
|
'开启后,每次打开软件默认自动连接,无需点击连接按钮\n在后台关闭软件后,软件将自动断开',
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,275 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
|
|
||||||
import 'package:kaer_with_panels/app/model/response/kr_user_available_subscribe.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
|
||||||
|
|
||||||
class KRSubscribeSelectorView extends StatelessWidget {
|
|
||||||
final KRHomeController? controller;
|
|
||||||
|
|
||||||
const KRSubscribeSelectorView({
|
|
||||||
super.key,
|
|
||||||
this.controller,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final homeController = controller ?? Get.find<KRHomeController>();
|
|
||||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
width: MediaQuery.of(context).size.width * 0.85,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
borderRadius: BorderRadius.circular(20.r),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.1),
|
|
||||||
blurRadius: 10.r,
|
|
||||||
offset: Offset(0, 2.w),
|
|
||||||
spreadRadius: 0,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue.withOpacity(0.05),
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(20.r),
|
|
||||||
topRight: Radius.circular(20.r),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_purchaseMembership.selectPackage,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Theme.of(context).textTheme.titleLarge?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => Navigator.pop(context),
|
|
||||||
borderRadius: BorderRadius.circular(20.r),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(4.w),
|
|
||||||
child: Icon(
|
|
||||||
Icons.close_rounded,
|
|
||||||
color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.6),
|
|
||||||
size: 18.w,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Obx(() {
|
|
||||||
final subscribes = homeController.kr_subscribeService.kr_availableSubscribes;
|
|
||||||
if (subscribes.isEmpty) {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 12.w),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.subscriptions_outlined,
|
|
||||||
size: 48.w,
|
|
||||||
color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.3),
|
|
||||||
),
|
|
||||||
SizedBox(height: 12.h),
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_purchaseMembership.noData,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.5),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxHeight: MediaQuery.of(context).size.height * 0.5,
|
|
||||||
),
|
|
||||||
child: ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 4.h, horizontal: 4.w),
|
|
||||||
itemCount: subscribes.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final subscribe = subscribes[index];
|
|
||||||
final isCurrent = subscribe.id == homeController.kr_subscribeService.kr_currentSubscribe.value?.id;
|
|
||||||
|
|
||||||
return _SubscribeItem(
|
|
||||||
subscribe: subscribe,
|
|
||||||
isCurrent: isCurrent,
|
|
||||||
onTap: () {
|
|
||||||
homeController.kr_switchSubscribe(subscribe);
|
|
||||||
Get.back();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
SizedBox(height: 8.h),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SubscribeItem extends StatelessWidget {
|
|
||||||
final KRUserAvailableSubscribeItem subscribe;
|
|
||||||
final bool isCurrent;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
|
|
||||||
const _SubscribeItem({
|
|
||||||
required this.subscribe,
|
|
||||||
required this.isCurrent,
|
|
||||||
required this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final usedTraffic = (subscribe.download + subscribe.upload) / 1024 / 1024 / 1024;
|
|
||||||
final totalTraffic = subscribe.traffic / 1024 / 1024 / 1024;
|
|
||||||
var percentage = totalTraffic > 0 ? usedTraffic / totalTraffic : 0.0;
|
|
||||||
|
|
||||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
final isUnlimited = subscribe.traffic == 0;
|
|
||||||
|
|
||||||
String getUsedTrafficDisplay() {
|
|
||||||
if (usedTraffic < 1) {
|
|
||||||
return '${(usedTraffic * 1024).toStringAsFixed(2)}MB';
|
|
||||||
} else {
|
|
||||||
return '${usedTraffic.toStringAsFixed(2)}GB';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h),
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: onTap,
|
|
||||||
borderRadius: BorderRadius.circular(12.r),
|
|
||||||
child: Ink(
|
|
||||||
padding: EdgeInsets.all(12.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isCurrent
|
|
||||||
? Colors.blue.withOpacity(isDarkMode ? 0.15 : 0.08)
|
|
||||||
: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(12.r),
|
|
||||||
border: Border.all(
|
|
||||||
color: isCurrent
|
|
||||||
? Colors.blue.withOpacity(isDarkMode ? 0.5 : 0.3)
|
|
||||||
: Theme.of(context).dividerColor.withOpacity(0.3),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
subscribe.name,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Theme.of(context).textTheme.titleLarge?.color,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (isCurrent)
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue,
|
|
||||||
borderRadius: BorderRadius.circular(16.r),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.blue.withOpacity(0.2),
|
|
||||||
blurRadius: 6.w,
|
|
||||||
offset: Offset(0, 1.w),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
AppTranslations.kr_home.currentConnectionTitle,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 8.h),
|
|
||||||
Text(
|
|
||||||
isUnlimited
|
|
||||||
? AppTranslations.kr_purchaseMembership.unlimitedTraffic
|
|
||||||
: '${getUsedTrafficDisplay()} / ${totalTraffic.toStringAsFixed(2)}GB',
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!isUnlimited) ...[
|
|
||||||
SizedBox(height: 8.h),
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(4.r),
|
|
||||||
child: LinearProgressIndicator(
|
|
||||||
value: percentage.clamp(0.0, 1.0),
|
|
||||||
backgroundColor: isDarkMode
|
|
||||||
? Colors.grey[700]?.withOpacity(0.7)
|
|
||||||
: Colors.grey[300]?.withOpacity(0.9),
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
|
||||||
_getTrafficColor(percentage, isDarkMode),
|
|
||||||
),
|
|
||||||
minHeight: 4.w,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _getTrafficColor(double percentage, bool isDarkMode) {
|
|
||||||
if (percentage >= 0.9) {
|
|
||||||
return isDarkMode
|
|
||||||
? Colors.red.withOpacity(0.8)
|
|
||||||
: Colors.red.withOpacity(0.7);
|
|
||||||
} else if (percentage >= 0.7) {
|
|
||||||
return isDarkMode
|
|
||||||
? Colors.orange.withOpacity(0.8)
|
|
||||||
: Colors.orange.withOpacity(0.7);
|
|
||||||
} else {
|
|
||||||
return isDarkMode
|
|
||||||
? Colors.blue.withOpacity(0.8)
|
|
||||||
: Colors.blue.withOpacity(0.7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
|
||||||
import 'package:kaer_with_panels/app/routes/app_pages.dart';
|
|
||||||
import 'package:kaer_with_panels/app/utils/kr_subscribe_navigation_util.dart';
|
|
||||||
import '../../../widgets/kr_app_text_style.dart';
|
|
||||||
|
|
||||||
/// 订阅卡片组件
|
|
||||||
class KRSubscriptionCard extends StatelessWidget {
|
|
||||||
const KRSubscriptionCard({
|
|
||||||
super.key,
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return _kr_buildSubscriptionCard(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建订阅卡片
|
|
||||||
Widget _kr_buildSubscriptionCard(BuildContext context) {
|
|
||||||
// 🔧 关键修复:完全移除 ScreenUtil,使用固定像素值避免缩放问题
|
|
||||||
return Container(
|
|
||||||
// 添加固定高度,确保卡片可见
|
|
||||||
constraints: const BoxConstraints(minHeight: 200),
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// 图标
|
|
||||||
Container(
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue.withOpacity(0.1),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.language,
|
|
||||||
color: Colors.blue,
|
|
||||||
size: 28,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
// 描述文字
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Text(
|
|
||||||
AppTranslations.kr_home.subscriptionDescription,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
height: 1.5,
|
|
||||||
// 🔧 关键修复:确保文本颜色可见
|
|
||||||
color: Theme.of(context).brightness == Brightness.dark
|
|
||||||
? Colors.white
|
|
||||||
: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
// 订阅按钮
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 46,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
KRSubscribeNavigationUtil.navigateToPurchase(tag: 'SubscriptionCard');
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.blue,
|
|
||||||
elevation: 0,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
AppTranslations.kr_home.subscribe,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _kr_buildListContainer(
|
|
||||||
BuildContext context, {
|
|
||||||
required Widget child,
|
|
||||||
EdgeInsetsGeometry? margin,
|
|
||||||
bool addBottomPadding = true,
|
|
||||||
}) {
|
|
||||||
return Container(
|
|
||||||
margin: margin ?? EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: IntrinsicWidth(
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -43,6 +43,7 @@ class KRInviteController extends GetxController {
|
|||||||
totalCommission: 0,
|
totalCommission: 0,
|
||||||
).obs;
|
).obs;
|
||||||
final kr_referCode = ''.obs;
|
final kr_referCode = ''.obs;
|
||||||
|
final kr_refererId = 0.obs;
|
||||||
final kr_isLoading = false.obs;
|
final kr_isLoading = false.obs;
|
||||||
final count = 0.obs;
|
final count = 0.obs;
|
||||||
final EasyRefreshController refreshController = EasyRefreshController();
|
final EasyRefreshController refreshController = EasyRefreshController();
|
||||||
@ -65,6 +66,7 @@ class KRInviteController extends GetxController {
|
|||||||
totalCommission: 0,
|
totalCommission: 0,
|
||||||
);
|
);
|
||||||
kr_referCode.value = '';
|
kr_referCode.value = '';
|
||||||
|
kr_refererId.value = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (KRAppRunData.getInstance().kr_isLogin.value) {
|
if (KRAppRunData.getInstance().kr_isLogin.value) {
|
||||||
@ -87,11 +89,14 @@ class KRInviteController extends GetxController {
|
|||||||
KRLogUtil.kr_i(' - kr_isLogin: ${appData.kr_isLogin.value}', tag: 'InviteController');
|
KRLogUtil.kr_i(' - kr_isLogin: ${appData.kr_isLogin.value}', tag: 'InviteController');
|
||||||
KRLogUtil.kr_i(' - kr_account: ${appData.kr_account.value}', tag: 'InviteController');
|
KRLogUtil.kr_i(' - kr_account: ${appData.kr_account.value}', tag: 'InviteController');
|
||||||
KRLogUtil.kr_i(' - kr_referCode: ${appData.kr_referCode.value}', tag: 'InviteController');
|
KRLogUtil.kr_i(' - kr_referCode: ${appData.kr_referCode.value}', tag: 'InviteController');
|
||||||
|
KRLogUtil.kr_i(' - kr_refererId: ${appData.kr_refererId.value}', tag: 'InviteController');
|
||||||
KRLogUtil.kr_i(' - kr_balance: ${appData.kr_balance.value}', tag: 'InviteController');
|
KRLogUtil.kr_i(' - kr_balance: ${appData.kr_balance.value}', tag: 'InviteController');
|
||||||
KRLogUtil.kr_i(' - kr_commission: ${appData.kr_commission.value}', tag: 'InviteController');
|
KRLogUtil.kr_i(' - kr_commission: ${appData.kr_commission.value}', tag: 'InviteController');
|
||||||
|
|
||||||
kr_referCode.value = appData.kr_referCode.value;
|
kr_referCode.value = appData.kr_referCode.value;
|
||||||
|
kr_refererId.value = appData.kr_refererId.value;
|
||||||
KRLogUtil.kr_i('📋 [InviteController] 获取到邀请码: "${kr_referCode.value}"', tag: 'InviteController');
|
KRLogUtil.kr_i('📋 [InviteController] 获取到邀请码: "${kr_referCode.value}"', tag: 'InviteController');
|
||||||
|
KRLogUtil.kr_i('📋 [InviteController] 获取到邀请人ID: ${kr_refererId.value}', tag: 'InviteController');
|
||||||
|
|
||||||
if (kr_referCode.value.isEmpty) {
|
if (kr_referCode.value.isEmpty) {
|
||||||
KRLogUtil.kr_w('⚠️ [InviteController] 邀请码为空!', tag: 'InviteController');
|
KRLogUtil.kr_w('⚠️ [InviteController] 邀请码为空!', tag: 'InviteController');
|
||||||
@ -148,19 +153,23 @@ class KRInviteController extends GetxController {
|
|||||||
/// 处理绑定邀请码
|
/// 处理绑定邀请码
|
||||||
Future<void> kr_handleBindInviteCode() async {
|
Future<void> kr_handleBindInviteCode() async {
|
||||||
final text = otherInviteCodeController.text;
|
final text = otherInviteCodeController.text;
|
||||||
print('输入的邀请码是: $text');
|
|
||||||
if (text.isEmpty) {
|
if (text.isEmpty) {
|
||||||
KRCommonUtil.kr_showToast('请输入邀请码');
|
KRCommonUtil.kr_showToast('请输入邀请码');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (text.trim().toLowerCase() == kr_referCode.value.trim().toLowerCase()) {
|
||||||
|
KRCommonUtil.kr_showToast('您不可以邀请自己');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final either = await KRUserApi().hi_inviteCode(text);
|
final either = await KRUserApi().hi_inviteCode(text);
|
||||||
either.fold(
|
either.fold(
|
||||||
(error) => KRCommonUtil.kr_showToast(error.msg),
|
(error) => KRCommonUtil.kr_showToast(error.msg),
|
||||||
(affiliateCount) {
|
(success) {
|
||||||
KRCommonUtil.kr_showToast('绑定成功: $text');
|
KRCommonUtil.kr_showToast('绑定成功: $text');
|
||||||
otherInviteCodeController.text = '';
|
otherInviteCodeController.text = '';
|
||||||
|
_kr_fetchUserInfo(); // 刷新用户信息以更新 refererId 并隐藏输入框
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -29,187 +29,201 @@ class KRInviteView extends GetView<KRInviteController> {
|
|||||||
children: [
|
children: [
|
||||||
// 1. 背景层/滚动层
|
// 1. 背景层/滚动层
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: SingleChildScrollView(
|
child: LayoutBuilder(
|
||||||
// 保持手动管理,增强稳定性
|
builder: (context, constraints) {
|
||||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.manual,
|
return SingleChildScrollView(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
child: Padding(
|
child: ConstrainedBox(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||||
child: Column(
|
child: IntrinsicHeight(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Padding(
|
||||||
children: [
|
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
||||||
SizedBox(height: 20.w),
|
child: Column(
|
||||||
// 🟢 第一行:奖励说明
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
borderRadius: BorderRadius.circular(25.r),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
KrLocalImage(
|
|
||||||
imageName: 'hi-home-logo',
|
|
||||||
imageType: ImageType.svg,
|
|
||||||
width: 54.w,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
SizedBox(width: 16.w),
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
'受邀用户首次付款时,他将与您分别获得3天免费使用时长',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.black,
|
|
||||||
fontSize: 14.sp,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
SizedBox(height: 26.w),
|
|
||||||
// 🟢 第二行:我的邀请码
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: Colors.white, width: 2.0),
|
|
||||||
borderRadius: BorderRadius.circular(1000.r),
|
|
||||||
),
|
|
||||||
child: Obx(
|
|
||||||
() => Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
|
SizedBox(height: 20.w),
|
||||||
|
// 🟢 第一行:奖励说明
|
||||||
Container(
|
Container(
|
||||||
width: 100.w,
|
width: double.infinity,
|
||||||
height: 40.w,
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.w),
|
||||||
alignment: Alignment.center,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
|
borderRadius: BorderRadius.circular(25.r),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
KrLocalImage(
|
||||||
|
imageName: 'hi-home-logo',
|
||||||
|
imageType: ImageType.svg,
|
||||||
|
width: 54.w,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
SizedBox(width: 16.w),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
'受邀用户首次付款时,他将与您分别获得3天免费使用时长',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 14.sp,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 26.w),
|
||||||
|
// 🟢 第二行:我的邀请码
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.w),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.white, width: 2.0),
|
||||||
borderRadius: BorderRadius.circular(1000.r),
|
borderRadius: BorderRadius.circular(1000.r),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Obx(
|
||||||
'邀请码',
|
() => Row(
|
||||||
style: TextStyle(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
color: Colors.black,
|
children: [
|
||||||
fontSize: 16.sp,
|
Container(
|
||||||
fontWeight: FontWeight.w600,
|
width: 100.w,
|
||||||
|
height: 40.w,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
borderRadius: BorderRadius.circular(1000.r),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'邀请码',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 16.sp,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
controller.kr_referCode.value,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 20.sp,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const KrLocalImage(
|
||||||
|
imageName: 'share-icon',
|
||||||
|
imageType: ImageType.svg,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (controller.kr_referCode.value.isNotEmpty) {
|
||||||
|
final code = controller.kr_referCode.value;
|
||||||
|
final text = '#您的好友邀请您使用Hi快网络加速器\n'
|
||||||
|
'安装完毕后,在软件内<邀请好友>页面粘贴以下邀请码\n'
|
||||||
|
'$code\n'
|
||||||
|
'您和您的好友将会分别获得3天免费时长\n\n'
|
||||||
|
'点击此处进入下载页面\n'
|
||||||
|
'或在浏览器输入hifastvpn.com下载#';
|
||||||
|
if (GetPlatform.isIOS) {
|
||||||
|
Share.share(text, subject: '直接分享Hi快VPN邀请链接');
|
||||||
|
} else {
|
||||||
|
Clipboard.setData(ClipboardData(text: text));
|
||||||
|
KRCommonUtil.kr_showToast(AppTranslations.kr_invite.inviteCodeCopied);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
|
||||||
child: Text(
|
const Spacer(),
|
||||||
controller.kr_referCode.value,
|
|
||||||
textAlign: TextAlign.center,
|
// 🟢 第三部分:接受他人邀请
|
||||||
style: TextStyle(
|
Obx(() {
|
||||||
color: Colors.white,
|
// 只有当没有被邀请时才显示
|
||||||
fontSize: 20.sp,
|
if (controller.kr_refererId.value != 0) {
|
||||||
fontWeight: FontWeight.bold,
|
return const SizedBox.shrink();
|
||||||
),
|
}
|
||||||
),
|
return Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
IconButton(
|
children: [
|
||||||
icon: const KrLocalImage(
|
Text(
|
||||||
imageName: 'share-icon',
|
'接受他人邀请',
|
||||||
imageType: ImageType.svg,
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
fontSize: 14.sp,
|
||||||
onPressed: () {
|
fontWeight: FontWeight.bold,
|
||||||
if (controller.kr_referCode.value.isNotEmpty) {
|
),
|
||||||
final code = controller.kr_referCode.value;
|
),
|
||||||
final text = '#您的好友邀请您使用Hi快网络加速器\n'
|
SizedBox(height: 8.h),
|
||||||
'安装完毕后,在软件内<邀请好友>页面粘贴以下邀请码\n'
|
RepaintBoundary(
|
||||||
'$code\n'
|
child: TextField(
|
||||||
'您和您的好友将会分别获得3天免费时长\n\n'
|
controller: controller.otherInviteCodeController,
|
||||||
'点击此处进入下载页面\n'
|
textAlign: TextAlign.center,
|
||||||
'或在浏览器输入hifastvpn.com下载#';
|
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||||
if (GetPlatform.isIOS) {
|
decoration: InputDecoration(
|
||||||
Share.share(text, subject: '直接分享Hi快VPN邀请链接');
|
hintText: '填入邀请人邀请码兑换免费时长...',
|
||||||
} else {
|
hintStyle: const TextStyle(color: Color(0xFFA6A6A6)),
|
||||||
Clipboard.setData(ClipboardData(text: text));
|
filled: true,
|
||||||
KRCommonUtil.kr_showToast(AppTranslations.kr_invite.inviteCodeCopied);
|
fillColor: Colors.transparent,
|
||||||
}
|
contentPadding: EdgeInsets.symmetric(horizontal: 22.w),
|
||||||
}
|
border: OutlineInputBorder(
|
||||||
},
|
borderRadius: BorderRadius.circular(1000.r),
|
||||||
),
|
borderSide: const BorderSide(color: Colors.white, width: 2.0),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(1000.r),
|
||||||
|
borderSide: const BorderSide(color: Colors.white, width: 2.0),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(1000.r),
|
||||||
|
borderSide: const BorderSide(color: Colors.white, width: 2.0),
|
||||||
|
),
|
||||||
|
constraints: BoxConstraints(maxHeight: 50.h),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 10.w),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50.w,
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(1000.r),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () => controller.kr_handleBindInviteCode(),
|
||||||
|
child: Text(
|
||||||
|
'保存',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 16.sp,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
// 底部留白,确保键盘弹出后能滚过遮挡区域
|
||||||
|
SizedBox(height: 90.w),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 160.w),
|
),
|
||||||
// 🟢 第三部分:接受他人邀请
|
);
|
||||||
Column(
|
},
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'接受他人邀请',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 14.sp,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 8.h),
|
|
||||||
// 使用 RepaintBoundary 隔离,减少父级重绘对 TextField 的影响
|
|
||||||
RepaintBoundary(
|
|
||||||
child: TextField(
|
|
||||||
controller: controller.otherInviteCodeController,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: '填入邀请人邀请码兑换免费时长...',
|
|
||||||
hintStyle: const TextStyle(color: Color(0xFFA6A6A6)),
|
|
||||||
filled: true,
|
|
||||||
fillColor: Colors.transparent,
|
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 22.w),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(1000.r),
|
|
||||||
borderSide: const BorderSide(color: Colors.white, width: 2.0),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(1000.r),
|
|
||||||
borderSide: const BorderSide(color: Colors.white, width: 2.0),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(1000.r),
|
|
||||||
borderSide: const BorderSide(color: Colors.white, width: 2.0),
|
|
||||||
),
|
|
||||||
constraints: BoxConstraints(maxHeight: 50.h),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 10.w),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 50.w,
|
|
||||||
child: ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(1000.r),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () => controller.kr_handleBindInviteCode(),
|
|
||||||
child: Text(
|
|
||||||
'保存',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.black,
|
|
||||||
fontSize: 16.sp,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// 底部留白,确保键盘弹出后能滚过遮挡区域
|
|
||||||
SizedBox(height: 250.w),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
|
|
||||||
import '../controllers/kr_language_selector_controller.dart';
|
|
||||||
|
|
||||||
class KRLanguageSelectorBinding extends Bindings {
|
|
||||||
@override
|
|
||||||
void dependencies() {
|
|
||||||
Get.lazyPut<KRLanguageSelectorController>(
|
|
||||||
() => KRLanguageSelectorController(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
|
|
||||||
|
|
||||||
class KRLanguageSelectorController extends GetxController {
|
|
||||||
// 使用 KRLanguage 枚举来加载语言
|
|
||||||
final RxList<KRLanguage> kr_languages = <KRLanguage>[].obs;
|
|
||||||
// 当前选中的语言代码
|
|
||||||
final RxString kr_selectedLanguage = ''.obs;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
|
|
||||||
kr_selectedLanguage.value = KRLanguageUtils.getCurrentLanguage().countryCode;
|
|
||||||
kr_loadLanguages();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载语言数据
|
|
||||||
void kr_loadLanguages() {
|
|
||||||
// 将英语放在前面
|
|
||||||
final sortedLanguages = KRLanguage.values.toList()
|
|
||||||
..sort((a, b) => a == KRLanguage.en ? -1 : 1);
|
|
||||||
|
|
||||||
kr_languages.value = sortedLanguages;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选择语言
|
|
||||||
Future<void> kr_selectLanguage(KRLanguage language) async {
|
|
||||||
try {
|
|
||||||
// 先更新选中状态
|
|
||||||
kr_selectedLanguage.value = language.countryCode;
|
|
||||||
// 然后切换语言
|
|
||||||
await KRLanguageUtils.switchLanguage(language);
|
|
||||||
} catch (err) {
|
|
||||||
Get.snackbar(
|
|
||||||
'错误',
|
|
||||||
'切换语言失败: $err',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
|
||||||
import '../controllers/kr_language_selector_controller.dart';
|
|
||||||
|
|
||||||
class KRLanguageSelectorView extends GetView<KRLanguageSelectorController> {
|
|
||||||
const KRLanguageSelectorView({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
extendBodyBehindAppBar: true,
|
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
|
||||||
body: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [
|
|
||||||
Color.fromRGBO(23, 151, 255, 0.15), // 渐变开始颜色
|
|
||||||
Color.fromRGBO(23, 151, 255, 0.05), // 中间过渡颜色
|
|
||||||
// 非渐变色区域
|
|
||||||
],
|
|
||||||
stops: [0.0, 0.28], // 调整渐变结束位置
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
AppBar(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
elevation: 0,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.arrow_back_ios,
|
|
||||||
size: 20.r,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
AppTranslations.kr_setting.switchLanguage,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Obx(
|
|
||||||
() => ListView.separated(
|
|
||||||
padding: EdgeInsets.all(16.r),
|
|
||||||
itemCount: controller.kr_languages.length,
|
|
||||||
separatorBuilder: (context, index) => SizedBox(height: 12.h),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final language = controller.kr_languages[index];
|
|
||||||
return _kr_buildLanguageCard(language, context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建语言卡片
|
|
||||||
Widget _kr_buildLanguageCard(KRLanguage language, BuildContext context) {
|
|
||||||
return InkWell(
|
|
||||||
onTap: () => controller.kr_selectLanguage(language),
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.all(16.r),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(12.r),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
// 国旗图标
|
|
||||||
CircleAvatar(
|
|
||||||
radius: 16.r,
|
|
||||||
backgroundColor: Colors.blue.withOpacity(0.1),
|
|
||||||
child: Text(
|
|
||||||
language.flagEmoji,
|
|
||||||
style: TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 12.w),
|
|
||||||
// 语言名称
|
|
||||||
Text(
|
|
||||||
language.languageName,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
// 选中标记
|
|
||||||
if (controller.kr_selectedLanguage.value == language.countryCode)
|
|
||||||
Icon(
|
|
||||||
Icons.check_circle,
|
|
||||||
color: Colors.blue,
|
|
||||||
size: 20.r,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -80,6 +80,7 @@ class KRLoginController extends GetxController
|
|||||||
|
|
||||||
/// 验证码倒计时
|
/// 验证码倒计时
|
||||||
var _countdown = 60; // 倒计时初始值
|
var _countdown = 60; // 倒计时初始值
|
||||||
|
DateTime? _endTime; // 倒计时结束时间
|
||||||
late Timer _timer;
|
late Timer _timer;
|
||||||
var kr_countdownText = AppTranslations.kr_login.sendCode.obs;
|
var kr_countdownText = AppTranslations.kr_login.sendCode.obs;
|
||||||
|
|
||||||
@ -294,6 +295,11 @@ class KRLoginController extends GetxController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!validateEmail(accountController.text.trim())) {
|
||||||
|
KRCommonUtil.kr_showToast('请输入有效的邮箱地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (psdController.text.isEmpty) {
|
if (psdController.text.isEmpty) {
|
||||||
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword);
|
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword);
|
||||||
return;
|
return;
|
||||||
@ -306,7 +312,12 @@ class KRLoginController extends GetxController
|
|||||||
/// 发送验证码(仅支持邮箱)
|
/// 发送验证码(仅支持邮箱)
|
||||||
void kr_sendCode() async {
|
void kr_sendCode() async {
|
||||||
if (accountController.text.isEmpty) {
|
if (accountController.text.isEmpty) {
|
||||||
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterAccount);
|
KRCommonUtil.kr_showToast('请输入邮箱');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateEmail(accountController.text.trim())) {
|
||||||
|
KRCommonUtil.kr_showToast('请输入有效的邮箱地址');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,8 +367,14 @@ class KRLoginController extends GetxController
|
|||||||
/// 开始注册(仅支持邮箱,验证码和邀请码可选)
|
/// 开始注册(仅支持邮箱,验证码和邀请码可选)
|
||||||
void kr_register() async {
|
void kr_register() async {
|
||||||
// 验证邮箱
|
// 验证邮箱
|
||||||
if (accountController.text.isEmpty) {
|
final email = accountController.text.trim();
|
||||||
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterAccount);
|
if (email.isEmpty) {
|
||||||
|
KRCommonUtil.kr_showToast('请输入邮箱');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateEmail(email)) {
|
||||||
|
KRCommonUtil.kr_showToast('请输入有效的邮箱地址');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -509,21 +526,31 @@ class KRLoginController extends GetxController
|
|||||||
/// 开始倒计时
|
/// 开始倒计时
|
||||||
void _startCountdown() {
|
void _startCountdown() {
|
||||||
kr_canSendCode.value = false;
|
kr_canSendCode.value = false;
|
||||||
|
_endTime = DateTime.now().add(const Duration(seconds: 60));
|
||||||
|
|
||||||
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
_timer.cancel();
|
||||||
if (_countdown > 0) {
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
_countdown -= 1;
|
final now = DateTime.now();
|
||||||
kr_countdownText.value = "${_countdown}s";
|
if (_endTime != null && _endTime!.isAfter(now)) {
|
||||||
|
final remaining = _endTime!.difference(now).inSeconds;
|
||||||
|
_countdown = remaining;
|
||||||
|
kr_countdownText.value = "${remaining}s";
|
||||||
} else {
|
} else {
|
||||||
kr_canSendCode.value = true;
|
kr_canSendCode.value = true;
|
||||||
kr_countdownText.value = AppTranslations.kr_login.sendCode;
|
kr_countdownText.value = AppTranslations.kr_login.sendCode;
|
||||||
_countdown = 60;
|
_countdown = 60;
|
||||||
|
_endTime = null;
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
_onCountdownFinished();
|
_onCountdownFinished();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 验证邮箱格式
|
||||||
|
bool validateEmail(String str) {
|
||||||
|
return RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$').hasMatch(str);
|
||||||
|
}
|
||||||
|
|
||||||
/// 设置登录数据(仅支持邮箱)
|
/// 设置登录数据(仅支持邮箱)
|
||||||
void _saveLoginData(String token) {
|
void _saveLoginData(String token) {
|
||||||
KRAppRunData.getInstance().kr_saveUserInfo(
|
KRAppRunData.getInstance().kr_saveUserInfo(
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
|
|
||||||
import 'package:kaer_with_panels/app/model/kr_area_code.dart'; // 假设这个文件中有 KRAreaCode 类
|
|
||||||
|
|
||||||
class KRSearchAreaController extends GetxController {
|
|
||||||
final areas = <KRAreaCodeItem>[].obs;
|
|
||||||
final searchQuery = ''.obs;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
areas.assignAll(KRAreaCode.kr_getCodeList());
|
|
||||||
}
|
|
||||||
|
|
||||||
List<KRAreaCodeItem> get filteredAreas {
|
|
||||||
if (searchQuery.value.isEmpty) {
|
|
||||||
return areas;
|
|
||||||
} else {
|
|
||||||
return areas
|
|
||||||
.where((area) => area.kr_dialCode.toLowerCase().contains(searchQuery.value.toLowerCase()))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -117,46 +117,6 @@ class KRLoginView extends GetView<KRLoginController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContentByEntry() {
|
|
||||||
final entry = (Get.arguments as Map<String, dynamic>?)?['entry'];
|
|
||||||
if (entry == 'forget_psd') {
|
|
||||||
return _buildForgetPasswordLayout();
|
|
||||||
} else if (entry == 'bind_email') {
|
|
||||||
return _buildBindEmailLayout();
|
|
||||||
} else if (entry == 'login') {
|
|
||||||
return _buildLoginEmailLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _buildForgetPasswordLayout(); // 默认显示修改密码
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildForgetPasswordLayout() {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
constraints: BoxConstraints(minHeight: 300.w),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildStandardInputField(
|
|
||||||
controller: controller.psdController,
|
|
||||||
hintText: '新密码',
|
|
||||||
isPassword: true,
|
|
||||||
),
|
|
||||||
SizedBox(height: 10.w),
|
|
||||||
_buildStandardInputField(
|
|
||||||
controller: controller.agPsdController,
|
|
||||||
hintText: '确认密码',
|
|
||||||
isPassword: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 30.h),
|
|
||||||
_buildSaveButton(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBindEmailLayout() {
|
Widget _buildBindEmailLayout() {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@ -192,33 +152,6 @@ class KRLoginView extends GetView<KRLoginController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLoginEmailLayout() {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
constraints: BoxConstraints(minHeight: 300.w),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildStandardInputField(
|
|
||||||
controller: controller.accountController,
|
|
||||||
hintText: 'Email',
|
|
||||||
),
|
|
||||||
SizedBox(height: 10),
|
|
||||||
_buildStandardInputField(
|
|
||||||
controller: controller.psdController,
|
|
||||||
hintText: '密码',
|
|
||||||
isPassword: true,
|
|
||||||
),
|
|
||||||
SizedBox(height: 10.w),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 30.h),
|
|
||||||
_buildSaveButton(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建标准输入框
|
/// 构建标准输入框
|
||||||
Widget _buildStandardInputField({
|
Widget _buildStandardInputField({
|
||||||
required TextEditingController controller,
|
required TextEditingController controller,
|
||||||
@ -283,14 +216,6 @@ class KRLoginView extends GetView<KRLoginController> {
|
|||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
var v = value.replaceAll(RegExp("\\s+"), "");
|
var v = value.replaceAll(RegExp("\\s+"), "");
|
||||||
if (v.length % 2 == 0 && v.isNotEmpty) {
|
|
||||||
final half = v.length ~/ 2;
|
|
||||||
final first = v.substring(0, half);
|
|
||||||
final second = v.substring(half);
|
|
||||||
if (first == second) {
|
|
||||||
v = first;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const maxLen = 6;
|
const maxLen = 6;
|
||||||
if (v.length > maxLen) {
|
if (v.length > maxLen) {
|
||||||
v = v.substring(0, maxLen);
|
v = v.substring(0, maxLen);
|
||||||
|
|||||||
@ -1,131 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:kaer_with_panels/app/model/kr_area_code.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
|
||||||
|
|
||||||
import '../controllers/kr_search_area_controller.dart';
|
|
||||||
|
|
||||||
class KRSearchAreaView extends GetView<KRSearchAreaController> {
|
|
||||||
final Function(KRAreaCodeItem, int) onSelect;
|
|
||||||
|
|
||||||
const KRSearchAreaView({super.key, required this.onSelect});
|
|
||||||
|
|
||||||
static void show(Function(KRAreaCodeItem, int) onSelect) {
|
|
||||||
Get.dialog(
|
|
||||||
KRSearchAreaView(onSelect: onSelect),
|
|
||||||
barrierDismissible: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context); // 获取当前主题
|
|
||||||
Get.lazyPut<KRSearchAreaController>(
|
|
||||||
() => KRSearchAreaController(),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => Get.back(), // 点击背景关闭弹框
|
|
||||||
child: Scaffold(
|
|
||||||
backgroundColor: Colors.black.withOpacity(0.0),
|
|
||||||
body: Center(
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () {}, // 阻止点击事件传递到背景
|
|
||||||
child: Container(
|
|
||||||
width: 300.w,
|
|
||||||
height: 450.h,
|
|
||||||
padding: EdgeInsets.all(16.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.primaryColor,
|
|
||||||
borderRadius: BorderRadius.circular(15.w),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'login.selectOtherRegion'.tr,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 15.w,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: theme.textTheme.titleMedium?.color),
|
|
||||||
),
|
|
||||||
SizedBox(height: 10.h),
|
|
||||||
TextField(
|
|
||||||
onChanged: (value) => controller.searchQuery.value = value,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
prefixIcon: Icon(Icons.search, color: Colors.grey),
|
|
||||||
hintText: 'login.search'.tr,
|
|
||||||
hintStyle: TextStyle(color: Colors.grey),
|
|
||||||
filled: true,
|
|
||||||
// fillColor: Colors.grey.shade200,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8.w),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 10.h),
|
|
||||||
),
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(fontSize: 14.sp, fontFamily: 'AlibabaPuHuiTi-Regular',),
|
|
||||||
),
|
|
||||||
// SizedBox(height: 5.h),
|
|
||||||
Obx(() => Expanded(
|
|
||||||
child: ListView.builder(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: controller.filteredAreas.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final area = controller.filteredAreas[index];
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
onSelect(area, index); // 调用回调函数
|
|
||||||
Get.back();
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
vertical: 10.h, horizontal: 0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
width: 0.2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
|
|
||||||
Text(area.kr_icon,
|
|
||||||
style: TextStyle(fontSize: 20.w)),
|
|
||||||
SizedBox(width: 12.w),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
area.kr_name,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 13.w,
|
|
||||||
fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"+" + area.kr_dialCode,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 13.w,
|
|
||||||
fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -246,13 +246,13 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
|
|
||||||
/// 获取用户已订阅套餐
|
/// 获取用户已订阅套餐
|
||||||
Future<void> kr_getAlreadySubscribe() async {
|
Future<void> kr_getAlreadySubscribe() async {
|
||||||
final either = await _kr_subscribeApi.kr_getAlreadySubscribe();
|
final either = await _kr_subscribeApi.kr_getAlreadySubscribe(includeExpired: 'all');
|
||||||
either.fold(
|
either.fold(
|
||||||
(error) => KRCommonUtil.kr_showToast(error.msg),
|
(error) => KRCommonUtil.kr_showToast(error.msg),
|
||||||
(alreadySubscribe) {
|
(alreadySubscribe) {
|
||||||
_kr_alreadySubscribe = alreadySubscribe;
|
_kr_alreadySubscribe = alreadySubscribe;
|
||||||
KRLogUtil.kr_i(
|
KRLogUtil.kr_i(
|
||||||
'已订阅套餐: ${_kr_alreadySubscribe.map((e) => e.subscribeId).toList()}',
|
'已获取所有订阅记录(含过期): ${_kr_alreadySubscribe.map((e) => "ID:${e.userSubscribeId}, SubID:${e.subscribeId}").toList()}',
|
||||||
tag: 'PurchaseMembershipController');
|
tag: 'PurchaseMembershipController');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -558,19 +558,18 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
final quantity = kr_getSelectedQuantity();
|
final quantity = kr_getSelectedQuantity();
|
||||||
|
|
||||||
// 判断是续订还是新购
|
// 判断是续订还是新购:查找匹配选中套餐 ID 的最后一项记录(最新的记录,可能已过期)
|
||||||
final isRenewal = _kr_alreadySubscribe
|
final matchingSubscribes = _kr_alreadySubscribe
|
||||||
.any((subscribe) => subscribe.subscribeId == selectedPlan.kr_id);
|
.where((subscribe) => subscribe.subscribeId == selectedPlan.kr_id)
|
||||||
final subscribeId = isRenewal
|
.toList();
|
||||||
? _kr_alreadySubscribe
|
|
||||||
.firstWhere(
|
final bool isRenewal = matchingSubscribes.isNotEmpty;
|
||||||
(subscribe) => subscribe.subscribeId == selectedPlan.kr_id,
|
final int subscribeId = isRenewal
|
||||||
orElse: () =>
|
? matchingSubscribes.last.userSubscribeId
|
||||||
KRAlreadySubscribe(userSubscribeId: 0, subscribeId: 0), // 默认值
|
|
||||||
)
|
|
||||||
.userSubscribeId
|
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
|
print('📊 [Purchase] 订阅判断: isRenewal=$isRenewal, userSubscribeId=$subscribeId');
|
||||||
|
|
||||||
// 根据判断结果调用不同的接口
|
// 根据判断结果调用不同的接口
|
||||||
final purchaseEither = isRenewal
|
final purchaseEither = isRenewal
|
||||||
? await _kr_subscribeApi.kr_renewal(
|
? await _kr_subscribeApi.kr_renewal(
|
||||||
|
|||||||
@ -1,96 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:kaer_with_panels/app/model/response/kr_package_list.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
|
||||||
|
|
||||||
/// 套餐详情弹框
|
|
||||||
class KRPlanDetailsDialog extends StatelessWidget {
|
|
||||||
final List<KRFeature> kr_features;
|
|
||||||
|
|
||||||
const KRPlanDetailsDialog({
|
|
||||||
Key? key,
|
|
||||||
required this.kr_features,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Dialog(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_purchaseMembership.planDetails,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Flexible(
|
|
||||||
child: ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: kr_features.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final feature = kr_features[index];
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
feature.kr_label,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
...feature.kr_details.map((detail) => Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16, bottom: 8),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.check_circle_outline,
|
|
||||||
size: 16,
|
|
||||||
color: Colors.green,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
detail.kr_description,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
if (index < kr_features.length - 1)
|
|
||||||
const Divider(height: 24),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
|
|
||||||
import '../controllers/kr_setting_controller.dart';
|
|
||||||
|
|
||||||
class KRSettingBinding extends Bindings {
|
|
||||||
@override
|
|
||||||
void dependencies() {
|
|
||||||
Get.lazyPut<KRSettingController>(
|
|
||||||
() => KRSettingController(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,170 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
|
|
||||||
import 'package:kaer_with_panels/app/routes/app_pages.dart';
|
|
||||||
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
|
||||||
import 'package:kaer_with_panels/app/utils/kr_country_util.dart';
|
|
||||||
import '../../../localization/app_translations.dart';
|
|
||||||
import '../../../themes/kr_theme_service.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
|
||||||
import 'package:kaer_with_panels/app/common/app_run_data.dart';
|
|
||||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
|
||||||
|
|
||||||
class KRSettingController extends GetxController {
|
|
||||||
// 创建 AppTranslationsSetting 的实例
|
|
||||||
final AppTranslationsSetting kr_appTranslationsSetting =
|
|
||||||
AppTranslationsSetting();
|
|
||||||
|
|
||||||
// 当前选择的国家
|
|
||||||
final RxString kr_currentCountry = ''.obs;
|
|
||||||
|
|
||||||
// 自动连接开关
|
|
||||||
final RxBool kr_autoConnect = true.obs;
|
|
||||||
|
|
||||||
// 通知开关
|
|
||||||
final RxBool kr_notification = true.obs;
|
|
||||||
|
|
||||||
// 帮助改进开关
|
|
||||||
final RxBool kr_helpImprove = true.obs;
|
|
||||||
|
|
||||||
// 版本号
|
|
||||||
final RxString kr_version = ''.obs;
|
|
||||||
|
|
||||||
// IOS评分
|
|
||||||
final String kr_iosRating = '';
|
|
||||||
|
|
||||||
// 当前语言
|
|
||||||
final RxString kr_language = ''.obs;
|
|
||||||
|
|
||||||
// 当前主题选项
|
|
||||||
final RxString kr_themeOption = ''.obs;
|
|
||||||
|
|
||||||
final RxString kr_vpnMode = ''.obs;
|
|
||||||
|
|
||||||
final RxString kr_vpnModeRemark = ''.obs;
|
|
||||||
|
|
||||||
// 修改 VPN 模式切换方法
|
|
||||||
void kr_changeVPNMode(String mode) {
|
|
||||||
KRLogUtil.kr_i('设置的VPN模式文本: ${kr_vpnMode.value}', tag: 'SettingController');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换语言
|
|
||||||
void kr_changeLanguage() {
|
|
||||||
Get.toNamed(Routes.KR_LANGUAGE_SELECTOR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除账号
|
|
||||||
void kr_deleteAccount() {
|
|
||||||
// 检查是否已登录
|
|
||||||
if (!KRAppRunData.getInstance().kr_isLogin.value) {
|
|
||||||
// 如果未登录,跳转到登录页面
|
|
||||||
// Get.toNamed(Routes.MR_LOGIN);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 已登录,跳转到删除账号页面
|
|
||||||
Get.toNamed(Routes.KR_DELETE_ACCOUNT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
_loadThemeOption();
|
|
||||||
kr_language.value = KRLanguageUtils.getCurrentLanguage().languageName;
|
|
||||||
|
|
||||||
// 语言变化时更新所有翻译文本
|
|
||||||
ever(KRLanguageUtils.kr_language, (_) {
|
|
||||||
kr_language.value = KRLanguageUtils.kr_language.value;
|
|
||||||
_loadThemeOption();
|
|
||||||
|
|
||||||
kr_currentCountry.value = "";
|
|
||||||
kr_currentCountry.value = KRCountryUtil.kr_getCurrentCountryName();
|
|
||||||
|
|
||||||
kr_vpnMode.value = '';
|
|
||||||
kr_vpnMode.value =
|
|
||||||
kr_getConnectionTypeString(KRSingBoxImp().kr_connectionType.value);
|
|
||||||
|
|
||||||
kr_vpnModeRemark.value = '';
|
|
||||||
kr_vpnModeRemark.value = kr_getConnectionTypeRemark(KRSingBoxImp().kr_connectionType.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
ever(KRCountryUtil.kr_currentCountry, (_) {
|
|
||||||
kr_currentCountry.value = KRCountryUtil.kr_getCurrentCountryName();
|
|
||||||
});
|
|
||||||
|
|
||||||
kr_currentCountry.value = KRCountryUtil.kr_getCurrentCountryName();
|
|
||||||
kr_vpnMode.value =
|
|
||||||
kr_getConnectionTypeString(KRSingBoxImp().kr_connectionType.value);
|
|
||||||
kr_vpnModeRemark.value = kr_getConnectionTypeRemark(KRSingBoxImp().kr_connectionType.value);
|
|
||||||
_kr_getVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
String kr_getConnectionTypeString(KRConnectionType type) {
|
|
||||||
switch (type) {
|
|
||||||
case KRConnectionType.global:
|
|
||||||
return AppTranslations.kr_setting.connectionTypeGlobal;
|
|
||||||
case KRConnectionType.rule:
|
|
||||||
return AppTranslations.kr_setting.connectionTypeRule;
|
|
||||||
// case KRConnectionType.direct:
|
|
||||||
// return AppTranslations.kr_setting.connectionTypeDirect;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String kr_getConnectionTypeRemark(KRConnectionType type) {
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case KRConnectionType.global:
|
|
||||||
return AppTranslations.kr_setting.connectionTypeGlobalRemark;
|
|
||||||
case KRConnectionType.rule:
|
|
||||||
return AppTranslations.kr_setting.connectionTypeRuleRemark;
|
|
||||||
// case KRConnectionType.direct:
|
|
||||||
// return AppTranslations.kr_setting.connectionTypeDirectRemark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _loadThemeOption() async {
|
|
||||||
final KRThemeService themeService = KRThemeService();
|
|
||||||
await themeService.init();
|
|
||||||
|
|
||||||
switch (themeService.kr_Theme) {
|
|
||||||
case ThemeMode.system:
|
|
||||||
kr_themeOption.value = AppTranslations.kr_setting.system;
|
|
||||||
break;
|
|
||||||
case ThemeMode.light:
|
|
||||||
kr_themeOption.value = AppTranslations.kr_setting.light;
|
|
||||||
break;
|
|
||||||
case ThemeMode.dark:
|
|
||||||
kr_themeOption.value = AppTranslations.kr_setting.dark;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final count = 0.obs;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onReady() {
|
|
||||||
super.onReady();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onClose() {
|
|
||||||
super.onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void increment() => count.value++;
|
|
||||||
|
|
||||||
void kr_updateConnectionType(KRConnectionType newType) {
|
|
||||||
if (KRSingBoxImp().kr_connectionType.value != newType) {
|
|
||||||
KRLogUtil.kr_i('更新连接类型: $newType', tag: 'SettingController');
|
|
||||||
KRSingBoxImp().kr_updateConnectionType(newType);
|
|
||||||
kr_vpnMode.value = kr_getConnectionTypeString(newType);
|
|
||||||
kr_vpnModeRemark.value = kr_getConnectionTypeRemark(newType);
|
|
||||||
// 这里可以添加其他需要的逻辑
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取版本号
|
|
||||||
Future<void> _kr_getVersion() async {
|
|
||||||
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
|
||||||
kr_version.value = packageInfo.version;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,493 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
|
||||||
import '../../../services/singbox_imp/kr_sing_box_imp.dart';
|
|
||||||
import '../controllers/kr_setting_controller.dart';
|
|
||||||
import '../../../themes/kr_theme_service.dart';
|
|
||||||
import '../../../localization/app_translations.dart';
|
|
||||||
import '../../../routes/app_pages.dart';
|
|
||||||
import '../../../common/app_run_data.dart';
|
|
||||||
|
|
||||||
class KRSettingView extends GetView<KRSettingController> {
|
|
||||||
const KRSettingView({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
extendBodyBehindAppBar: true,
|
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
|
||||||
appBar: AppBar(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
elevation: 0,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.arrow_back_ios,
|
|
||||||
size: 20.r,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
title: Text(
|
|
||||||
AppTranslations.kr_setting.title,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: Obx(() {
|
|
||||||
return Container(
|
|
||||||
height: MediaQuery.of(context).size.height,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [
|
|
||||||
Color.fromRGBO(23, 151, 255, 0.15),
|
|
||||||
Color.fromRGBO(23, 151, 255, 0.05),
|
|
||||||
],
|
|
||||||
stops: [0.0, 0.3],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
physics: const BouncingScrollPhysics(),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
bottom: MediaQuery.of(context).padding.bottom),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(height: kToolbarHeight + 20.w),
|
|
||||||
_kr_buildSectionTitle(
|
|
||||||
context, AppTranslations.kr_setting.vpnConnection),
|
|
||||||
_kr_buildVPNSection(context),
|
|
||||||
_kr_buildSectionTitle(
|
|
||||||
context, AppTranslations.kr_setting.general),
|
|
||||||
_kr_buildGeneralSection(context),
|
|
||||||
SizedBox(height: 100.h),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _kr_buildSectionTitle(BuildContext context, String title) {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(16.w, 24.h, 16.w, 8.h),
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _kr_buildVPNSection(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.symmetric(horizontal: 16.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(12.r),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_kr_buildSelectionTile(
|
|
||||||
context,
|
|
||||||
title: AppTranslations.kr_setting.mode,
|
|
||||||
value: controller.kr_vpnMode.value,
|
|
||||||
// subtitle: controller.kr_vpnModeRemark.value,
|
|
||||||
onTap: () => _kr_showRouteRuleSelectionSheet(context),
|
|
||||||
),
|
|
||||||
_kr_buildDivider(),
|
|
||||||
// _kr_buildSwitchTile(
|
|
||||||
// context,
|
|
||||||
// title: AppTranslations.kr_setting.autoConnect,
|
|
||||||
// value: controller.kr_autoConnect,
|
|
||||||
// onChanged: (value) => controller.kr_autoConnect.value = value,
|
|
||||||
// ),
|
|
||||||
// _kr_buildDivider(),
|
|
||||||
// _kr_buildSelectionTile(
|
|
||||||
// context,
|
|
||||||
// title: AppTranslations.kr_setting.routeRule,
|
|
||||||
// value: controller.kr_routeRule.value,
|
|
||||||
// onTap: () => _kr_showRouteRuleSelectionSheet(context),
|
|
||||||
// ),
|
|
||||||
// _kr_buildDivider(),
|
|
||||||
_kr_buildSelectionTile(
|
|
||||||
context,
|
|
||||||
title: AppTranslations.kr_setting.countrySelector,
|
|
||||||
subtitle: AppTranslations.kr_setting.connectionTypeRuleRemark,
|
|
||||||
value: controller.kr_currentCountry.value,
|
|
||||||
onTap: () => Get.toNamed(Routes.KR_COUNTRY_SELECTOR),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _kr_showVPNModeSelectionSheet(BuildContext context) {
|
|
||||||
Get.bottomSheet(
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 16.r),
|
|
||||||
child: Wrap(
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
title: Center(
|
|
||||||
child: Text(AppTranslations.kr_setting.vpnModeSmart),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
controller.kr_changeVPNMode(AppTranslations.kr_setting.vpnModeSmart);
|
|
||||||
Get.back();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Center(
|
|
||||||
child: Text(AppTranslations.kr_setting.vpnModeSecure),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
controller.kr_changeVPNMode(AppTranslations.kr_setting.vpnModeSecure);
|
|
||||||
Get.back();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _kr_showRouteRuleSelectionSheet(BuildContext context) {
|
|
||||||
Get.bottomSheet(
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 16.r),
|
|
||||||
child: Wrap(
|
|
||||||
children: KRConnectionType.values.map((type) {
|
|
||||||
return ListTile(
|
|
||||||
title: Center(
|
|
||||||
child: Text(
|
|
||||||
controller.kr_getConnectionTypeString(type),
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
controller.kr_updateConnectionType(type);
|
|
||||||
Get.back();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _kr_buildGeneralSection(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.symmetric(horizontal: 16.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(12.r),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Obx(() => _kr_buildSelectionTile(
|
|
||||||
context,
|
|
||||||
title: AppTranslations.kr_setting.appearance,
|
|
||||||
value: controller.kr_themeOption.value,
|
|
||||||
onTap: () => _showThemeSelectionSheet(context),
|
|
||||||
)),
|
|
||||||
_kr_buildDivider(),
|
|
||||||
_kr_buildSwitchTile(
|
|
||||||
context,
|
|
||||||
title: AppTranslations.kr_setting.notifications,
|
|
||||||
value: controller.kr_notification,
|
|
||||||
onChanged: (value) => controller.kr_notification.value = value,
|
|
||||||
),
|
|
||||||
_kr_buildDivider(),
|
|
||||||
_kr_buildSwitchTile(
|
|
||||||
context,
|
|
||||||
title: AppTranslations.kr_setting.helpImprove,
|
|
||||||
value: controller.kr_helpImprove,
|
|
||||||
onChanged: (value) => controller.kr_helpImprove.value = value,
|
|
||||||
),
|
|
||||||
_kr_buildDivider(),
|
|
||||||
Obx(() {
|
|
||||||
final appRunData = KRAppRunData.getInstance();
|
|
||||||
final isLoggedIn = appRunData.kr_isLogin.value;
|
|
||||||
final isDeviceLogin = appRunData.isDeviceLogin();
|
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
|
||||||
// 未登录,不显示此项
|
|
||||||
return SizedBox.shrink();
|
|
||||||
} else if (isDeviceLogin) {
|
|
||||||
// 设备登录(游客模式),显示"点击这里登录/注册"
|
|
||||||
return _kr_buildActionTile(
|
|
||||||
context,
|
|
||||||
title: 'userInfo.loginRegister'.tr,
|
|
||||||
trailing: "",
|
|
||||||
onTap: () => Get.toNamed(Routes.MR_LOGIN),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 正常登录,显示用户邮箱
|
|
||||||
final userEmail = appRunData.kr_account.value ?? AppTranslations.kr_userInfo.myAccount;
|
|
||||||
return _kr_buildActionTile(
|
|
||||||
context,
|
|
||||||
title: userEmail,
|
|
||||||
trailing: AppTranslations.kr_setting.goToDelete,
|
|
||||||
onTap: controller.kr_deleteAccount,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
_kr_buildDivider(),
|
|
||||||
// _kr_buildTitleTile(
|
|
||||||
// context,
|
|
||||||
// title: AppTranslations.kr_setting.rateUs,
|
|
||||||
// ),
|
|
||||||
// _kr_buildDivider(),
|
|
||||||
Obx(() => _kr_buildValueTile(
|
|
||||||
context,
|
|
||||||
title: AppTranslations.kr_setting.version,
|
|
||||||
value: controller.kr_version.value,
|
|
||||||
)),
|
|
||||||
_kr_buildDivider(),
|
|
||||||
_kr_buildSelectionTile(
|
|
||||||
context,
|
|
||||||
title: AppTranslations.kr_setting.switchLanguage,
|
|
||||||
value: controller.kr_language.value,
|
|
||||||
onTap: controller.kr_changeLanguage,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showThemeSelectionSheet(BuildContext context) {
|
|
||||||
Get.bottomSheet(
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 16.r),
|
|
||||||
child: Wrap(
|
|
||||||
children: ThemeMode.values.map((option) {
|
|
||||||
String optionText;
|
|
||||||
switch (option) {
|
|
||||||
case ThemeMode.system:
|
|
||||||
optionText = AppTranslations.kr_setting.system;
|
|
||||||
break;
|
|
||||||
case ThemeMode.light:
|
|
||||||
optionText = AppTranslations.kr_setting.light;
|
|
||||||
break;
|
|
||||||
case ThemeMode.dark:
|
|
||||||
optionText = AppTranslations.kr_setting.dark;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return ListTile(
|
|
||||||
title: Center(
|
|
||||||
child: Text(
|
|
||||||
optionText,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () async {
|
|
||||||
final KRThemeService themeService = KRThemeService();
|
|
||||||
await themeService.kr_switchTheme(option);
|
|
||||||
|
|
||||||
controller.kr_themeOption.value = optionText;
|
|
||||||
Get.back();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _kr_buildSelectionTile(
|
|
||||||
BuildContext context, {
|
|
||||||
required String title,
|
|
||||||
required String value,
|
|
||||||
String? subtitle,
|
|
||||||
required VoidCallback onTap,
|
|
||||||
}) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(
|
|
||||||
title,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitle: subtitle != null
|
|
||||||
? Text(
|
|
||||||
subtitle,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 4.w),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 16.r,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: onTap,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _kr_buildSwitchTile(
|
|
||||||
BuildContext context, {
|
|
||||||
required String title,
|
|
||||||
String? subtitle,
|
|
||||||
required RxBool value,
|
|
||||||
required Function(bool) onChanged,
|
|
||||||
}) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(
|
|
||||||
title,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitle: subtitle != null
|
|
||||||
? Text(
|
|
||||||
subtitle,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
trailing: Obx(
|
|
||||||
() => CupertinoSwitch(
|
|
||||||
value: value.value,
|
|
||||||
onChanged: onChanged,
|
|
||||||
activeColor: Colors.blue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _kr_buildActionTile(
|
|
||||||
BuildContext context, {
|
|
||||||
required String title,
|
|
||||||
required String trailing,
|
|
||||||
required VoidCallback onTap,
|
|
||||||
}) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(
|
|
||||||
title,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: Text(
|
|
||||||
trailing,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: onTap,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _kr_buildTitleTile(
|
|
||||||
BuildContext context, {
|
|
||||||
required String title,
|
|
||||||
}) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(
|
|
||||||
title,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _kr_buildValueTile(
|
|
||||||
BuildContext context, {
|
|
||||||
required String title,
|
|
||||||
required String value,
|
|
||||||
}) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(
|
|
||||||
title,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: Text(
|
|
||||||
value,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _kr_buildDivider() {
|
|
||||||
return Divider(
|
|
||||||
height: 1.h,
|
|
||||||
thickness: 0.2,
|
|
||||||
color: const Color(0xFFEEEEEE),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
import '../controllers/kr_webview_controller.dart';
|
|
||||||
|
|
||||||
class KRWebViewBinding extends Bindings {
|
|
||||||
@override
|
|
||||||
void dependencies() {
|
|
||||||
Get.lazyPut<KRWebViewController>(
|
|
||||||
() => KRWebViewController(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,192 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:webview_flutter/webview_flutter.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
||||||
import 'dart:io' show Platform;
|
|
||||||
|
|
||||||
import 'package:kaer_with_panels/app/services/api_service/api.dart';
|
|
||||||
import 'package:kaer_with_panels/app/services/api_service/kr_web_api.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
|
||||||
|
|
||||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
|
||||||
|
|
||||||
/// WebView 控制器
|
|
||||||
/// 用于管理 WebView 的状态和行为
|
|
||||||
class KRWebViewController extends GetxController {
|
|
||||||
// 页面加载状态
|
|
||||||
final RxBool kr_isLoading = true.obs;
|
|
||||||
|
|
||||||
// 页面标题
|
|
||||||
final RxString kr_title = ''.obs;
|
|
||||||
|
|
||||||
// WebView 控制器
|
|
||||||
late final WebViewController kr_webViewController;
|
|
||||||
|
|
||||||
// 默认URL
|
|
||||||
static const String kr_defaultUrl = '';
|
|
||||||
|
|
||||||
final String kr_url = Get.arguments['url'] as String;
|
|
||||||
|
|
||||||
// Web API 实例
|
|
||||||
final KRWebApi _kr_webApi = KRWebApi();
|
|
||||||
|
|
||||||
// 内容类型
|
|
||||||
final RxBool kr_isHtml = false.obs;
|
|
||||||
final RxBool kr_isMarkdown = false.obs;
|
|
||||||
final RxString kr_content = ''.obs;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
// 根据 URL 类型决定初始化方式
|
|
||||||
if (kr_url.contains(Api.kr_getSiteTos) || kr_url.contains(Api.kr_getSitePrivacy)) {
|
|
||||||
// 用户协议和隐私政策页面,直接获取文本内容
|
|
||||||
if (kr_url.contains(Api.kr_getSiteTos)) {
|
|
||||||
kr_title.value = AppTranslations.kr_login.termsOfService;
|
|
||||||
} else {
|
|
||||||
kr_title.value = AppTranslations.kr_login.privacyPolicy;
|
|
||||||
}
|
|
||||||
kr_getWebText();
|
|
||||||
} else {
|
|
||||||
// 其他页面,初始化 WebView
|
|
||||||
kr_initWebView();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 初始化 WebView
|
|
||||||
void kr_initWebView() {
|
|
||||||
kr_webViewController = WebViewController()
|
|
||||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
|
||||||
..setNavigationDelegate(
|
|
||||||
NavigationDelegate(
|
|
||||||
onNavigationRequest: (NavigationRequest request) async {
|
|
||||||
// 只在移动平台处理支付应用跳转
|
|
||||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
|
||||||
KRLogUtil.kr_i('处理支付链接: ${request.url}', tag: 'WebViewController');
|
|
||||||
// 处理支付链接
|
|
||||||
if (await kr_handleUrlLaunch(request.url)) {
|
|
||||||
return NavigationDecision.prevent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NavigationDecision.navigate;
|
|
||||||
},
|
|
||||||
onPageStarted: kr_handlePageStarted,
|
|
||||||
onPageFinished: kr_handlePageFinished,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 检查是否是用户协议或隐私政策
|
|
||||||
if (kr_url.contains(Api.kr_getSiteTos) || kr_url.contains(Api.kr_getSitePrivacy)) {
|
|
||||||
kr_getWebText();
|
|
||||||
} else {
|
|
||||||
kr_webViewController.loadRequest(Uri.parse(kr_url));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取网页文本内容并加载到 WebView
|
|
||||||
Future<void> kr_getWebText() async {
|
|
||||||
try {
|
|
||||||
final response = await _kr_webApi.kr_getWebText(kr_url);
|
|
||||||
response.fold(
|
|
||||||
(error) async {
|
|
||||||
KRLogUtil.kr_e('获取网页内容失败: $error', tag: 'WebViewController');
|
|
||||||
// 如果获取失败,直接设置错误内容
|
|
||||||
kr_content.value = 'Failed to load, please try again later';
|
|
||||||
kr_isLoading.value = false;
|
|
||||||
},
|
|
||||||
(content) async {
|
|
||||||
KRLogUtil.kr_i('获取到内容: $content', tag: 'WebViewController');
|
|
||||||
// 判断内容类型,优先判断 Markdown
|
|
||||||
kr_isMarkdown.value = content.contains('**') ||
|
|
||||||
content.contains('*') ||
|
|
||||||
content.contains('#') ||
|
|
||||||
content.contains('- ') ||
|
|
||||||
content.contains('[');
|
|
||||||
kr_isHtml.value = !kr_isMarkdown.value && content.contains('<') && content.contains('>');
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('内容类型 - Markdown: ${kr_isMarkdown.value}, HTML: ${kr_isHtml.value}', tag: 'WebViewController');
|
|
||||||
|
|
||||||
if (kr_isMarkdown.value) {
|
|
||||||
// 如果是 Markdown 内容,直接使用
|
|
||||||
kr_content.value = content;
|
|
||||||
} else if (kr_isHtml.value) {
|
|
||||||
// 如果是 HTML 内容,直接使用
|
|
||||||
kr_content.value = content;
|
|
||||||
} else {
|
|
||||||
// 如果是普通文本,直接使用
|
|
||||||
kr_content.value = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
kr_isLoading.value = false;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('获取网页内容出错: $e', tag: 'WebViewController');
|
|
||||||
kr_content.value = 'Loading error, please try again later';
|
|
||||||
kr_isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理页面开始加载事件
|
|
||||||
void kr_handlePageStarted(String url) {
|
|
||||||
kr_isLoading.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理页面加载完成事件
|
|
||||||
void kr_handlePageFinished(String url) async {
|
|
||||||
kr_isLoading.value = false;
|
|
||||||
await kr_updateTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 更新页面标题
|
|
||||||
Future<void> kr_updateTitle() async {
|
|
||||||
final String? kr_pageTitle = await kr_webViewController.getTitle();
|
|
||||||
kr_title.value = kr_pageTitle ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 重新加载页面
|
|
||||||
Future<void> kr_reloadPage() async {
|
|
||||||
await kr_webViewController.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 加载新的URL
|
|
||||||
Future<void> kr_loadUrl(String url) async {
|
|
||||||
await kr_webViewController.loadRequest(Uri.parse(url));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理URL启动
|
|
||||||
Future<bool> kr_handleUrlLaunch(String url) async {
|
|
||||||
try {
|
|
||||||
KRLogUtil.kr_i('正在处理URL跳转: $url', tag: 'WebViewController');
|
|
||||||
final uri = Uri.parse(url);
|
|
||||||
// 处理支付应用和外部链接
|
|
||||||
if (uri.scheme == 'alipays' ||
|
|
||||||
uri.scheme == 'alipay' ||
|
|
||||||
uri.scheme == 'weixin' ||
|
|
||||||
uri.scheme == 'wx') {
|
|
||||||
KRLogUtil.kr_i('检测到支付应用scheme: ${uri.scheme}', tag: 'WebViewController');
|
|
||||||
// 尝试打开支付应用
|
|
||||||
if (await canLaunchUrl(uri)) {
|
|
||||||
return await launchUrl(
|
|
||||||
uri,
|
|
||||||
mode: LaunchMode.externalApplication,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 如果支付应用无法打开,尝试使用外部浏览器打开
|
|
||||||
final httpUri = Uri.parse('https://${uri.host}${uri.path}?${uri.query}');
|
|
||||||
KRLogUtil.kr_i('尝试使用浏览器打开: $httpUri', tag: 'WebViewController');
|
|
||||||
if (await canLaunchUrl(httpUri)) {
|
|
||||||
return await launchUrl(
|
|
||||||
httpUri,
|
|
||||||
mode: LaunchMode.externalApplication,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
KRLogUtil.kr_e('无法启动URL: $url', tag: 'WebViewController');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('URL跳转错误: $e', tag: 'WebViewController');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,177 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:webview_flutter/webview_flutter.dart';
|
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
|
||||||
import '../../../widgets/kr_app_text_style.dart';
|
|
||||||
import '../controllers/kr_webview_controller.dart';
|
|
||||||
import '../../../services/api_service/api.dart';
|
|
||||||
import '../../../utils/kr_log_util.dart';
|
|
||||||
|
|
||||||
/// WebView 页面组件
|
|
||||||
class KRWebView extends GetView<KRWebViewController> {
|
|
||||||
const KRWebView({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
elevation: 0,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.arrow_back_ios,
|
|
||||||
size: 20.sp,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
title: Text(
|
|
||||||
controller.kr_title.value,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: _buildBody(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建主体内容
|
|
||||||
Widget _buildBody() {
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
_buildContent(),
|
|
||||||
_buildLoadingIndicator(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建内容组件
|
|
||||||
Widget _buildContent() {
|
|
||||||
if (controller.kr_url.contains(Api.kr_getSiteTos) ||
|
|
||||||
controller.kr_url.contains(Api.kr_getSitePrivacy)) {
|
|
||||||
return _buildProtocolContent();
|
|
||||||
} else {
|
|
||||||
return _buildWebView();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建协议内容
|
|
||||||
Widget _buildProtocolContent() {
|
|
||||||
return Obx(() {
|
|
||||||
if (controller.kr_isHtml.value) {
|
|
||||||
return SingleChildScrollView(
|
|
||||||
padding: EdgeInsets.all(16.w),
|
|
||||||
child: Html(
|
|
||||||
data: controller.kr_content.value,
|
|
||||||
style: {
|
|
||||||
'body': Style(
|
|
||||||
margin: Margins.all(0),
|
|
||||||
padding: HtmlPaddings.all(0),
|
|
||||||
fontSize: FontSize(14.sp),
|
|
||||||
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
|
|
||||||
fontFamily: 'AlibabaPuHuiTi-Regular',
|
|
||||||
lineHeight: LineHeight(1.4),
|
|
||||||
),
|
|
||||||
'p': Style(
|
|
||||||
margin: Margins.only(bottom: 8.h),
|
|
||||||
),
|
|
||||||
'b': Style(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
'i': Style(
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
),
|
|
||||||
'a': Style(
|
|
||||||
color: Colors.blue,
|
|
||||||
textDecoration: TextDecoration.underline,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
shrinkWrap: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (controller.kr_isMarkdown.value) {
|
|
||||||
return SingleChildScrollView(
|
|
||||||
padding: EdgeInsets.all(16.w),
|
|
||||||
child: MarkdownBody(
|
|
||||||
data: controller.kr_content.value,
|
|
||||||
styleSheet: MarkdownStyleSheet(
|
|
||||||
p: TextStyle(
|
|
||||||
fontSize: 14.sp,
|
|
||||||
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
|
|
||||||
fontFamily: 'AlibabaPuHuiTi-Regular',
|
|
||||||
height: 1.4,
|
|
||||||
),
|
|
||||||
strong: TextStyle(
|
|
||||||
fontSize: 14.sp,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
|
|
||||||
fontFamily: 'AlibabaPuHuiTi-Regular',
|
|
||||||
height: 1.4,
|
|
||||||
),
|
|
||||||
em: TextStyle(
|
|
||||||
fontSize: 14.sp,
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
|
|
||||||
fontFamily: 'AlibabaPuHuiTi-Regular',
|
|
||||||
height: 1.4,
|
|
||||||
),
|
|
||||||
a: TextStyle(
|
|
||||||
fontSize: 14.sp,
|
|
||||||
color: Colors.blue,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
fontFamily: 'AlibabaPuHuiTi-Regular',
|
|
||||||
height: 1.4,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return SingleChildScrollView(
|
|
||||||
padding: EdgeInsets.all(16.w),
|
|
||||||
child: Text(
|
|
||||||
controller.kr_content.value,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14.sp,
|
|
||||||
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
|
|
||||||
fontFamily: 'AlibabaPuHuiTi-Regular',
|
|
||||||
height: 1.4,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建 WebView 组件
|
|
||||||
Widget _buildWebView() {
|
|
||||||
return WebViewWidget(
|
|
||||||
controller: controller.kr_webViewController,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建加载指示器
|
|
||||||
Widget _buildLoadingIndicator() {
|
|
||||||
return Obx(
|
|
||||||
() => controller.kr_isLoading.value
|
|
||||||
? const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 显示错误提示
|
|
||||||
void _showErrorSnackbar(String title, String message) {
|
|
||||||
Get.snackbar(
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -14,6 +14,7 @@ import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
|
|||||||
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
|
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
|
||||||
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
||||||
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
|
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
|
||||||
|
import 'package:kaer_with_panels/app/utils/kr_http_adapter_util.dart';
|
||||||
import 'package:kaer_with_panels/singbox/model/singbox_status.dart';
|
import 'package:kaer_with_panels/singbox/model/singbox_status.dart';
|
||||||
|
|
||||||
// import 'package:crypto/crypto.dart';
|
// import 'package:crypto/crypto.dart';
|
||||||
@ -74,52 +75,11 @@ class HttpUtil {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 🔧 配置HttpClientAdapter 优先走本地 sing-box mixed 端口,
|
// 🔧 配置HttpClientAdapter 优先走本地 sing-box mixed 端口,
|
||||||
// 若代理不可用则回退到直连
|
// 若代理不可用则回推到直连
|
||||||
KRLogUtil.kr_i('🔧 配置 HttpClientAdapter...', tag: 'HttpUtil');
|
KRLogUtil.kr_i('🔧 配置 HttpClientAdapter...', tag: 'HttpUtil');
|
||||||
_dio.httpClientAdapter = IOHttpClientAdapter(
|
_dio.httpClientAdapter = KRHttpAdapterUtil.createAdapter(
|
||||||
createHttpClient: () {
|
useSingBoxProxy: true,
|
||||||
KRLogUtil.kr_i('📱 createHttpClient 回调被调用', tag: 'HttpUtil');
|
timeout: const Duration(seconds: 10),
|
||||||
final client = HttpClient();
|
|
||||||
|
|
||||||
// ✅ 优化:智能代理回退逻辑
|
|
||||||
client.findProxy = (url) {
|
|
||||||
try {
|
|
||||||
// 检查 SingBox 是否正在运行
|
|
||||||
final singBoxStatus = KRSingBoxImp.instance.kr_status;
|
|
||||||
final isProxyAvailable = singBoxStatus == SingboxStatus.started();
|
|
||||||
|
|
||||||
if (!isProxyAvailable) {
|
|
||||||
// 代理未运行,直接使用直连
|
|
||||||
KRLogUtil.kr_i(
|
|
||||||
'🔄 代理未运行,使用直连模式: $url',
|
|
||||||
tag: 'HttpUtil',
|
|
||||||
);
|
|
||||||
return 'DIRECT';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 代理正在运行,使用代理配置
|
|
||||||
final proxyConfig = KRSingBoxImp.instance.kr_buildProxyRule();
|
|
||||||
KRLogUtil.kr_i(
|
|
||||||
'✅ 使用代理模式, url: $url, proxy: $proxyConfig',
|
|
||||||
tag: 'HttpUtil',
|
|
||||||
);
|
|
||||||
return proxyConfig;
|
|
||||||
} catch (e) {
|
|
||||||
// 发生异常时回退到直连
|
|
||||||
KRLogUtil.kr_w(
|
|
||||||
'⚠️ 代理配置异常,回退到直连: $e',
|
|
||||||
tag: 'HttpUtil',
|
|
||||||
);
|
|
||||||
return 'DIRECT';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ✅ 优化:设置连接失败时的自动回退
|
|
||||||
client.connectionTimeout = const Duration(seconds: 10);
|
|
||||||
client.badCertificateCallback = (cert, host, port) => true;
|
|
||||||
|
|
||||||
return client;
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
KRLogUtil.kr_i('✅ HttpUtil.initDio() 初始化完成', tag: 'HttpUtil');
|
KRLogUtil.kr_i('✅ HttpUtil.initDio() 初始化完成', tag: 'HttpUtil');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,10 +8,6 @@ import 'package:kaer_with_panels/app/modules/hi_node_list/views/hi_page_node_vie
|
|||||||
import 'package:kaer_with_panels/app/modules/hi_user_info/bindings/hi_user_info_binding.dart';
|
import 'package:kaer_with_panels/app/modules/hi_user_info/bindings/hi_user_info_binding.dart';
|
||||||
import 'package:kaer_with_panels/app/modules/hi_user_info/views/hi_user_info_view.dart';
|
import 'package:kaer_with_panels/app/modules/hi_user_info/views/hi_user_info_view.dart';
|
||||||
|
|
||||||
import '../modules/kr_language_selector/bindings/kr_language_selector_binding.dart';
|
|
||||||
import '../modules/kr_language_selector/views/kr_language_selector_view.dart';
|
|
||||||
import '../modules/kr_country_selector/bindings/kr_country_selector_binding.dart';
|
|
||||||
import '../modules/kr_country_selector/views/kr_country_selector_view.dart';
|
|
||||||
import '../modules/kr_crisp_chat/bindings/kr_crisp_binding.dart';
|
import '../modules/kr_crisp_chat/bindings/kr_crisp_binding.dart';
|
||||||
import '../modules/kr_crisp_chat/views/kr_crisp_view.dart';
|
import '../modules/kr_crisp_chat/views/kr_crisp_view.dart';
|
||||||
import '../modules/kr_delete_account/bindings/kr_delete_account_binding.dart';
|
import '../modules/kr_delete_account/bindings/kr_delete_account_binding.dart';
|
||||||
@ -26,16 +22,10 @@ import '../modules/kr_message/bindings/kr_message_binding.dart';
|
|||||||
import '../modules/kr_message/views/kr_message_view.dart';
|
import '../modules/kr_message/views/kr_message_view.dart';
|
||||||
import '../modules/kr_purchase_membership/bindings/kr_purchase_membership_binding.dart';
|
import '../modules/kr_purchase_membership/bindings/kr_purchase_membership_binding.dart';
|
||||||
import '../modules/kr_purchase_membership/views/kr_purchase_membership_view.dart';
|
import '../modules/kr_purchase_membership/views/kr_purchase_membership_view.dart';
|
||||||
import '../modules/kr_setting/bindings/kr_setting_binding.dart';
|
|
||||||
import '../modules/kr_setting/views/kr_setting_view.dart';
|
|
||||||
import '../modules/kr_webview/bindings/kr_webview_binding.dart';
|
|
||||||
import '../modules/kr_webview/views/kr_webview_view.dart';
|
|
||||||
import '../modules/kr_order_status/bindings/kr_order_status_binding.dart';
|
import '../modules/kr_order_status/bindings/kr_order_status_binding.dart';
|
||||||
import '../modules/kr_order_status/views/kr_order_status_view.dart';
|
import '../modules/kr_order_status/views/kr_order_status_view.dart';
|
||||||
import '../modules/kr_splash/bindings/kr_splash_binding.dart';
|
import '../modules/kr_splash/bindings/kr_splash_binding.dart';
|
||||||
import '../modules/kr_splash/views/kr_splash_view.dart';
|
import '../modules/kr_splash/views/kr_splash_view.dart';
|
||||||
import '../modules/kr_device_management/bindings/kr_device_management_binding.dart';
|
|
||||||
import '../modules/kr_device_management/views/kr_device_management_view.dart';
|
|
||||||
import 'package:kaer_with_panels/app/routes/transitions/slide_transparent_transition.dart';
|
import 'package:kaer_with_panels/app/routes/transitions/slide_transparent_transition.dart';
|
||||||
import 'package:kaer_with_panels/app/widgets/swipe/swipe_wrapper.dart';
|
import 'package:kaer_with_panels/app/widgets/swipe/swipe_wrapper.dart';
|
||||||
|
|
||||||
@ -84,30 +74,12 @@ class AppPages {
|
|||||||
popGesture: false,
|
popGesture: false,
|
||||||
arguments: {'showSubscriptionButton': true}, // 显示购买按钮
|
arguments: {'showSubscriptionButton': true}, // 显示购买按钮
|
||||||
),
|
),
|
||||||
GetPage(
|
|
||||||
name: _Paths.KR_SETTING,
|
|
||||||
page: () => SwipeWrapper.detect(() => const KRSettingView()),
|
|
||||||
binding: KRSettingBinding(),
|
|
||||||
popGesture: false,
|
|
||||||
),
|
|
||||||
GetPage(
|
GetPage(
|
||||||
name: _Paths.KR_INVITE,
|
name: _Paths.KR_INVITE,
|
||||||
page: () => SwipeWrapper.detect(() => const KRInviteView()),
|
page: () => SwipeWrapper.detect(() => const KRInviteView()),
|
||||||
binding: KRInviteBinding(),
|
binding: KRInviteBinding(),
|
||||||
popGesture: false,
|
popGesture: false,
|
||||||
),
|
),
|
||||||
GetPage(
|
|
||||||
name: _Paths.KR_LANGUAGE_SELECTOR,
|
|
||||||
page: () => SwipeWrapper.detect(() => const KRLanguageSelectorView()),
|
|
||||||
binding: KRLanguageSelectorBinding(),
|
|
||||||
popGesture: false,
|
|
||||||
),
|
|
||||||
GetPage(
|
|
||||||
name: _Paths.KR_COUNTRY_SELECTOR,
|
|
||||||
page: () => SwipeWrapper.detect(() => const KRCountrySelectorView()),
|
|
||||||
binding: KRCountrySelectorBinding(),
|
|
||||||
popGesture: false,
|
|
||||||
),
|
|
||||||
GetPage(
|
GetPage(
|
||||||
name: _Paths.KR_PURCHASE_MEMBERSHIP,
|
name: _Paths.KR_PURCHASE_MEMBERSHIP,
|
||||||
page: () => SwipeWrapper.detect(() => const KRPurchaseMembershipView()),
|
page: () => SwipeWrapper.detect(() => const KRPurchaseMembershipView()),
|
||||||
@ -127,12 +99,6 @@ class AppPages {
|
|||||||
binding: KrDeleteAccountBinding(),
|
binding: KrDeleteAccountBinding(),
|
||||||
popGesture: false,
|
popGesture: false,
|
||||||
),
|
),
|
||||||
GetPage(
|
|
||||||
name: Routes.KR_WEBVIEW,
|
|
||||||
page: () => SwipeWrapper.detect(() => const KRWebView()),
|
|
||||||
binding: KRWebViewBinding(),
|
|
||||||
popGesture: false,
|
|
||||||
),
|
|
||||||
GetPage(
|
GetPage(
|
||||||
name: Routes.KR_ORDER_STATUS,
|
name: Routes.KR_ORDER_STATUS,
|
||||||
page: () => SwipeWrapper.detect(() => const KROrderStatusView()),
|
page: () => SwipeWrapper.detect(() => const KROrderStatusView()),
|
||||||
@ -145,12 +111,6 @@ class AppPages {
|
|||||||
binding: KRCrispBinding(),
|
binding: KRCrispBinding(),
|
||||||
popGesture: false,
|
popGesture: false,
|
||||||
),
|
),
|
||||||
GetPage(
|
|
||||||
name: _Paths.KR_DEVICE_MANAGEMENT,
|
|
||||||
page: () => SwipeWrapper.detect(() => const KRDeviceManagementView()),
|
|
||||||
binding: KRDeviceManagementBinding(),
|
|
||||||
popGesture: false,
|
|
||||||
),
|
|
||||||
GetPage(
|
GetPage(
|
||||||
name: _Paths.HI_NODE_LIST,
|
name: _Paths.HI_NODE_LIST,
|
||||||
page: () => SwipeWrapper.detect(() => const HINodePageView()),
|
page: () => SwipeWrapper.detect(() => const HINodePageView()),
|
||||||
|
|||||||
@ -17,7 +17,6 @@ abstract class Routes {
|
|||||||
static const KR_PURCHASE_MEMBERSHIP = _Paths.KR_PURCHASE_MEMBERSHIP;
|
static const KR_PURCHASE_MEMBERSHIP = _Paths.KR_PURCHASE_MEMBERSHIP;
|
||||||
static const KR_MESSAGE = _Paths.KR_MESSAGE;
|
static const KR_MESSAGE = _Paths.KR_MESSAGE;
|
||||||
static const KR_DELETE_ACCOUNT = _Paths.KR_DELETE_ACCOUNT;
|
static const KR_DELETE_ACCOUNT = _Paths.KR_DELETE_ACCOUNT;
|
||||||
static const KR_WEBVIEW = _Paths.KR_WEBVIEW;
|
|
||||||
static const KR_ORDER_STATUS = '/kr-order-status';
|
static const KR_ORDER_STATUS = '/kr-order-status';
|
||||||
static const KR_CRISP = _Paths.KR_CRISP;
|
static const KR_CRISP = _Paths.KR_CRISP;
|
||||||
static const KR_DEVICE_MANAGEMENT = _Paths.KR_DEVICE_MANAGEMENT;
|
static const KR_DEVICE_MANAGEMENT = _Paths.KR_DEVICE_MANAGEMENT;
|
||||||
@ -43,7 +42,6 @@ abstract class _Paths {
|
|||||||
static const KR_PURCHASE_MEMBERSHIP = '/kr-purchase-membership';
|
static const KR_PURCHASE_MEMBERSHIP = '/kr-purchase-membership';
|
||||||
static const KR_MESSAGE = '/kr-message';
|
static const KR_MESSAGE = '/kr-message';
|
||||||
static const KR_DELETE_ACCOUNT = '/kr-delete-account';
|
static const KR_DELETE_ACCOUNT = '/kr-delete-account';
|
||||||
static const KR_WEBVIEW = '/kr_webview';
|
|
||||||
static const KR_CRISP = '/kr-crisp';
|
static const KR_CRISP = '/kr-crisp';
|
||||||
static const KR_DEVICE_MANAGEMENT = '/kr-device-management';
|
static const KR_DEVICE_MANAGEMENT = '/kr-device-management';
|
||||||
static const HI_MENU = '/hi_menu';
|
static const HI_MENU = '/hi_menu';
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import '../kr_site_config_service.dart';
|
|||||||
import '../../common/app_config.dart';
|
import '../../common/app_config.dart';
|
||||||
import 'package:dio/dio.dart' as dio;
|
import 'package:dio/dio.dart' as dio;
|
||||||
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
||||||
|
import 'package:kaer_with_panels/app/utils/kr_http_adapter_util.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
class KRAuthApi {
|
class KRAuthApi {
|
||||||
@ -401,6 +402,13 @@ class KRAuthApi {
|
|||||||
|
|
||||||
// 使用 Dio 直接发送请求(因为需要特殊的加密处理)
|
// 使用 Dio 直接发送请求(因为需要特殊的加密处理)
|
||||||
final dioInstance = dio.Dio();
|
final dioInstance = dio.Dio();
|
||||||
|
// ✅ 关键修复:统一使用配置好的 Adapter,包含 SSL 验证绕过和代理处理
|
||||||
|
dioInstance.httpClientAdapter = KRHttpAdapterUtil.createAdapter(
|
||||||
|
useSingBoxProxy: false, // 设备登录建议先直连
|
||||||
|
forceDirect: true,
|
||||||
|
timeout: const Duration(seconds: 10),
|
||||||
|
);
|
||||||
|
|
||||||
final baseUrl = AppConfig.getInstance().baseUrl;
|
final baseUrl = AppConfig.getInstance().baseUrl;
|
||||||
final url = '$baseUrl${Api.kr_deviceLogin}';
|
final url = '$baseUrl${Api.kr_deviceLogin}';
|
||||||
|
|
||||||
|
|||||||
@ -229,8 +229,11 @@ class KRSubscribeApi {
|
|||||||
|
|
||||||
/// 获取用户已订阅套餐
|
/// 获取用户已订阅套餐
|
||||||
Future<Either<HttpError, List<KRAlreadySubscribe>>>
|
Future<Either<HttpError, List<KRAlreadySubscribe>>>
|
||||||
kr_getAlreadySubscribe() async {
|
kr_getAlreadySubscribe({String? includeExpired}) async {
|
||||||
final Map<String, dynamic> data = <String, dynamic>{};
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||||||
|
if (includeExpired != null) {
|
||||||
|
data['includeExpired'] = includeExpired;
|
||||||
|
}
|
||||||
|
|
||||||
BaseResponse<KRAlreadySubscribeList> baseResponse =
|
BaseResponse<KRAlreadySubscribeList> baseResponse =
|
||||||
await HttpUtil.getInstance().request<KRAlreadySubscribeList>(
|
await HttpUtil.getInstance().request<KRAlreadySubscribeList>(
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import 'package:get/get.dart';
|
|||||||
import '../modules/kr_home/views/hi_subscription_corner_button.dart';
|
import '../modules/kr_home/views/hi_subscription_corner_button.dart';
|
||||||
import 'package:kaer_with_panels/main.dart';
|
import 'package:kaer_with_panels/main.dart';
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:kaer_with_panels/app/routes/app_pages.dart';
|
||||||
|
|
||||||
class GlobalOverlayService extends GetxService {
|
class GlobalOverlayService extends GetxService {
|
||||||
Color? _currentColor;
|
Color? _currentColor;
|
||||||
@ -24,9 +26,14 @@ class GlobalOverlayService extends GetxService {
|
|||||||
}
|
}
|
||||||
OverlayEntry? _overlayEntry;
|
OverlayEntry? _overlayEntry;
|
||||||
final RxBool _isVisible = false.obs;
|
final RxBool _isVisible = false.obs;
|
||||||
|
final RxString _currentRoute = ''.obs;
|
||||||
|
|
||||||
bool get isVisible => _isVisible.value;
|
bool get isVisible => _isVisible.value;
|
||||||
|
|
||||||
|
void updateCurrentRoute(String route) {
|
||||||
|
_currentRoute.value = route;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
@ -77,20 +84,56 @@ class GlobalOverlayService extends GetxService {
|
|||||||
|
|
||||||
// 2️⃣ money-icon,独立定位,固定在屏幕右上角
|
// 2️⃣ money-icon,独立定位,固定在屏幕右上角
|
||||||
Positioned(
|
Positioned(
|
||||||
top: MediaQuery.of(context).padding.top + 8,
|
top: MediaQuery.of(context).padding.top,
|
||||||
right: ((radius - (statusBarHeight / 2)) / 2) + 3,
|
right: (radius / 2),
|
||||||
child: GestureDetector(
|
child: FractionalTranslation(
|
||||||
behavior: HitTestBehavior.translucent, // 让区域可响应点击
|
// Offset 的第一个参数是 x,第二个是 y
|
||||||
onTap: () {
|
// 0.5 代表向右偏移自身宽度的 50%,-0.5 代表向左偏移自身宽度的 50%
|
||||||
// ✅ 这里“代理”点击事件,转发到底层按钮动画逻辑
|
translation: const Offset(0.5, 0),
|
||||||
print('🔥 money-icon tapped → trigger HISubscriptionCornerButton animation');
|
child: GestureDetector(
|
||||||
GlobalOverlayService.instance.triggerSubscriptionAnimation();
|
behavior: HitTestBehavior.translucent,
|
||||||
},
|
onTap: () {
|
||||||
child: IgnorePointer(
|
print('🔥 money-icon tapped');
|
||||||
ignoring: false, // 不阻断事件
|
GlobalOverlayService.instance.triggerSubscriptionAnimation();
|
||||||
child: KrLocalImage(
|
},
|
||||||
imageName: 'money-icon',
|
child: IgnorePointer(
|
||||||
imageType: ImageType.svg,
|
ignoring: false,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
KrLocalImage(
|
||||||
|
imageName: 'money-icon',
|
||||||
|
imageType: ImageType.svg,
|
||||||
|
width: 32.w, // 建议固定宽度以保证计算准确
|
||||||
|
),
|
||||||
|
SizedBox(height: 2.w),
|
||||||
|
Obx(() {
|
||||||
|
final current = _currentRoute.value;
|
||||||
|
print('🔥 GlobalOverlayService Obx: currentRoute = $current');
|
||||||
|
|
||||||
|
// ✅ 结合三种方式判断:1. 显式设为透明 2. 路由名称匹配 3. 包含关键特征
|
||||||
|
bool isHidden = _currentColor == Colors.transparent ||
|
||||||
|
current == Routes.KR_PURCHASE_MEMBERSHIP ||
|
||||||
|
current.contains(Routes.KR_PURCHASE_MEMBERSHIP) ||
|
||||||
|
Routes.KR_PURCHASE_MEMBERSHIP.contains(current) ||
|
||||||
|
current == Routes.KR_ORDER_STATUS ||
|
||||||
|
current.contains('purchase-membership');
|
||||||
|
|
||||||
|
if (isHidden) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Text(
|
||||||
|
'购买套餐',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 12.sp,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -225,17 +225,21 @@ class KRDeviceInfoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Windows设备ID - 使用机器GUID
|
/// Windows设备ID - 使用机器GUID
|
||||||
|
/// 🔧 修复:不使用 flutter_udid,因为它会调用 wmic 命令弹出黑窗口
|
||||||
Future<String> _getWindowsDeviceId() async {
|
Future<String> _getWindowsDeviceId() async {
|
||||||
try {
|
try {
|
||||||
final windowsInfo = await _deviceInfo.windowsInfo;
|
final windowsInfo = await _deviceInfo.windowsInfo;
|
||||||
|
|
||||||
// 优先使用 flutter_udid
|
// 🔧 修复:不使用 FlutterUdid.consistentUdid
|
||||||
String udid = await FlutterUdid.consistentUdid;
|
// 因为它在 Windows 上会调用 "cmd.exe /c wmic csproduct get UUID"
|
||||||
|
// 这个调用没有使用 CREATE_NO_WINDOW 标志,会弹出黑色命令行窗口
|
||||||
|
|
||||||
|
// 直接使用 device_info_plus 提供的信息构建唯一标识
|
||||||
|
// windowsInfo.deviceId 已经是一个稳定的设备标识
|
||||||
|
|
||||||
// 构建多因子字符串
|
// 构建多因子字符串
|
||||||
final factors = [
|
final factors = [
|
||||||
udid,
|
windowsInfo.deviceId, // 设备ID (最稳定,来自注册表 MachineGuid)
|
||||||
windowsInfo.deviceId, // 设备ID
|
|
||||||
windowsInfo.computerName, // 计算机名
|
windowsInfo.computerName, // 计算机名
|
||||||
windowsInfo.productName, // 产品名
|
windowsInfo.productName, // 产品名
|
||||||
windowsInfo.numberOfCores.toString(), // CPU核心数
|
windowsInfo.numberOfCores.toString(), // CPU核心数
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import '../model/response/kr_site_config.dart';
|
import '../model/response/kr_site_config.dart';
|
||||||
import '../common/app_config.dart';
|
import '../common/app_config.dart';
|
||||||
import '../utils/kr_log_util.dart';
|
import '../utils/kr_log_util.dart';
|
||||||
|
import '../utils/kr_http_adapter_util.dart';
|
||||||
import 'singbox_imp/kr_sing_box_imp.dart';
|
import 'singbox_imp/kr_sing_box_imp.dart';
|
||||||
|
|
||||||
/// 网站配置服务
|
/// 网站配置服务
|
||||||
@ -25,6 +26,14 @@ class KRSiteConfigService extends ChangeNotifier {
|
|||||||
'🌐 网站配置服务:使用直连模式(不通过代理)',
|
'🌐 网站配置服务:使用直连模式(不通过代理)',
|
||||||
tag: 'KRSiteConfigService',
|
tag: 'KRSiteConfigService',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 🔧 关键修复:添加 SSL 证书验证跳过,强制直连
|
||||||
|
_dio.httpClientAdapter = KRHttpAdapterUtil.createAdapter(
|
||||||
|
useSingBoxProxy: false,
|
||||||
|
forceDirect: true,
|
||||||
|
timeout: const Duration(seconds: 20),
|
||||||
|
);
|
||||||
|
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('🌐 网站配置服务:使用直连模式,避免 SingBox 未初始化问题');
|
print('🌐 网站配置服务:使用直连模式,避免 SingBox 未初始化问题');
|
||||||
}
|
}
|
||||||
|
|||||||
47
lib/app/utils/kr_http_adapter_util.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:dio/io.dart';
|
||||||
|
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
||||||
|
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||||
|
|
||||||
|
class KRHttpAdapterUtil {
|
||||||
|
/// 创建标准配置的 HttpClientAdapter
|
||||||
|
///
|
||||||
|
/// [useSingBoxProxy] 是否集成 sing-box 代理规则,默认 true
|
||||||
|
/// [forceDirect] 是否强制直连(不使用任何代理),默认 false
|
||||||
|
static HttpClientAdapter createAdapter({
|
||||||
|
bool useSingBoxProxy = true,
|
||||||
|
bool forceDirect = false,
|
||||||
|
Duration timeout = const Duration(seconds: 10),
|
||||||
|
}) {
|
||||||
|
return IOHttpClientAdapter(
|
||||||
|
createHttpClient: () {
|
||||||
|
final client = HttpClient();
|
||||||
|
client.connectionTimeout = timeout;
|
||||||
|
|
||||||
|
// ✅ 统一修复:允许所有 SSL 证书
|
||||||
|
client.badCertificateCallback = (cert, host, port) => true;
|
||||||
|
|
||||||
|
if (forceDirect) {
|
||||||
|
client.findProxy = (url) {
|
||||||
|
KRLogUtil.kr_i('🔍 请求使用直连: $url', tag: 'KRHttpAdapterUtil');
|
||||||
|
return 'DIRECT';
|
||||||
|
};
|
||||||
|
} else if (useSingBoxProxy) {
|
||||||
|
client.findProxy = (url) {
|
||||||
|
try {
|
||||||
|
final proxyConfig = KRSingBoxImp.instance.kr_buildProxyRule();
|
||||||
|
KRLogUtil.kr_i('🔍 请求使用代理: $proxyConfig, url: $url', tag: 'KRHttpAdapterUtil');
|
||||||
|
return proxyConfig;
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_w('⚠️ 获取代理配置异常,回退到 DIRECT: $e', tag: 'KRHttpAdapterUtil');
|
||||||
|
return 'DIRECT';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -25,8 +25,9 @@ class KRWindowManager with WindowListener, TrayListener {
|
|||||||
KRLogUtil.kr_i('kr_initWindowManager: 窗口管理器已初始化');
|
KRLogUtil.kr_i('kr_initWindowManager: 窗口管理器已初始化');
|
||||||
|
|
||||||
const WindowOptions windowOptions = WindowOptions(
|
const WindowOptions windowOptions = WindowOptions(
|
||||||
size: Size(800, 668),
|
size: Size(420, 800),
|
||||||
minimumSize: Size(400, 334),
|
minimumSize: Size(420, 800),
|
||||||
|
maximumSize: Size(420, 800),
|
||||||
center: true,
|
center: true,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
skipTaskbar: false,
|
skipTaskbar: false,
|
||||||
@ -46,16 +47,20 @@ class KRWindowManager with WindowListener, TrayListener {
|
|||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
await windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
await windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
||||||
await windowManager.setTitle('HiFastVPN');
|
await windowManager.setTitle('HiFastVPN');
|
||||||
await windowManager.setSize(const Size(800, 668));
|
await windowManager.setSize(const Size(420, 800));
|
||||||
await windowManager.setMinimumSize(const Size(400, 334));
|
await windowManager.setMinimumSize(const Size(420, 800));
|
||||||
|
await windowManager.setMaximumSize(const Size(420, 800));
|
||||||
|
await windowManager.setResizable(false);
|
||||||
await windowManager.center();
|
await windowManager.center();
|
||||||
await windowManager.show();
|
await windowManager.show();
|
||||||
// 阻止窗口关闭
|
// 阻止窗口关闭
|
||||||
await windowManager.setPreventClose(true);
|
await windowManager.setPreventClose(true);
|
||||||
} else {
|
} else {
|
||||||
await windowManager.setTitle('HiFastVPN');
|
await windowManager.setTitle('HiFastVPN');
|
||||||
await windowManager.setSize(const Size(800, 668));
|
await windowManager.setSize(const Size(420, 800));
|
||||||
await windowManager.setMinimumSize(const Size(400, 334));
|
await windowManager.setMinimumSize(const Size(420, 800));
|
||||||
|
await windowManager.setMaximumSize(const Size(420, 800));
|
||||||
|
await windowManager.setResizable(false);
|
||||||
await windowManager.center();
|
await windowManager.center();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||||
|
import 'package:kaer_with_panels/app/utils/kr_windows_process_util.dart';
|
||||||
|
|
||||||
/// Windows DNS 管理工具类
|
/// Windows DNS 管理工具类
|
||||||
///
|
///
|
||||||
@ -38,28 +39,48 @@ class KRWindowsDnsUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
KRLogUtil.kr_i('📦 开始备份 Windows DNS 设置...', tag: 'WindowsDNS');
|
// 🔒 添加5秒超时保护
|
||||||
|
return await Future.value(() async {
|
||||||
|
KRLogUtil.kr_i('📦 开始备份 Windows DNS 设置...', tag: 'WindowsDNS');
|
||||||
|
|
||||||
// 1. 获取主网络接口
|
// 1. 获取主网络接口
|
||||||
final interfaceName = await _kr_getPrimaryNetworkInterface();
|
final interfaceName = await _kr_getPrimaryNetworkInterface();
|
||||||
if (interfaceName == null) {
|
if (interfaceName == null) {
|
||||||
KRLogUtil.kr_e('❌ 无法获取主网络接口', tag: 'WindowsDNS');
|
KRLogUtil.kr_e('❌ 无法获取主网络接口', tag: 'WindowsDNS');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
_primaryInterfaceName = interfaceName;
|
_primaryInterfaceName = interfaceName;
|
||||||
KRLogUtil.kr_i('🔍 主网络接口: $_primaryInterfaceName', tag: 'WindowsDNS');
|
KRLogUtil.kr_i('🔍 主网络接口: $_primaryInterfaceName', tag: 'WindowsDNS');
|
||||||
|
|
||||||
// 2. 获取当前 DNS 服务器
|
// 2. 获取当前 DNS 服务器
|
||||||
final dnsServers = await _kr_getCurrentDnsServers(interfaceName);
|
final dnsServers = await _kr_getCurrentDnsServers(interfaceName);
|
||||||
if (dnsServers.isEmpty) {
|
|
||||||
KRLogUtil.kr_w('⚠️ 当前 DNS 为空,可能是自动获取', tag: 'WindowsDNS');
|
|
||||||
_originalDnsServers = []; // 空列表表示 DHCP 自动获取
|
|
||||||
} else {
|
|
||||||
_originalDnsServers = dnsServers;
|
|
||||||
KRLogUtil.kr_i('✅ 已备份 DNS: ${dnsServers.join(", ")}', tag: 'WindowsDNS');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
// 🔧 P0修复1: 过滤掉 127.0.0.1 (sing-box 的本地 DNS)
|
||||||
|
// 原因:如果备份了 127.0.0.1,关闭 VPN 后恢复为 127.0.0.1,但 sing-box 已停止,导致 DNS 无法解析
|
||||||
|
final validDnsServers = dnsServers.where((dns) => !dns.startsWith('127.')).toList();
|
||||||
|
|
||||||
|
if (validDnsServers.isEmpty) {
|
||||||
|
KRLogUtil.kr_w('⚠️ 当前 DNS 为空或全是本地地址,设为 DHCP 自动获取', tag: 'WindowsDNS');
|
||||||
|
if (dnsServers.isNotEmpty) {
|
||||||
|
KRLogUtil.kr_i(' (已过滤的本地DNS: ${dnsServers.join(", ")})', tag: 'WindowsDNS');
|
||||||
|
}
|
||||||
|
_originalDnsServers = []; // 空列表表示 DHCP 自动获取
|
||||||
|
} else {
|
||||||
|
_originalDnsServers = validDnsServers;
|
||||||
|
KRLogUtil.kr_i('✅ 已备份有效 DNS: ${validDnsServers.join(", ")}', tag: 'WindowsDNS');
|
||||||
|
if (dnsServers.length != validDnsServers.length) {
|
||||||
|
KRLogUtil.kr_i(' (已过滤掉 ${dnsServers.length - validDnsServers.length} 个本地地址)', tag: 'WindowsDNS');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}()).timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
onTimeout: () {
|
||||||
|
KRLogUtil.kr_w('⏱️ DNS 备份操作超时(5秒),跳过备份', tag: 'WindowsDNS');
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
KRLogUtil.kr_e('❌ 备份 DNS 设置失败: $e', tag: 'WindowsDNS');
|
KRLogUtil.kr_e('❌ 备份 DNS 设置失败: $e', tag: 'WindowsDNS');
|
||||||
return false;
|
return false;
|
||||||
@ -80,22 +101,33 @@ class KRWindowsDnsUtil {
|
|||||||
try {
|
try {
|
||||||
KRLogUtil.kr_i('🔄 开始恢复 Windows DNS 设置...', tag: 'WindowsDNS');
|
KRLogUtil.kr_i('🔄 开始恢复 Windows DNS 设置...', tag: 'WindowsDNS');
|
||||||
|
|
||||||
// 1. 检查是否有备份
|
// 🔧 P1修复: 恢复时重新检测主接口,防止网络切换导致恢复错误接口
|
||||||
if (_primaryInterfaceName == null) {
|
final currentInterface = await _kr_getPrimaryNetworkInterface();
|
||||||
KRLogUtil.kr_w('⚠️ 没有备份的网络接口,尝试自动检测', tag: 'WindowsDNS');
|
if (currentInterface == null) {
|
||||||
_primaryInterfaceName = await _kr_getPrimaryNetworkInterface();
|
KRLogUtil.kr_e('❌ 无法检测当前网络接口,执行兜底恢复', tag: 'WindowsDNS');
|
||||||
if (_primaryInterfaceName == null) {
|
return await _kr_fallbackRestoreDns();
|
||||||
KRLogUtil.kr_e('❌ 无法检测网络接口,执行兜底恢复', tag: 'WindowsDNS');
|
|
||||||
return await _kr_fallbackRestoreDns();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 恢复原始 DNS
|
// 检查接口是否变化
|
||||||
|
if (_primaryInterfaceName != null && _primaryInterfaceName != currentInterface) {
|
||||||
|
KRLogUtil.kr_w('⚠️ 网络接口已变化: $_primaryInterfaceName → $currentInterface', tag: 'WindowsDNS');
|
||||||
|
KRLogUtil.kr_w(' 执行兜底恢复以确保当前接口DNS正常', tag: 'WindowsDNS');
|
||||||
|
_primaryInterfaceName = currentInterface; // 更新为当前接口
|
||||||
|
return await _kr_fallbackRestoreDns();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用当前检测到的接口
|
||||||
|
_primaryInterfaceName = currentInterface;
|
||||||
|
KRLogUtil.kr_i('🔍 当前网络接口: $_primaryInterfaceName', tag: 'WindowsDNS');
|
||||||
|
|
||||||
|
// 1. 检查是否有备份的DNS
|
||||||
if (_originalDnsServers == null) {
|
if (_originalDnsServers == null) {
|
||||||
KRLogUtil.kr_w('⚠️ 没有备份的 DNS,执行兜底恢复', tag: 'WindowsDNS');
|
KRLogUtil.kr_w('⚠️ 没有备份的 DNS,执行兜底恢复', tag: 'WindowsDNS');
|
||||||
return await _kr_fallbackRestoreDns();
|
return await _kr_fallbackRestoreDns();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 恢复原始 DNS
|
||||||
|
|
||||||
if (_originalDnsServers!.isEmpty) {
|
if (_originalDnsServers!.isEmpty) {
|
||||||
// 原本是 DHCP 自动获取
|
// 原本是 DHCP 自动获取
|
||||||
KRLogUtil.kr_i('🔄 恢复为 DHCP 自动获取 DNS', tag: 'WindowsDNS');
|
KRLogUtil.kr_i('🔄 恢复为 DHCP 自动获取 DNS', tag: 'WindowsDNS');
|
||||||
@ -130,6 +162,15 @@ class KRWindowsDnsUtil {
|
|||||||
return await _kr_fallbackRestoreDns();
|
return await _kr_fallbackRestoreDns();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔧 P2优化: 测试 DNS 解析是否真正可用
|
||||||
|
KRLogUtil.kr_i('🧪 测试 DNS 解析功能...', tag: 'WindowsDNS');
|
||||||
|
final canResolve = await _kr_testDnsResolution();
|
||||||
|
if (!canResolve) {
|
||||||
|
KRLogUtil.kr_w('⚠️ DNS 解析测试失败,执行兜底恢复', tag: 'WindowsDNS');
|
||||||
|
return await _kr_fallbackRestoreDns();
|
||||||
|
}
|
||||||
|
KRLogUtil.kr_i('✅ DNS 解析测试通过', tag: 'WindowsDNS');
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
KRLogUtil.kr_e('❌ 恢复 DNS 设置失败: $e', tag: 'WindowsDNS');
|
KRLogUtil.kr_e('❌ 恢复 DNS 设置失败: $e', tag: 'WindowsDNS');
|
||||||
@ -187,7 +228,7 @@ class KRWindowsDnsUtil {
|
|||||||
Future<String?> _kr_getPrimaryNetworkInterface() async {
|
Future<String?> _kr_getPrimaryNetworkInterface() async {
|
||||||
try {
|
try {
|
||||||
// 使用 netsh 获取接口列表
|
// 使用 netsh 获取接口列表
|
||||||
final result = await Process.run('netsh', ['interface', 'show', 'interface']);
|
final result = await KRWindowsProcessUtil.runHidden('netsh', ['interface', 'show', 'interface']);
|
||||||
|
|
||||||
if (result.exitCode != 0) {
|
if (result.exitCode != 0) {
|
||||||
KRLogUtil.kr_e('❌ 获取网络接口失败: ${result.stderr}', tag: 'WindowsDNS');
|
KRLogUtil.kr_e('❌ 获取网络接口失败: ${result.stderr}', tag: 'WindowsDNS');
|
||||||
@ -271,7 +312,7 @@ class KRWindowsDnsUtil {
|
|||||||
/// 返回:DNS 服务器列表
|
/// 返回:DNS 服务器列表
|
||||||
Future<List<String>> _kr_getCurrentDnsServers(String interfaceName) async {
|
Future<List<String>> _kr_getCurrentDnsServers(String interfaceName) async {
|
||||||
try {
|
try {
|
||||||
final result = await Process.run('netsh', [
|
final result = await KRWindowsProcessUtil.runHidden('netsh', [
|
||||||
'interface',
|
'interface',
|
||||||
'ipv4',
|
'ipv4',
|
||||||
'show',
|
'show',
|
||||||
@ -294,10 +335,9 @@ class KRWindowsDnsUtil {
|
|||||||
final ipMatch = RegExp(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b').firstMatch(line);
|
final ipMatch = RegExp(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b').firstMatch(line);
|
||||||
if (ipMatch != null) {
|
if (ipMatch != null) {
|
||||||
final ip = ipMatch.group(0)!;
|
final ip = ipMatch.group(0)!;
|
||||||
// 排除本地回环地址
|
// 🔧 关键修复:不过滤127.0.0.1,以便正确检测DNS是否还在使用sing-box的本地DNS
|
||||||
if (!ip.startsWith('127.')) {
|
// 这样在恢复DNS时,第126行的验证才能正确检测到127.0.0.1并触发兜底恢复
|
||||||
dnsServers.add(ip);
|
dnsServers.add(ip);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,7 +365,7 @@ class KRWindowsDnsUtil {
|
|||||||
|
|
||||||
// 1. 设置主 DNS
|
// 1. 设置主 DNS
|
||||||
KRLogUtil.kr_i('🔧 设置主 DNS: ${dnsServers[0]}', tag: 'WindowsDNS');
|
KRLogUtil.kr_i('🔧 设置主 DNS: ${dnsServers[0]}', tag: 'WindowsDNS');
|
||||||
var result = await Process.run('netsh', [
|
var result = await KRWindowsProcessUtil.runHidden('netsh', [
|
||||||
'interface',
|
'interface',
|
||||||
'ipv4',
|
'ipv4',
|
||||||
'set',
|
'set',
|
||||||
@ -345,7 +385,7 @@ class KRWindowsDnsUtil {
|
|||||||
if (dnsServers.length > 1) {
|
if (dnsServers.length > 1) {
|
||||||
for (int i = 1; i < dnsServers.length; i++) {
|
for (int i = 1; i < dnsServers.length; i++) {
|
||||||
KRLogUtil.kr_i('🔧 设置备用 DNS ${i}: ${dnsServers[i]}', tag: 'WindowsDNS');
|
KRLogUtil.kr_i('🔧 设置备用 DNS ${i}: ${dnsServers[i]}', tag: 'WindowsDNS');
|
||||||
result = await Process.run('netsh', [
|
result = await KRWindowsProcessUtil.runHidden('netsh', [
|
||||||
'interface',
|
'interface',
|
||||||
'ipv4',
|
'ipv4',
|
||||||
'add',
|
'add',
|
||||||
@ -384,7 +424,7 @@ class KRWindowsDnsUtil {
|
|||||||
try {
|
try {
|
||||||
KRLogUtil.kr_i('🔧 设置 DNS 为自动获取 (DHCP)', tag: 'WindowsDNS');
|
KRLogUtil.kr_i('🔧 设置 DNS 为自动获取 (DHCP)', tag: 'WindowsDNS');
|
||||||
|
|
||||||
final result = await Process.run('netsh', [
|
final result = await KRWindowsProcessUtil.runHidden('netsh', [
|
||||||
'interface',
|
'interface',
|
||||||
'ipv4',
|
'ipv4',
|
||||||
'set',
|
'set',
|
||||||
@ -416,7 +456,7 @@ class KRWindowsDnsUtil {
|
|||||||
try {
|
try {
|
||||||
KRLogUtil.kr_i('🔄 刷新 DNS 缓存...', tag: 'WindowsDNS');
|
KRLogUtil.kr_i('🔄 刷新 DNS 缓存...', tag: 'WindowsDNS');
|
||||||
|
|
||||||
final result = await Process.run('ipconfig', ['/flushdns']);
|
final result = await KRWindowsProcessUtil.runHidden('ipconfig', ['/flushdns']);
|
||||||
|
|
||||||
if (result.exitCode == 0) {
|
if (result.exitCode == 0) {
|
||||||
KRLogUtil.kr_i('✅ DNS 缓存已刷新', tag: 'WindowsDNS');
|
KRLogUtil.kr_i('✅ DNS 缓存已刷新', tag: 'WindowsDNS');
|
||||||
@ -428,6 +468,51 @@ class KRWindowsDnsUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 🔧 P2优化: 测试 DNS 解析是否真正可用
|
||||||
|
///
|
||||||
|
/// 通过 nslookup 测试常见域名解析
|
||||||
|
/// 返回:true 表示 DNS 可用,false 表示 DNS 不可用
|
||||||
|
Future<bool> _kr_testDnsResolution() async {
|
||||||
|
try {
|
||||||
|
// 测试多个常见域名,提高成功率
|
||||||
|
final testDomains = ['www.baidu.com', 'www.qq.com', 'dns.alidns.com'];
|
||||||
|
|
||||||
|
for (var domain in testDomains) {
|
||||||
|
try {
|
||||||
|
// 使用 nslookup 测试 DNS 解析,设置 2 秒超时
|
||||||
|
final result = await KRWindowsProcessUtil.runHidden(
|
||||||
|
'nslookup',
|
||||||
|
[domain],
|
||||||
|
).timeout(
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
onTimeout: () {
|
||||||
|
return ProcessResult(0, 1, '', 'Timeout');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.exitCode == 0) {
|
||||||
|
final output = result.stdout.toString();
|
||||||
|
// 检查输出是否包含 IP 地址(简单验证)
|
||||||
|
if (output.contains('Address:') || output.contains('地址:')) {
|
||||||
|
KRLogUtil.kr_i('✅ DNS 解析测试通过: $domain', tag: 'WindowsDNS');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 单个域名失败,继续测试下一个
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有域名都解析失败
|
||||||
|
KRLogUtil.kr_w('⚠️ 所有测试域名解析均失败', tag: 'WindowsDNS');
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('❌ DNS 解析测试异常: $e', tag: 'WindowsDNS');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 清除备份数据
|
/// 清除备份数据
|
||||||
///
|
///
|
||||||
/// 在应用退出或不需要时调用
|
/// 在应用退出或不需要时调用
|
||||||
|
|||||||
594
lib/app/utils/kr_windows_process_util.dart
Normal file
@ -0,0 +1,594 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:ffi';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:ffi/ffi.dart';
|
||||||
|
|
||||||
|
class KRWindowsProcessUtil {
|
||||||
|
/// 🔍 调试标志:设为 true 可以追踪所有命令执行(用于排查黑窗问题)
|
||||||
|
static const bool _debugCommandExecution = true; // ← 开启调试
|
||||||
|
|
||||||
|
static Future<ProcessResult> runHidden(String executable, List<String> arguments) async {
|
||||||
|
if (!Platform.isWindows) {
|
||||||
|
return Process.run(executable, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await _runHiddenWindows(executable, arguments);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<int> startHidden(String executable, List<String> arguments) async {
|
||||||
|
if (!Platform.isWindows) {
|
||||||
|
final process = await Process.start(executable, arguments);
|
||||||
|
return process.pid;
|
||||||
|
}
|
||||||
|
return _startHiddenWindows(executable, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _buildCommandLine(String executable, List<String> arguments) {
|
||||||
|
final parts = <String>[_quoteArgument(executable)];
|
||||||
|
for (final arg in arguments) {
|
||||||
|
parts.add(_quoteArgument(arg));
|
||||||
|
}
|
||||||
|
return parts.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool _shouldSearchPath(String executable) {
|
||||||
|
if (executable.isEmpty) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !(executable.contains('\\') || executable.contains('/') || executable.contains(':'));
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _quoteArgument(String value) {
|
||||||
|
if (value.isEmpty) {
|
||||||
|
return '""';
|
||||||
|
}
|
||||||
|
final needsQuotes = value.contains(' ') || value.contains('\t') || value.contains('"');
|
||||||
|
if (!needsQuotes) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
final buffer = StringBuffer('"');
|
||||||
|
var backslashes = 0;
|
||||||
|
for (var i = 0; i < value.length; i++) {
|
||||||
|
final char = value[i];
|
||||||
|
if (char == '\\') {
|
||||||
|
backslashes++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char == '"') {
|
||||||
|
buffer.write('\\' * (backslashes * 2 + 1));
|
||||||
|
buffer.write('"');
|
||||||
|
backslashes = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (backslashes > 0) {
|
||||||
|
buffer.write('\\' * backslashes);
|
||||||
|
backslashes = 0;
|
||||||
|
}
|
||||||
|
buffer.write(char);
|
||||||
|
}
|
||||||
|
if (backslashes > 0) {
|
||||||
|
buffer.write('\\' * (backslashes * 2));
|
||||||
|
}
|
||||||
|
buffer.write('"');
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<ProcessResult> _runHiddenWindows(String executable, List<String> arguments) async {
|
||||||
|
final stdoutPipe = _createPipe();
|
||||||
|
final stderrPipe = _createPipe();
|
||||||
|
|
||||||
|
final startupInfo = calloc<STARTUPINFO>();
|
||||||
|
final processInfo = calloc<PROCESS_INFORMATION>();
|
||||||
|
final commandLine = _buildCommandLine(executable, arguments).toNativeUtf16();
|
||||||
|
final applicationName = _shouldSearchPath(executable) ? nullptr : executable.toNativeUtf16();
|
||||||
|
final stdInput = _getStdInputHandle();
|
||||||
|
|
||||||
|
startupInfo.ref
|
||||||
|
..cb = sizeOf<STARTUPINFO>()
|
||||||
|
..dwFlags = STARTF_USESTDHANDLES
|
||||||
|
..hStdInput = stdInput
|
||||||
|
..hStdOutput = stdoutPipe.write
|
||||||
|
..hStdError = stderrPipe.write;
|
||||||
|
|
||||||
|
final created = _CreateProcessW(
|
||||||
|
applicationName,
|
||||||
|
commandLine,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
TRUE,
|
||||||
|
CREATE_NO_WINDOW,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
startupInfo,
|
||||||
|
processInfo,
|
||||||
|
);
|
||||||
|
|
||||||
|
calloc.free(commandLine);
|
||||||
|
if (applicationName != nullptr) {
|
||||||
|
calloc.free(applicationName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (created == 0) {
|
||||||
|
_closeHandle(stdoutPipe.read);
|
||||||
|
_closeHandle(stdoutPipe.write);
|
||||||
|
_closeHandle(stderrPipe.read);
|
||||||
|
_closeHandle(stderrPipe.write);
|
||||||
|
calloc.free(startupInfo);
|
||||||
|
calloc.free(processInfo);
|
||||||
|
throw Exception('CreateProcessW failed: ${_GetLastError()}');
|
||||||
|
}
|
||||||
|
|
||||||
|
_closeHandle(stdoutPipe.write);
|
||||||
|
_closeHandle(stderrPipe.write);
|
||||||
|
|
||||||
|
final output = await _collectOutput(processInfo.ref.hProcess, stdoutPipe.read, stderrPipe.read);
|
||||||
|
final exitCode = _getExitCode(processInfo.ref.hProcess);
|
||||||
|
|
||||||
|
_closeHandle(stdoutPipe.read);
|
||||||
|
_closeHandle(stderrPipe.read);
|
||||||
|
_closeHandle(processInfo.ref.hThread);
|
||||||
|
_closeHandle(processInfo.ref.hProcess);
|
||||||
|
|
||||||
|
final pid = processInfo.ref.dwProcessId;
|
||||||
|
calloc.free(startupInfo);
|
||||||
|
calloc.free(processInfo);
|
||||||
|
|
||||||
|
return ProcessResult(pid, exitCode, output.stdout, output.stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<int> _startHiddenWindows(String executable, List<String> arguments) async {
|
||||||
|
final startupInfo = calloc<STARTUPINFO>();
|
||||||
|
final processInfo = calloc<PROCESS_INFORMATION>();
|
||||||
|
final commandLine = _buildCommandLine(executable, arguments).toNativeUtf16();
|
||||||
|
final applicationName = _shouldSearchPath(executable) ? nullptr : executable.toNativeUtf16();
|
||||||
|
|
||||||
|
startupInfo.ref.cb = sizeOf<STARTUPINFO>();
|
||||||
|
|
||||||
|
final created = _CreateProcessW(
|
||||||
|
applicationName,
|
||||||
|
commandLine,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
FALSE,
|
||||||
|
CREATE_NO_WINDOW,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
startupInfo,
|
||||||
|
processInfo,
|
||||||
|
);
|
||||||
|
|
||||||
|
calloc.free(commandLine);
|
||||||
|
if (applicationName != nullptr) {
|
||||||
|
calloc.free(applicationName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (created == 0) {
|
||||||
|
calloc.free(startupInfo);
|
||||||
|
calloc.free(processInfo);
|
||||||
|
throw Exception('CreateProcessW failed: ${_GetLastError()}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final pid = processInfo.ref.dwProcessId;
|
||||||
|
|
||||||
|
_closeHandle(processInfo.ref.hThread);
|
||||||
|
_closeHandle(processInfo.ref.hProcess);
|
||||||
|
calloc.free(startupInfo);
|
||||||
|
calloc.free(processInfo);
|
||||||
|
|
||||||
|
return pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
static _Pipe _createPipe() {
|
||||||
|
final readHandle = calloc<Pointer<Void>>();
|
||||||
|
final writeHandle = calloc<Pointer<Void>>();
|
||||||
|
final securityAttributes = calloc<SECURITY_ATTRIBUTES>();
|
||||||
|
securityAttributes.ref
|
||||||
|
..nLength = sizeOf<SECURITY_ATTRIBUTES>()
|
||||||
|
..bInheritHandle = TRUE
|
||||||
|
..lpSecurityDescriptor = nullptr;
|
||||||
|
|
||||||
|
final created = _CreatePipe(readHandle, writeHandle, securityAttributes, 0);
|
||||||
|
calloc.free(securityAttributes);
|
||||||
|
if (created == 0) {
|
||||||
|
calloc.free(readHandle);
|
||||||
|
calloc.free(writeHandle);
|
||||||
|
throw Exception('CreatePipe failed: ${_GetLastError()}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final readValue = readHandle.value;
|
||||||
|
final writeValue = writeHandle.value;
|
||||||
|
calloc.free(readHandle);
|
||||||
|
calloc.free(writeHandle);
|
||||||
|
|
||||||
|
final infoResult = _SetHandleInformation(readValue, HANDLE_FLAG_INHERIT, 0);
|
||||||
|
if (infoResult == 0) {
|
||||||
|
_closeHandle(readValue);
|
||||||
|
_closeHandle(writeValue);
|
||||||
|
throw Exception('SetHandleInformation failed: ${_GetLastError()}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return _Pipe(readValue, writeValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Pointer<Void> _getStdInputHandle() {
|
||||||
|
final handle = _GetStdHandle(STD_INPUT_HANDLE);
|
||||||
|
if (handle == INVALID_HANDLE_VALUE || handle == 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return Pointer<Void>.fromAddress(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<_ProcessOutput> _collectOutput(
|
||||||
|
Pointer<Void> process,
|
||||||
|
Pointer<Void> stdoutHandle,
|
||||||
|
Pointer<Void> stderrHandle,
|
||||||
|
) async {
|
||||||
|
final stdoutBuilder = BytesBuilder();
|
||||||
|
final stderrBuilder = BytesBuilder();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
final stdoutRead = _drainPipe(stdoutHandle, stdoutBuilder);
|
||||||
|
final stderrRead = _drainPipe(stderrHandle, stderrBuilder);
|
||||||
|
final waitResult = _WaitForSingleObject(process, 0);
|
||||||
|
if (waitResult == WAIT_OBJECT_0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (waitResult == WAIT_FAILED) {
|
||||||
|
throw Exception('WaitForSingleObject failed: ${_GetLastError()}');
|
||||||
|
}
|
||||||
|
if (!stdoutRead && !stderrRead) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 10));
|
||||||
|
} else {
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (_drainPipe(stdoutHandle, stdoutBuilder) || _drainPipe(stderrHandle, stderrBuilder)) {
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _ProcessOutput(
|
||||||
|
_decodeOutput(stdoutBuilder),
|
||||||
|
_decodeOutput(stderrBuilder),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool _drainPipe(Pointer<Void> handle, BytesBuilder builder) {
|
||||||
|
final buffer = calloc<Uint8>(4096);
|
||||||
|
final bytesRead = calloc<Uint32>();
|
||||||
|
final available = calloc<Uint32>();
|
||||||
|
var didRead = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
final peekOk = _PeekNamedPipe(handle, nullptr, 0, nullptr, available, nullptr);
|
||||||
|
if (peekOk == 0 || available.value == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
final toRead = available.value < 4096 ? available.value : 4096;
|
||||||
|
final ok = _ReadFile(handle, buffer.cast<Void>(), toRead, bytesRead, nullptr);
|
||||||
|
final read = ok == 0 ? 0 : bytesRead.value;
|
||||||
|
if (read == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
builder.add(buffer.asTypedList(read));
|
||||||
|
didRead = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
calloc.free(buffer);
|
||||||
|
calloc.free(bytesRead);
|
||||||
|
calloc.free(available);
|
||||||
|
|
||||||
|
return didRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _decodeOutput(BytesBuilder builder) {
|
||||||
|
if (builder.length == 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
final bytes = builder.toBytes();
|
||||||
|
try {
|
||||||
|
return systemEncoding.decode(bytes);
|
||||||
|
} catch (_) {
|
||||||
|
return utf8.decode(bytes, allowMalformed: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _getExitCode(Pointer<Void> process) {
|
||||||
|
final exitCode = calloc<Uint32>();
|
||||||
|
final ok = _GetExitCodeProcess(process, exitCode);
|
||||||
|
final code = ok == 0 ? -1 : exitCode.value;
|
||||||
|
calloc.free(exitCode);
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _closeHandle(Pointer<Void> handle) {
|
||||||
|
if (handle == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_CloseHandle(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔧 WinINet API helpers for proxy settings
|
||||||
|
/// 查询当前系统代理设置
|
||||||
|
static String? queryWindowsProxyServer() {
|
||||||
|
if (!Platform.isWindows) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final bufferSize = calloc<Uint32>();
|
||||||
|
bufferSize.value = sizeOf<INTERNET_PROXY_INFO>();
|
||||||
|
|
||||||
|
final proxyInfo = calloc<INTERNET_PROXY_INFO>();
|
||||||
|
|
||||||
|
final result = _InternetQueryOptionW(nullptr, INTERNET_OPTION_PROXY, proxyInfo.cast<Void>(), bufferSize);
|
||||||
|
if (result == 0) {
|
||||||
|
calloc.free(bufferSize);
|
||||||
|
calloc.free(proxyInfo);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final proxyServer = proxyInfo.ref.lpszProxy.toDartString();
|
||||||
|
calloc.free(bufferSize);
|
||||||
|
calloc.free(proxyInfo);
|
||||||
|
|
||||||
|
return proxyServer.isEmpty ? null : proxyServer;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置系统代理
|
||||||
|
static bool setWindowsProxyServer(String? server) {
|
||||||
|
if (!Platform.isWindows) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final proxyInfo = calloc<INTERNET_PROXY_INFO>();
|
||||||
|
|
||||||
|
if (server != null && server.isNotEmpty) {
|
||||||
|
// 设置代理模式
|
||||||
|
proxyInfo.ref.dwAccessType = INTERNET_OPEN_TYPE_PROXY;
|
||||||
|
proxyInfo.ref.lpszProxy = server.toNativeUtf16();
|
||||||
|
proxyInfo.ref.lpszProxyBypass = ''.toNativeUtf16();
|
||||||
|
} else {
|
||||||
|
// 禁用代理
|
||||||
|
proxyInfo.ref.dwAccessType = INTERNET_OPEN_TYPE_DIRECT;
|
||||||
|
proxyInfo.ref.lpszProxy = nullptr;
|
||||||
|
proxyInfo.ref.lpszProxyBypass = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = _InternetSetOptionW(
|
||||||
|
nullptr,
|
||||||
|
INTERNET_OPTION_PROXY,
|
||||||
|
proxyInfo.cast<Void>(),
|
||||||
|
sizeOf<INTERNET_PROXY_INFO>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (server != null && server.isNotEmpty) {
|
||||||
|
calloc.free(proxyInfo.ref.lpszProxy);
|
||||||
|
calloc.free(proxyInfo.ref.lpszProxyBypass);
|
||||||
|
}
|
||||||
|
calloc.free(proxyInfo);
|
||||||
|
|
||||||
|
return result != 0;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 禁用系统代理
|
||||||
|
static bool disableWindowsProxy() {
|
||||||
|
return setWindowsProxyServer(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Pipe {
|
||||||
|
final Pointer<Void> read;
|
||||||
|
final Pointer<Void> write;
|
||||||
|
|
||||||
|
_Pipe(this.read, this.write);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProcessOutput {
|
||||||
|
final String stdout;
|
||||||
|
final String stderr;
|
||||||
|
|
||||||
|
_ProcessOutput(this.stdout, this.stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int TRUE = 1;
|
||||||
|
const int FALSE = 0;
|
||||||
|
|
||||||
|
const int STARTF_USESTDHANDLES = 0x00000100;
|
||||||
|
const int CREATE_NO_WINDOW = 0x08000000;
|
||||||
|
const int HANDLE_FLAG_INHERIT = 0x00000001;
|
||||||
|
const int WAIT_OBJECT_0 = 0x00000000;
|
||||||
|
const int WAIT_FAILED = 0xFFFFFFFF;
|
||||||
|
const int STD_INPUT_HANDLE = -10;
|
||||||
|
const int INVALID_HANDLE_VALUE = -1;
|
||||||
|
|
||||||
|
final class SECURITY_ATTRIBUTES extends Struct {
|
||||||
|
@Uint32()
|
||||||
|
external int nLength;
|
||||||
|
|
||||||
|
external Pointer<Void> lpSecurityDescriptor;
|
||||||
|
|
||||||
|
@Int32()
|
||||||
|
external int bInheritHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class STARTUPINFO extends Struct {
|
||||||
|
@Uint32()
|
||||||
|
external int cb;
|
||||||
|
|
||||||
|
external Pointer<Utf16> lpReserved;
|
||||||
|
|
||||||
|
external Pointer<Utf16> lpDesktop;
|
||||||
|
|
||||||
|
external Pointer<Utf16> lpTitle;
|
||||||
|
|
||||||
|
@Uint32()
|
||||||
|
external int dwX;
|
||||||
|
|
||||||
|
@Uint32()
|
||||||
|
external int dwY;
|
||||||
|
|
||||||
|
@Uint32()
|
||||||
|
external int dwXSize;
|
||||||
|
|
||||||
|
@Uint32()
|
||||||
|
external int dwYSize;
|
||||||
|
|
||||||
|
@Uint32()
|
||||||
|
external int dwXCountChars;
|
||||||
|
|
||||||
|
@Uint32()
|
||||||
|
external int dwYCountChars;
|
||||||
|
|
||||||
|
@Uint32()
|
||||||
|
external int dwFillAttribute;
|
||||||
|
|
||||||
|
@Uint32()
|
||||||
|
external int dwFlags;
|
||||||
|
|
||||||
|
@Uint16()
|
||||||
|
external int wShowWindow;
|
||||||
|
|
||||||
|
@Uint16()
|
||||||
|
external int cbReserved2;
|
||||||
|
|
||||||
|
external Pointer<Uint8> lpReserved2;
|
||||||
|
|
||||||
|
external Pointer<Void> hStdInput;
|
||||||
|
|
||||||
|
external Pointer<Void> hStdOutput;
|
||||||
|
|
||||||
|
external Pointer<Void> hStdError;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class PROCESS_INFORMATION extends Struct {
|
||||||
|
external Pointer<Void> hProcess;
|
||||||
|
|
||||||
|
external Pointer<Void> hThread;
|
||||||
|
|
||||||
|
@Uint32()
|
||||||
|
external int dwProcessId;
|
||||||
|
|
||||||
|
@Uint32()
|
||||||
|
external int dwThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
final DynamicLibrary _kernel32 = DynamicLibrary.open('kernel32.dll');
|
||||||
|
|
||||||
|
final _CreatePipe = _kernel32.lookupFunction<
|
||||||
|
Int32 Function(Pointer<Pointer<Void>>, Pointer<Pointer<Void>>, Pointer<SECURITY_ATTRIBUTES>, Uint32),
|
||||||
|
int Function(Pointer<Pointer<Void>>, Pointer<Pointer<Void>>, Pointer<SECURITY_ATTRIBUTES>, int)>(
|
||||||
|
'CreatePipe',
|
||||||
|
);
|
||||||
|
|
||||||
|
final _SetHandleInformation = _kernel32.lookupFunction<
|
||||||
|
Int32 Function(Pointer<Void>, Uint32, Uint32),
|
||||||
|
int Function(Pointer<Void>, int, int)>(
|
||||||
|
'SetHandleInformation',
|
||||||
|
);
|
||||||
|
|
||||||
|
final _CreateProcessW = _kernel32.lookupFunction<
|
||||||
|
Int32 Function(
|
||||||
|
Pointer<Utf16>,
|
||||||
|
Pointer<Utf16>,
|
||||||
|
Pointer<SECURITY_ATTRIBUTES>,
|
||||||
|
Pointer<SECURITY_ATTRIBUTES>,
|
||||||
|
Int32,
|
||||||
|
Uint32,
|
||||||
|
Pointer<Void>,
|
||||||
|
Pointer<Utf16>,
|
||||||
|
Pointer<STARTUPINFO>,
|
||||||
|
Pointer<PROCESS_INFORMATION>,
|
||||||
|
),
|
||||||
|
int Function(
|
||||||
|
Pointer<Utf16>,
|
||||||
|
Pointer<Utf16>,
|
||||||
|
Pointer<SECURITY_ATTRIBUTES>,
|
||||||
|
Pointer<SECURITY_ATTRIBUTES>,
|
||||||
|
int,
|
||||||
|
int,
|
||||||
|
Pointer<Void>,
|
||||||
|
Pointer<Utf16>,
|
||||||
|
Pointer<STARTUPINFO>,
|
||||||
|
Pointer<PROCESS_INFORMATION>,
|
||||||
|
)>(
|
||||||
|
'CreateProcessW',
|
||||||
|
);
|
||||||
|
|
||||||
|
final _PeekNamedPipe = _kernel32.lookupFunction<
|
||||||
|
Int32 Function(Pointer<Void>, Pointer<Void>, Uint32, Pointer<Uint32>, Pointer<Uint32>, Pointer<Uint32>),
|
||||||
|
int Function(Pointer<Void>, Pointer<Void>, int, Pointer<Uint32>, Pointer<Uint32>, Pointer<Uint32>)>(
|
||||||
|
'PeekNamedPipe',
|
||||||
|
);
|
||||||
|
|
||||||
|
final _ReadFile = _kernel32.lookupFunction<
|
||||||
|
Int32 Function(Pointer<Void>, Pointer<Void>, Uint32, Pointer<Uint32>, Pointer<Void>),
|
||||||
|
int Function(Pointer<Void>, Pointer<Void>, int, Pointer<Uint32>, Pointer<Void>)>(
|
||||||
|
'ReadFile',
|
||||||
|
);
|
||||||
|
|
||||||
|
final _CloseHandle = _kernel32.lookupFunction<
|
||||||
|
Int32 Function(Pointer<Void>),
|
||||||
|
int Function(Pointer<Void>)>(
|
||||||
|
'CloseHandle',
|
||||||
|
);
|
||||||
|
|
||||||
|
final _WaitForSingleObject = _kernel32.lookupFunction<
|
||||||
|
Uint32 Function(Pointer<Void>, Uint32),
|
||||||
|
int Function(Pointer<Void>, int)>(
|
||||||
|
'WaitForSingleObject',
|
||||||
|
);
|
||||||
|
|
||||||
|
final _GetStdHandle = _kernel32.lookupFunction<
|
||||||
|
IntPtr Function(Int32),
|
||||||
|
int Function(int)>(
|
||||||
|
'GetStdHandle',
|
||||||
|
);
|
||||||
|
|
||||||
|
final _GetExitCodeProcess = _kernel32.lookupFunction<
|
||||||
|
Int32 Function(Pointer<Void>, Pointer<Uint32>),
|
||||||
|
int Function(Pointer<Void>, Pointer<Uint32>)>(
|
||||||
|
'GetExitCodeProcess',
|
||||||
|
);
|
||||||
|
|
||||||
|
final _GetLastError = _kernel32.lookupFunction<
|
||||||
|
Uint32 Function(),
|
||||||
|
int Function()>(
|
||||||
|
'GetLastError',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔧 WinINet API for proxy settings - 用于替代 reg 命令,消除黑屏
|
||||||
|
final DynamicLibrary _wininet = DynamicLibrary.open('wininet.dll');
|
||||||
|
|
||||||
|
const int INTERNET_OPTION_PROXY = 38;
|
||||||
|
const int INTERNET_OPEN_TYPE_PROXY = 3;
|
||||||
|
const int INTERNET_OPEN_TYPE_DIRECT = 1;
|
||||||
|
|
||||||
|
final class INTERNET_PROXY_INFO extends Struct {
|
||||||
|
@Int32()
|
||||||
|
external int dwAccessType;
|
||||||
|
|
||||||
|
external Pointer<Utf16> lpszProxy;
|
||||||
|
|
||||||
|
external Pointer<Utf16> lpszProxyBypass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WinINet InternetSetOption API - 用于设置系统代理
|
||||||
|
final _InternetSetOptionW = _wininet.lookupFunction<
|
||||||
|
Int32 Function(Pointer<Void>, Uint32, Pointer<Void>, Uint32),
|
||||||
|
int Function(Pointer<Void>, int, Pointer<Void>, int)>(
|
||||||
|
'InternetSetOptionW',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// WinINet InternetQueryOption API - 用于查询系统代理
|
||||||
|
final _InternetQueryOptionW = _wininet.lookupFunction<
|
||||||
|
Int32 Function(Pointer<Void>, Uint32, Pointer<Void>, Pointer<Uint32>),
|
||||||
|
int Function(Pointer<Void>, int, Pointer<Void>, Pointer<Uint32>)>(
|
||||||
|
'InternetQueryOptionW',
|
||||||
|
);
|
||||||
@ -106,7 +106,7 @@ class _HIDialogState extends State<HIDialog> {
|
|||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(23.r),
|
borderRadius: BorderRadius.circular(23.r),
|
||||||
),
|
),
|
||||||
minimumSize: Size.fromHeight(40.w),
|
minimumSize: Size(85.w, 40.w),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
),
|
),
|
||||||
@ -120,7 +120,7 @@ class _HIDialogState extends State<HIDialog> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
widget.confirmText ?? AppTranslations.kr_dialog.kr_confirm,
|
widget.confirmText ?? (widget.cancelText == null ? '好的' : AppTranslations.kr_dialog.kr_confirm),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16.sp,
|
fontSize: 16.sp,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@ -136,96 +136,106 @@ class _HIDialogState extends State<HIDialog> {
|
|||||||
final dialog = Dialog(
|
final dialog = Dialog(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
insetPadding: EdgeInsets.all(20.w),
|
insetPadding: EdgeInsets.all(20.w),
|
||||||
child: ClipRRect(
|
child: Transform.translate(
|
||||||
borderRadius: BorderRadius.circular(34.r),
|
offset: Offset(0, -30.w),
|
||||||
child: BackdropFilter(
|
child: ClipRRect(
|
||||||
filter: ImageFilter.blur(sigmaX: 26, sigmaY: 26), // 毛玻璃模糊
|
borderRadius: BorderRadius.circular(34.r),
|
||||||
child: Container(
|
child: BackdropFilter(
|
||||||
width: 245.w,
|
filter: ImageFilter.blur(sigmaX: 26, sigmaY: 26), // 毛玻璃模糊
|
||||||
padding: EdgeInsets.fromLTRB(30.w, 16.w, 30.w, 16.w),
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
width: 245.w,
|
||||||
color: const Color(0xF5F5F5).withOpacity(0.6), // 半透明底色
|
padding: EdgeInsets.fromLTRB(30.w, 16.w, 30.w, 16.w),
|
||||||
borderRadius: BorderRadius.circular(34.r),
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
color: const Color(0xFFDEDEDE), // 半透明底色
|
||||||
color: Colors.white.withOpacity(0.2), // 高光边框
|
borderRadius: BorderRadius.circular(34.r),
|
||||||
width: 1.5,
|
border: Border.all(
|
||||||
|
color: Colors.white.withOpacity(0.2), // 高光边框
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
child: Column(
|
||||||
child: Column(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: [
|
if (widget.title != null) ...[
|
||||||
if (widget.title != null) ...[
|
Align(
|
||||||
Align(
|
alignment: Alignment.centerLeft,
|
||||||
alignment: Alignment.centerLeft,
|
child: Text(
|
||||||
child: Text(
|
widget.title!,
|
||||||
widget.title!,
|
style: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: 14.sp,
|
||||||
fontSize: 14.sp,
|
fontWeight: FontWeight.w600,
|
||||||
fontWeight: FontWeight.w600,
|
color: Colors.black,
|
||||||
color: Colors.black,
|
height: 1.3,
|
||||||
fontFamily: 'AlibabaPuHuiTi-Medium',
|
),
|
||||||
height: 1.3,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
SizedBox(height: 4.w),
|
||||||
SizedBox(height: 4.w),
|
],
|
||||||
],
|
if (widget.message != null || widget.customMessageWidget != null) ...[
|
||||||
if (widget.message != null || widget.customMessageWidget != null) ...[
|
Container(
|
||||||
Container(
|
constraints: BoxConstraints(maxHeight: 200.h),
|
||||||
constraints: BoxConstraints(maxHeight: 200.h),
|
child: SingleChildScrollView(
|
||||||
child: SingleChildScrollView(
|
child: widget.customMessageWidget ??
|
||||||
child: widget.customMessageWidget ??
|
Text(
|
||||||
Text(
|
widget.message!,
|
||||||
widget.message!,
|
textAlign: TextAlign.left,
|
||||||
textAlign: TextAlign.left,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14.sp,
|
|
||||||
color: Colors.black,
|
|
||||||
height: 1.4,
|
|
||||||
fontFamily: 'AlibabaPuHuiTi-Medium',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (widget.confirmText != null || widget.cancelText != null) ...[
|
|
||||||
SizedBox(height: 28.w),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
if (widget.confirmText != null) Expanded(child: _buildConfirmButton()),
|
|
||||||
if (widget.cancelText != null && widget.confirmText != null)
|
|
||||||
SizedBox(width: 12.w),
|
|
||||||
if (widget.cancelText != null)
|
|
||||||
Expanded(
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Get.back();
|
|
||||||
widget.onCancel?.call();
|
|
||||||
},
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
backgroundColor: const Color(0xFFFF00B7),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(23.r),
|
|
||||||
),
|
|
||||||
minimumSize: Size.fromHeight(40.w),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
widget.cancelText!,
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16.sp,
|
fontSize: 14.sp,
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
|
height: 1.4,
|
||||||
fontFamily: 'AlibabaPuHuiTi-Medium',
|
fontFamily: 'AlibabaPuHuiTi-Medium',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (widget.confirmText != null || widget.cancelText != null || (widget.confirmText == null && widget.cancelText == null)) ...[
|
||||||
|
SizedBox(height: 12.w),
|
||||||
|
if (widget.confirmText == null && widget.cancelText == null)
|
||||||
|
Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 85.w,
|
||||||
|
child: _buildConfirmButton(),
|
||||||
),
|
),
|
||||||
],
|
)
|
||||||
),
|
else
|
||||||
]
|
Row(
|
||||||
],
|
children: [
|
||||||
|
if (widget.confirmText != null) Expanded(child: _buildConfirmButton()),
|
||||||
|
if (widget.cancelText != null && widget.confirmText != null)
|
||||||
|
SizedBox(width: 12.w),
|
||||||
|
if (widget.cancelText != null)
|
||||||
|
Expanded(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Get.back();
|
||||||
|
widget.onCancel?.call();
|
||||||
|
},
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFFFF00B7),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(23.r),
|
||||||
|
),
|
||||||
|
minimumSize: Size(85.w, 40.w),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
widget.cancelText!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16.sp,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black,
|
||||||
|
fontFamily: 'AlibabaPuHuiTi-Medium',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -6,7 +6,8 @@ import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
|||||||
import 'dart:math' as math; // 导入 math 库以使用 pi
|
import 'dart:math' as math; // 导入 math 库以使用 pi
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../routes/app_pages.dart';
|
import 'package:kaer_with_panels/app/routes/app_pages.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
/// 可折叠列表项的数据模型
|
/// 可折叠列表项的数据模型
|
||||||
class HICollapsibleItem {
|
class HICollapsibleItem {
|
||||||
@ -61,15 +62,11 @@ class _HICollapsibleItemWidgetState extends State<HICollapsibleItemWidget> {
|
|||||||
color: const Color(0xFFADFF5B), // 链接颜色
|
color: const Color(0xFFADFF5B), // 链接颜色
|
||||||
),
|
),
|
||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = () {
|
..onTap = () async {
|
||||||
// 在这里处理点击事件,例如打开一个网页
|
final Uri url = Uri.parse('https://hifastvpn.com/help');
|
||||||
// 注意:您需要添加 url_launcher 依赖
|
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
|
||||||
Get.toNamed(
|
Get.snackbar('错误', '无法打开链接: $url');
|
||||||
Routes.KR_WEBVIEW,
|
}
|
||||||
arguments: {
|
|
||||||
'url': 'https://www.baidu.com',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,117 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
|
||||||
|
|
||||||
/// 语言切换弹框组件
|
|
||||||
class KRLanguageSwitchDialog extends StatelessWidget {
|
|
||||||
const KRLanguageSwitchDialog({super.key});
|
|
||||||
|
|
||||||
/// 显示语言切换弹框的静态方法
|
|
||||||
static Future<void> kr_show() async {
|
|
||||||
final isChineseRegion = await KRLanguageUtils.checkInitialLanguage();
|
|
||||||
if (isChineseRegion) {
|
|
||||||
await Get.dialog(
|
|
||||||
const KRLanguageSwitchDialog(),
|
|
||||||
barrierDismissible: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Dialog(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
child: Container(
|
|
||||||
width: 280.w,
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(24.r),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// 图标
|
|
||||||
KrLocalImage(
|
|
||||||
imageName: 'language_switch',
|
|
||||||
width: 120.w,
|
|
||||||
height: 120.h,
|
|
||||||
),
|
|
||||||
SizedBox(height: 16.h),
|
|
||||||
// 标题
|
|
||||||
Text(
|
|
||||||
'根据您所在地区以及您的语言设置是否切换到中文语言?',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 24.h),
|
|
||||||
// 切换按钮
|
|
||||||
_kr_buildButton(
|
|
||||||
context: context,
|
|
||||||
text: '切换',
|
|
||||||
isPrimary: true,
|
|
||||||
onTap: () async {
|
|
||||||
final zhLanguage = KRLanguage.values.firstWhere(
|
|
||||||
(lang) => lang.countryCode == 'zh',
|
|
||||||
orElse: () => KRLanguage.zh,
|
|
||||||
);
|
|
||||||
await KRLanguageUtils.switchLanguage(zhLanguage);
|
|
||||||
Get.back();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SizedBox(height: 12.h),
|
|
||||||
// 不切换按钮
|
|
||||||
_kr_buildButton(
|
|
||||||
context: context,
|
|
||||||
text: '不切换',
|
|
||||||
isPrimary: false,
|
|
||||||
onTap: () => Get.back(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建按钮
|
|
||||||
Widget _kr_buildButton({
|
|
||||||
required BuildContext context,
|
|
||||||
required String text,
|
|
||||||
required bool isPrimary,
|
|
||||||
required VoidCallback onTap,
|
|
||||||
}) {
|
|
||||||
return InkWell(
|
|
||||||
onTap: onTap,
|
|
||||||
child: Container(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 44.h,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isPrimary ? Colors.blue : Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(22.r),
|
|
||||||
border: isPrimary
|
|
||||||
? null
|
|
||||||
: Border.all(
|
|
||||||
color: Colors.blue,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text(
|
|
||||||
text,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: isPrimary ? Colors.white : Colors.blue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -42,9 +42,8 @@ class KRSubscriptionExpiryText extends StatelessWidget {
|
|||||||
final day = expireDateTime.day.toString().padLeft(2, '0');
|
final day = expireDateTime.day.toString().padLeft(2, '0');
|
||||||
final hour = expireDateTime.hour.toString().padLeft(2, '0');
|
final hour = expireDateTime.hour.toString().padLeft(2, '0');
|
||||||
final minute = expireDateTime.minute.toString().padLeft(2, '0');
|
final minute = expireDateTime.minute.toString().padLeft(2, '0');
|
||||||
final second = expireDateTime.second.toString().padLeft(2, '0');
|
|
||||||
|
|
||||||
final formattedDateTime = '$year/$month/$day $hour:$minute:$second';
|
final formattedDateTime = '$year/$month/$day $hour:$minute';
|
||||||
expiryText = '到期时间:$formattedDateTime';
|
expiryText = '到期时间:$formattedDateTime';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -110,41 +110,28 @@ Widget _myApp(GetxTranslations translations, Locale initialLocale) {
|
|||||||
initialRoute: Routes.KR_SPLASH,
|
initialRoute: Routes.KR_SPLASH,
|
||||||
getPages: AppPages.routes,
|
getPages: AppPages.routes,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
/// 屏幕适配 - 统一使用手机端设计尺寸
|
||||||
/// 屏幕适配
|
ScreenUtil.init(
|
||||||
ScreenUtil.init(context,
|
context,
|
||||||
designSize: const Size(868, 668), minTextAdapt: true);
|
designSize: const Size(375, 812),
|
||||||
} else {
|
minTextAdapt: true,
|
||||||
/// 屏幕适配
|
);
|
||||||
ScreenUtil.init(context,
|
|
||||||
designSize: const Size(375, 667), minTextAdapt: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// child = FlutterEasyLoading(child: child); // 已替换为自定义组件
|
// child = FlutterEasyLoading(child: child); // 已替换为自定义组件
|
||||||
|
|
||||||
// 添加生命周期监听
|
// 添加生命周期监听
|
||||||
Widget wrappedChild = Listener(
|
Widget wrappedChild = Listener(
|
||||||
onPointerDown: (_) async {
|
onPointerDown: (_) async {
|
||||||
// 确保地图缓存已初始化
|
|
||||||
// try {
|
|
||||||
// final store = FMTCStore(KRFMTC.kr_storeName);
|
|
||||||
// if (!await store.manage.ready) {
|
|
||||||
// await store.manage.create();
|
|
||||||
// await KRFMTC.kr_initMapCache();
|
|
||||||
// }
|
|
||||||
// } catch (e) {
|
|
||||||
// print('地图缓存初始化失败: $e');
|
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 如果是 Mac 平台,添加顶部安全区域
|
// 如果是桌面平台,添加顶部安全区域
|
||||||
if (Platform.isMacOS) {
|
if (Platform.isMacOS || Platform.isWindows) {
|
||||||
wrappedChild = MediaQuery(
|
wrappedChild = MediaQuery(
|
||||||
data: MediaQuery.of(context).copyWith(
|
data: MediaQuery.of(context).copyWith(
|
||||||
padding: MediaQuery.of(context).padding.copyWith(
|
padding: MediaQuery.of(context).padding.copyWith(
|
||||||
top: 10.w, // Mac 平台顶部安全区域
|
top: 24.w, // 桌面端顶部安全区域,避开窗口控件
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: wrappedChild,
|
child: wrappedChild,
|
||||||
@ -181,9 +168,15 @@ Widget _myApp(GetxTranslations translations, Locale initialLocale) {
|
|||||||
transitionDuration: TransitionConfig.defaultDuration, // 设置动画持续时间
|
transitionDuration: TransitionConfig.defaultDuration, // 设置动画持续时间
|
||||||
customTransition: TransitionConfig.createDefaultTransition(),
|
customTransition: TransitionConfig.createDefaultTransition(),
|
||||||
routingCallback: (routing) {
|
routingCallback: (routing) {
|
||||||
if (routing == null) return;
|
if (routing == null || routing.current == null) return;
|
||||||
if(Routes.KR_PURCHASE_MEMBERSHIP.contains(routing.current)) return;
|
|
||||||
// 需要显示订阅按钮的路由列表
|
final String current = routing.current!;
|
||||||
|
print('🔥 routing.current: $current');
|
||||||
|
|
||||||
|
// ✅ 同步路由状态到服务,用于内部 UI 响应(如隐藏/显示文字)
|
||||||
|
GlobalOverlayService.instance.updateCurrentRoute(current);
|
||||||
|
|
||||||
|
// ✅ 显式列表,用于容错匹配
|
||||||
const showButtonRoutes = [
|
const showButtonRoutes = [
|
||||||
Routes.MR_LOGIN,
|
Routes.MR_LOGIN,
|
||||||
Routes.HI_MENU,
|
Routes.HI_MENU,
|
||||||
@ -191,11 +184,22 @@ Widget _myApp(GetxTranslations translations, Locale initialLocale) {
|
|||||||
Routes.HI_USER_INFO,
|
Routes.HI_USER_INFO,
|
||||||
Routes.KR_ORDER_STATUS,
|
Routes.KR_ORDER_STATUS,
|
||||||
];
|
];
|
||||||
print('routing.current${routing.current}');
|
|
||||||
GlobalOverlayService.instance.updateSubscriptionButtonColor(null);
|
// 检查是否是购买页(使用多种匹配方式确保成功)
|
||||||
if (showButtonRoutes.contains(routing.current)) {
|
bool isPurchasePage = current == Routes.KR_PURCHASE_MEMBERSHIP ||
|
||||||
|
current.contains(Routes.KR_PURCHASE_MEMBERSHIP) ||
|
||||||
|
Routes.KR_PURCHASE_MEMBERSHIP.contains(current);
|
||||||
|
|
||||||
|
if (isPurchasePage) {
|
||||||
|
// ✅ 购买页:必须可见但透明,解决返回时的遮挡问题
|
||||||
|
GlobalOverlayService.instance.updateSubscriptionButtonColor(Colors.transparent);
|
||||||
|
GlobalOverlayService.instance.safeShowSubscriptionButton();
|
||||||
|
} else if (showButtonRoutes.any((r) => current == r || current.contains(r) || r.contains(current))) {
|
||||||
|
// ✅ 其他白名单页面:可见并恢复默认主色
|
||||||
|
GlobalOverlayService.instance.updateSubscriptionButtonColor(null);
|
||||||
GlobalOverlayService.instance.safeShowSubscriptionButton();
|
GlobalOverlayService.instance.safeShowSubscriptionButton();
|
||||||
} else {
|
} else {
|
||||||
|
// ✅ 默认:隐藏
|
||||||
GlobalOverlayService.instance.hideSubscriptionButton();
|
GlobalOverlayService.instance.hideSubscriptionButton();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import screen_retriever_macos
|
|||||||
import share_plus
|
import share_plus
|
||||||
import tray_manager
|
import tray_manager
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
import webview_flutter_wkwebview
|
|
||||||
import window_manager
|
import window_manager
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
@ -31,6 +30,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
|
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
|
||||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
152
pubspec.lock
@ -5,26 +5,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
|
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "67.0.0"
|
version: "85.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
|
sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.4.1"
|
version: "7.6.0"
|
||||||
analyzer_plugin:
|
analyzer_plugin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer_plugin
|
name: analyzer_plugin
|
||||||
sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
|
sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.3"
|
version: "0.13.4"
|
||||||
ansicolor:
|
ansicolor:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -37,10 +37,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: archive
|
name: archive
|
||||||
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.6.1"
|
version: "4.0.7"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -77,10 +77,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build
|
name: build
|
||||||
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
|
sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.4.2"
|
||||||
build_config:
|
build_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -101,26 +101,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_resolvers
|
name: build_resolvers
|
||||||
sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
|
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2"
|
version: "2.4.4"
|
||||||
build_runner:
|
build_runner:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d"
|
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.13"
|
version: "2.4.15"
|
||||||
build_runner_core:
|
build_runner_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_runner_core
|
name: build_runner_core
|
||||||
sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0
|
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.3.2"
|
version: "8.0.0"
|
||||||
built_collection:
|
built_collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -293,34 +293,42 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: custom_lint_core
|
name: custom_lint_core
|
||||||
sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6
|
sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.3"
|
version: "0.7.5"
|
||||||
|
custom_lint_visitor:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: custom_lint_visitor
|
||||||
|
sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0+7.7.0"
|
||||||
dart_mappable:
|
dart_mappable:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: dart_mappable
|
name: dart_mappable
|
||||||
sha256: "6eda273146ed930c1f161d0b29f4bc9ef9e87ecfb9341607833bf76b008fb7d5"
|
sha256: "2255b2c00e328a65fef5a8df2dabfc0dc9c2e518c33a50051a4519b1c7a28c48"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.3.1"
|
version: "4.5.0"
|
||||||
dart_mappable_builder:
|
dart_mappable_builder:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: dart_mappable_builder
|
name: dart_mappable_builder
|
||||||
sha256: b3673a6d190f2ea766b39ea298d4c55d1caca9382a536cf164ffe7e2f955c501
|
sha256: adea8c55aac73c8254aa14a8272b788eb0f72799dd8e4810a9b664ec9b4e353c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.3.1+1"
|
version: "4.5.0"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dart_style
|
name: dart_style
|
||||||
sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
|
sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.6"
|
version: "3.0.1"
|
||||||
dartx:
|
dartx:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -381,18 +389,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: drift
|
name: drift
|
||||||
sha256: df027d168a2985a2e9da900adeba2ab0136f0d84436592cf3cd5135f82c8579c
|
sha256: "540cf382a3bfa99b76e51514db5b0ebcd81ce3679b7c1c9cb9478ff3735e47a1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.21.0"
|
version: "2.28.2"
|
||||||
drift_dev:
|
drift_dev:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: drift_dev
|
name: drift_dev
|
||||||
sha256: "623649abe932fc17bd32e578e7e05f7ac5e7dd0b33e6c8669a0634105d1389bf"
|
sha256: "68c138e884527d2bd61df2ade276c3a144df84d1adeb0ab8f3196b5afe021bd4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.21.2"
|
version: "2.28.0"
|
||||||
easy_refresh:
|
easy_refresh:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -474,18 +482,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_gen_core
|
name: flutter_gen_core
|
||||||
sha256: "53890b653738f34363d9f0d40f82104c261716bd551d3ba65f648770b6764c21"
|
sha256: b6bafbbd981da2f964eb45bcb8b8a7676a281084f8922c0c75de4cfbaa849311
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.9.0"
|
version: "5.12.0"
|
||||||
flutter_gen_runner:
|
flutter_gen_runner:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_gen_runner
|
name: flutter_gen_runner
|
||||||
sha256: de70b42eb5329f712c8b041069d081ad5fb5109f32d6d1ea9c1b39596786215d
|
sha256: c99b10af9d404e3f46fd1927e7d90099779e935e86022674c4c2a9e6c2a93b29
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.9.0"
|
version: "5.12.0"
|
||||||
flutter_hooks:
|
flutter_hooks:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -652,10 +660,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: freezed
|
name: freezed
|
||||||
sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1
|
sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.2"
|
version: "2.5.8"
|
||||||
freezed_annotation:
|
freezed_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -692,10 +700,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: go_router_builder
|
name: go_router_builder
|
||||||
sha256: "3425b72dea69209754ac6b71b4da34165dcd4d4a2934713029945709a246427a"
|
sha256: "7f6f4bfb97cadc3d25378a0237fe4ddd98b54d6094b5a5c158b775a2cc30843e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.1"
|
version: "2.9.0"
|
||||||
google_identity_services_web:
|
google_identity_services_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -724,10 +732,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: grpc
|
name: grpc
|
||||||
sha256: e93ee3bce45c134bf44e9728119102358c7cd69de7832d9a874e2e74eb8cab40
|
sha256: "2dde469ddd8bbd7a33a0765da417abe1ad2142813efce3a86c512041294e2b26"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.4"
|
version: "4.1.0"
|
||||||
hashcodes:
|
hashcodes:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -808,6 +816,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.0.2"
|
||||||
|
image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.7.2"
|
||||||
image_size_getter:
|
image_size_getter:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -908,10 +924,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: json_serializable
|
name: json_serializable
|
||||||
sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
|
sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.8.0"
|
version: "6.9.5"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1208,6 +1224,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.2"
|
version: "1.5.2"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.3"
|
||||||
protobuf:
|
protobuf:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1292,10 +1316,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: riverpod_analyzer_utils
|
name: riverpod_analyzer_utils
|
||||||
sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f"
|
sha256: "837a6dc33f490706c7f4632c516bcd10804ee4d9ccc8046124ca56388715fdf3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.1"
|
version: "0.5.9"
|
||||||
riverpod_annotation:
|
riverpod_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1308,10 +1332,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: riverpod_generator
|
name: riverpod_generator
|
||||||
sha256: d451608bf17a372025fc36058863737636625dfdb7e3cbf6142e0dfeb366ab22
|
sha256: "120d3310f687f43e7011bb213b90a436f1bbc300f0e4b251a72c39bccb017a4f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.0"
|
version: "2.6.4"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1433,10 +1457,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_gen
|
name: source_gen
|
||||||
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
|
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "2.0.0"
|
||||||
source_helper:
|
source_helper:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1473,10 +1497,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqlparser
|
name: sqlparser
|
||||||
sha256: d77749237609784e337ec36c979d41f6f38a7b279df98622ae23929c8eb954a4
|
sha256: "57090342af1ce32bb499aa641f4ecdd2d6231b9403cea537ac059e803cc20d67"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.39.2"
|
version: "0.41.2"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1725,38 +1749,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.3"
|
||||||
webview_flutter:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: webview_flutter
|
|
||||||
sha256: "889a0a678e7c793c308c68739996227c9661590605e70b1f6cf6b9a6634f7aec"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.10.0"
|
|
||||||
webview_flutter_android:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: webview_flutter_android
|
|
||||||
sha256: "512c26ccc5b8a571fd5d13ec994b7509f142ff6faf85835e243dde3538fdc713"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.3.2"
|
|
||||||
webview_flutter_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: webview_flutter_platform_interface
|
|
||||||
sha256: "7cb32b21825bd65569665c32bb00a34ded5779786d6201f5350979d2d529940d"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.13.0"
|
|
||||||
webview_flutter_wkwebview:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: webview_flutter_wkwebview
|
|
||||||
sha256: a3d461fe3467014e05f3ac4962e5fdde2a4bf44c561cb53e9ae5c586600fdbc3
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.22.0"
|
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -49,7 +49,7 @@ dependencies:
|
|||||||
|
|
||||||
# 网络和数据处理
|
# 网络和数据处理
|
||||||
dio: ^5.4.1
|
dio: ^5.4.1
|
||||||
grpc: ^3.2.4
|
grpc: ^4.1.0
|
||||||
protobuf: ^3.1.0
|
protobuf: ^3.1.0
|
||||||
json_annotation: ^4.9.0
|
json_annotation: ^4.9.0
|
||||||
dart_mappable: ^4.2.1
|
dart_mappable: ^4.2.1
|
||||||
@ -71,7 +71,6 @@ dependencies:
|
|||||||
|
|
||||||
# 平台集成
|
# 平台集成
|
||||||
window_manager: ^0.4.3
|
window_manager: ^0.4.3
|
||||||
webview_flutter: ^4.7.0
|
|
||||||
url_launcher: ^6.3.1
|
url_launcher: ^6.3.1
|
||||||
flutter_inappwebview: ^6.1.5 # 最新稳定版本
|
flutter_inappwebview: ^6.1.5 # 最新稳定版本
|
||||||
crisp_sdk: ^1.1.0 # 使用 crisp_sdk,配合最新的 flutter_inappwebview
|
crisp_sdk: ^1.1.0 # 使用 crisp_sdk,配合最新的 flutter_inappwebview
|
||||||
@ -129,7 +128,7 @@ dev_dependencies:
|
|||||||
drift_dev: ^2.16.0
|
drift_dev: ^2.16.0
|
||||||
ffigen: ^8.0.2
|
ffigen: ^8.0.2
|
||||||
slang_build_runner: ^3.30.0
|
slang_build_runner: ^3.30.0
|
||||||
flutter_gen_runner: ^5.4.0
|
flutter_gen_runner: ^5.12.0
|
||||||
go_router_builder: ^2.4.1
|
go_router_builder: ^2.4.1
|
||||||
dart_mappable_builder: ^4.2.1
|
dart_mappable_builder: ^4.2.1
|
||||||
|
|
||||||
|
|||||||