A photo manager for VRChat.

f*ck it 0.2.0

+10 -2
changelog
··· 14 14 - Fixed the control icons do they actually do what they're supposed to do 15 15 - Fixed window not focusing when opening from tray 16 16 17 - v0.1.8: 17 + v0.2.0: 18 18 - Migrate to tauri v2 19 - - Photo loading is slightly faster 19 + 20 + - Photos shouldn't cause the ui to lag while loading 21 + - Removed the metadata loading screen in favour of loading the metadata just before an image it rendered 22 + - Added the context menu back to the photo viewer screen 23 + - Fixed some weird bugs where the world data cache would be ignored 24 + - Fixed the ui forgetting the user account in some cases where the token stored it still valid 25 + 26 + Dev Stuff: 27 + - Fixed indentation to be more constistant
+1
public/icon/clock-regular.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M464 256A208 208 0 1 1 48 256a208 208 0 1 1 416 0zM0 256a256 256 0 1 0 512 0A256 256 0 1 0 0 256zM232 120l0 136c0 8 4 15.5 10.7 20l96 64c11 7.4 25.9 4.4 33.3-6.7s4.4-25.9-6.7-33.3L280 243.2 280 120c0-13.3-10.7-24-24-24s-24 10.7-24 24z"/></svg>
+1
public/icon/sliders-solid.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M0 416c0 17.7 14.3 32 32 32l54.7 0c12.3 28.3 40.5 48 73.3 48s61-19.7 73.3-48L480 448c17.7 0 32-14.3 32-32s-14.3-32-32-32l-246.7 0c-12.3-28.3-40.5-48-73.3-48s-61 19.7-73.3 48L32 384c-17.7 0-32 14.3-32 32zm128 0a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zM320 256a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zm32-80c-32.8 0-61 19.7-73.3 48L32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l246.7 0c12.3 28.3 40.5 48 73.3 48s61-19.7 73.3-48l54.7 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-54.7 0c-12.3-28.3-40.5-48-73.3-48zM192 128a32 32 0 1 1 0-64 32 32 0 1 1 0 64zm73.3-64C253 35.7 224.8 16 192 16s-61 19.7-73.3 48L32 64C14.3 64 0 78.3 0 96s14.3 32 32 32l86.7 0c12.3 28.3 40.5 48 73.3 48s61-19.7 73.3-48L480 128c17.7 0 32-14.3 32-32s-14.3-32-32-32L265.3 64z"/></svg>
+559
src-tauri/Cargo.lock
··· 33 33 ] 34 34 35 35 [[package]] 36 + name = "aligned-vec" 37 + version = "0.5.0" 38 + source = "registry+https://github.com/rust-lang/crates.io-index" 39 + checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" 40 + 41 + [[package]] 36 42 name = "alloc-no-stdlib" 37 43 version = "2.0.4" 38 44 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 69 75 checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 70 76 71 77 [[package]] 78 + name = "arbitrary" 79 + version = "1.3.2" 80 + source = "registry+https://github.com/rust-lang/crates.io-index" 81 + checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" 82 + 83 + [[package]] 84 + name = "arg_enum_proc_macro" 85 + version = "0.3.4" 86 + source = "registry+https://github.com/rust-lang/crates.io-index" 87 + checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" 88 + dependencies = [ 89 + "proc-macro2", 90 + "quote", 91 + "syn 2.0.75", 92 + ] 93 + 94 + [[package]] 95 + name = "arrayvec" 96 + version = "0.7.6" 97 + source = "registry+https://github.com/rust-lang/crates.io-index" 98 + checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 99 + 100 + [[package]] 72 101 name = "atk" 73 102 version = "0.18.0" 74 103 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 104 133 checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 105 134 106 135 [[package]] 136 + name = "av1-grain" 137 + version = "0.2.3" 138 + source = "registry+https://github.com/rust-lang/crates.io-index" 139 + checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" 140 + dependencies = [ 141 + "anyhow", 142 + "arrayvec", 143 + "log", 144 + "nom", 145 + "num-rational", 146 + "v_frame", 147 + ] 148 + 149 + [[package]] 150 + name = "avif-serialize" 151 + version = "0.8.1" 152 + source = "registry+https://github.com/rust-lang/crates.io-index" 153 + checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" 154 + dependencies = [ 155 + "arrayvec", 156 + ] 157 + 158 + [[package]] 107 159 name = "backtrace" 108 160 version = "0.3.73" 109 161 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 131 183 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 132 184 133 185 [[package]] 186 + name = "bit_field" 187 + version = "0.10.2" 188 + source = "registry+https://github.com/rust-lang/crates.io-index" 189 + checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" 190 + 191 + [[package]] 134 192 name = "bitflags" 135 193 version = "1.3.2" 136 194 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 144 202 dependencies = [ 145 203 "serde", 146 204 ] 205 + 206 + [[package]] 207 + name = "bitstream-io" 208 + version = "2.5.3" 209 + source = "registry+https://github.com/rust-lang/crates.io-index" 210 + checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" 147 211 148 212 [[package]] 149 213 name = "block" ··· 189 253 "alloc-no-stdlib", 190 254 "alloc-stdlib", 191 255 ] 256 + 257 + [[package]] 258 + name = "built" 259 + version = "0.7.4" 260 + source = "registry+https://github.com/rust-lang/crates.io-index" 261 + checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4" 192 262 193 263 [[package]] 194 264 name = "bumpalo" ··· 209 279 checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 210 280 211 281 [[package]] 282 + name = "byteorder-lite" 283 + version = "0.1.0" 284 + source = "registry+https://github.com/rust-lang/crates.io-index" 285 + checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 286 + 287 + [[package]] 212 288 name = "bytes" 213 289 version = "1.7.1" 214 290 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 290 366 source = "registry+https://github.com/rust-lang/crates.io-index" 291 367 checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" 292 368 dependencies = [ 369 + "jobserver", 370 + "libc", 293 371 "shlex", 294 372 ] 295 373 ··· 376 454 ] 377 455 378 456 [[package]] 457 + name = "color_quant" 458 + version = "1.1.0" 459 + source = "registry+https://github.com/rust-lang/crates.io-index" 460 + checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 461 + 462 + [[package]] 379 463 name = "combine" 380 464 version = "4.6.7" 381 465 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 521 605 ] 522 606 523 607 [[package]] 608 + name = "crossbeam-deque" 609 + version = "0.8.5" 610 + source = "registry+https://github.com/rust-lang/crates.io-index" 611 + checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 612 + dependencies = [ 613 + "crossbeam-epoch", 614 + "crossbeam-utils", 615 + ] 616 + 617 + [[package]] 618 + name = "crossbeam-epoch" 619 + version = "0.9.18" 620 + source = "registry+https://github.com/rust-lang/crates.io-index" 621 + checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 622 + dependencies = [ 623 + "crossbeam-utils", 624 + ] 625 + 626 + [[package]] 524 627 name = "crossbeam-utils" 525 628 version = "0.8.20" 526 629 source = "registry+https://github.com/rust-lang/crates.io-index" 527 630 checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 528 631 529 632 [[package]] 633 + name = "crunchy" 634 + version = "0.2.2" 635 + source = "registry+https://github.com/rust-lang/crates.io-index" 636 + checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 637 + 638 + [[package]] 530 639 name = "crypto-common" 531 640 version = "0.1.6" 532 641 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 698 807 ] 699 808 700 809 [[package]] 810 + name = "document-features" 811 + version = "0.2.10" 812 + source = "registry+https://github.com/rust-lang/crates.io-index" 813 + checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" 814 + dependencies = [ 815 + "litrs", 816 + ] 817 + 818 + [[package]] 701 819 name = "dpi" 702 820 version = "0.1.1" 703 821 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 732 850 version = "1.0.17" 733 851 source = "registry+https://github.com/rust-lang/crates.io-index" 734 852 checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" 853 + 854 + [[package]] 855 + name = "either" 856 + version = "1.13.0" 857 + source = "registry+https://github.com/rust-lang/crates.io-index" 858 + checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 735 859 736 860 [[package]] 737 861 name = "embed-resource" ··· 789 913 ] 790 914 791 915 [[package]] 916 + name = "exr" 917 + version = "1.72.0" 918 + source = "registry+https://github.com/rust-lang/crates.io-index" 919 + checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" 920 + dependencies = [ 921 + "bit_field", 922 + "flume", 923 + "half", 924 + "lebe", 925 + "miniz_oxide 0.7.4", 926 + "rayon-core", 927 + "smallvec", 928 + "zune-inflate", 929 + ] 930 + 931 + [[package]] 932 + name = "fast_image_resize" 933 + version = "4.2.1" 934 + source = "registry+https://github.com/rust-lang/crates.io-index" 935 + checksum = "2ca4b58827213977eabab8ee8d8258db8441338f3a1832a1c0f2de3372175531" 936 + dependencies = [ 937 + "bytemuck", 938 + "cfg-if", 939 + "document-features", 940 + "image 0.25.2", 941 + "num-traits", 942 + "thiserror", 943 + ] 944 + 945 + [[package]] 792 946 name = "fastrand" 793 947 version = "2.1.0" 794 948 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 842 996 checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" 843 997 dependencies = [ 844 998 "bitflags 1.3.2", 999 + ] 1000 + 1001 + [[package]] 1002 + name = "flume" 1003 + version = "0.11.0" 1004 + source = "registry+https://github.com/rust-lang/crates.io-index" 1005 + checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" 1006 + dependencies = [ 1007 + "spin", 845 1008 ] 846 1009 847 1010 [[package]] ··· 1147 1310 ] 1148 1311 1149 1312 [[package]] 1313 + name = "gif" 1314 + version = "0.13.1" 1315 + source = "registry+https://github.com/rust-lang/crates.io-index" 1316 + checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" 1317 + dependencies = [ 1318 + "color_quant", 1319 + "weezl", 1320 + ] 1321 + 1322 + [[package]] 1150 1323 name = "gimli" 1151 1324 version = "0.29.0" 1152 1325 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1320 1493 ] 1321 1494 1322 1495 [[package]] 1496 + name = "half" 1497 + version = "2.4.1" 1498 + source = "registry+https://github.com/rust-lang/crates.io-index" 1499 + checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" 1500 + dependencies = [ 1501 + "cfg-if", 1502 + "crunchy", 1503 + ] 1504 + 1505 + [[package]] 1323 1506 name = "hashbrown" 1324 1507 version = "0.12.3" 1325 1508 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1543 1726 ] 1544 1727 1545 1728 [[package]] 1729 + name = "image" 1730 + version = "0.24.9" 1731 + source = "registry+https://github.com/rust-lang/crates.io-index" 1732 + checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" 1733 + dependencies = [ 1734 + "bytemuck", 1735 + "byteorder", 1736 + "color_quant", 1737 + "num-traits", 1738 + "png", 1739 + ] 1740 + 1741 + [[package]] 1742 + name = "image" 1743 + version = "0.25.2" 1744 + source = "registry+https://github.com/rust-lang/crates.io-index" 1745 + checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" 1746 + dependencies = [ 1747 + "bytemuck", 1748 + "byteorder-lite", 1749 + "color_quant", 1750 + "exr", 1751 + "gif", 1752 + "image-webp", 1753 + "num-traits", 1754 + "png", 1755 + "qoi", 1756 + "ravif", 1757 + "rayon", 1758 + "rgb", 1759 + "tiff", 1760 + "zune-core", 1761 + "zune-jpeg", 1762 + ] 1763 + 1764 + [[package]] 1765 + name = "image-webp" 1766 + version = "0.1.3" 1767 + source = "registry+https://github.com/rust-lang/crates.io-index" 1768 + checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" 1769 + dependencies = [ 1770 + "byteorder-lite", 1771 + "quick-error", 1772 + ] 1773 + 1774 + [[package]] 1775 + name = "imgref" 1776 + version = "1.10.1" 1777 + source = "registry+https://github.com/rust-lang/crates.io-index" 1778 + checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" 1779 + 1780 + [[package]] 1546 1781 name = "indexmap" 1547 1782 version = "1.9.3" 1548 1783 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1612 1847 ] 1613 1848 1614 1849 [[package]] 1850 + name = "interpolate_name" 1851 + version = "0.2.4" 1852 + source = "registry+https://github.com/rust-lang/crates.io-index" 1853 + checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" 1854 + dependencies = [ 1855 + "proc-macro2", 1856 + "quote", 1857 + "syn 2.0.75", 1858 + ] 1859 + 1860 + [[package]] 1615 1861 name = "interprocess" 1616 1862 version = "1.2.1" 1617 1863 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1647 1893 dependencies = [ 1648 1894 "is-docker", 1649 1895 "once_cell", 1896 + ] 1897 + 1898 + [[package]] 1899 + name = "itertools" 1900 + version = "0.12.1" 1901 + source = "registry+https://github.com/rust-lang/crates.io-index" 1902 + checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 1903 + dependencies = [ 1904 + "either", 1650 1905 ] 1651 1906 1652 1907 [[package]] ··· 1707 1962 checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 1708 1963 1709 1964 [[package]] 1965 + name = "jobserver" 1966 + version = "0.1.32" 1967 + source = "registry+https://github.com/rust-lang/crates.io-index" 1968 + checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" 1969 + dependencies = [ 1970 + "libc", 1971 + ] 1972 + 1973 + [[package]] 1974 + name = "jpeg-decoder" 1975 + version = "0.3.1" 1976 + source = "registry+https://github.com/rust-lang/crates.io-index" 1977 + checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" 1978 + 1979 + [[package]] 1710 1980 name = "js-sys" 1711 1981 version = "0.3.70" 1712 1982 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1800 2070 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1801 2071 1802 2072 [[package]] 2073 + name = "lebe" 2074 + version = "0.5.2" 2075 + source = "registry+https://github.com/rust-lang/crates.io-index" 2076 + checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" 2077 + 2078 + [[package]] 1803 2079 name = "libappindicator" 1804 2080 version = "0.9.0" 1805 2081 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1830 2106 checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 1831 2107 1832 2108 [[package]] 2109 + name = "libfuzzer-sys" 2110 + version = "0.4.7" 2111 + source = "registry+https://github.com/rust-lang/crates.io-index" 2112 + checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" 2113 + dependencies = [ 2114 + "arbitrary", 2115 + "cc", 2116 + "once_cell", 2117 + ] 2118 + 2119 + [[package]] 1833 2120 name = "libloading" 1834 2121 version = "0.7.4" 1835 2122 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1857 2144 checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 1858 2145 1859 2146 [[package]] 2147 + name = "litrs" 2148 + version = "0.4.1" 2149 + source = "registry+https://github.com/rust-lang/crates.io-index" 2150 + checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 2151 + 2152 + [[package]] 1860 2153 name = "lock_api" 1861 2154 version = "0.4.12" 1862 2155 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1888 2181 ] 1889 2182 1890 2183 [[package]] 2184 + name = "loop9" 2185 + version = "0.1.5" 2186 + source = "registry+https://github.com/rust-lang/crates.io-index" 2187 + checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" 2188 + dependencies = [ 2189 + "imgref", 2190 + ] 2191 + 2192 + [[package]] 1891 2193 name = "mac" 1892 2194 version = "0.1.1" 1893 2195 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1932 2234 checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" 1933 2235 1934 2236 [[package]] 2237 + name = "maybe-rayon" 2238 + version = "0.1.1" 2239 + source = "registry+https://github.com/rust-lang/crates.io-index" 2240 + checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" 2241 + dependencies = [ 2242 + "cfg-if", 2243 + ] 2244 + 2245 + [[package]] 1935 2246 name = "memchr" 1936 2247 version = "2.7.4" 1937 2248 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1953 2264 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1954 2265 1955 2266 [[package]] 2267 + name = "minimal-lexical" 2268 + version = "0.2.1" 2269 + source = "registry+https://github.com/rust-lang/crates.io-index" 2270 + checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 2271 + 2272 + [[package]] 1956 2273 name = "miniz_oxide" 1957 2274 version = "0.7.4" 1958 2275 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2085 2402 checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" 2086 2403 2087 2404 [[package]] 2405 + name = "nom" 2406 + version = "7.1.3" 2407 + source = "registry+https://github.com/rust-lang/crates.io-index" 2408 + checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 2409 + dependencies = [ 2410 + "memchr", 2411 + "minimal-lexical", 2412 + ] 2413 + 2414 + [[package]] 2415 + name = "noop_proc_macro" 2416 + version = "0.3.0" 2417 + source = "registry+https://github.com/rust-lang/crates.io-index" 2418 + checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" 2419 + 2420 + [[package]] 2088 2421 name = "notify" 2089 2422 version = "6.1.1" 2090 2423 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2114 2447 ] 2115 2448 2116 2449 [[package]] 2450 + name = "num-bigint" 2451 + version = "0.4.6" 2452 + source = "registry+https://github.com/rust-lang/crates.io-index" 2453 + checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 2454 + dependencies = [ 2455 + "num-integer", 2456 + "num-traits", 2457 + ] 2458 + 2459 + [[package]] 2117 2460 name = "num-conv" 2118 2461 version = "0.1.0" 2119 2462 source = "registry+https://github.com/rust-lang/crates.io-index" 2120 2463 checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 2121 2464 2122 2465 [[package]] 2466 + name = "num-derive" 2467 + version = "0.4.2" 2468 + source = "registry+https://github.com/rust-lang/crates.io-index" 2469 + checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 2470 + dependencies = [ 2471 + "proc-macro2", 2472 + "quote", 2473 + "syn 2.0.75", 2474 + ] 2475 + 2476 + [[package]] 2477 + name = "num-integer" 2478 + version = "0.1.46" 2479 + source = "registry+https://github.com/rust-lang/crates.io-index" 2480 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 2481 + dependencies = [ 2482 + "num-traits", 2483 + ] 2484 + 2485 + [[package]] 2486 + name = "num-rational" 2487 + version = "0.4.2" 2488 + source = "registry+https://github.com/rust-lang/crates.io-index" 2489 + checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 2490 + dependencies = [ 2491 + "num-bigint", 2492 + "num-integer", 2493 + "num-traits", 2494 + ] 2495 + 2496 + [[package]] 2123 2497 name = "num-traits" 2124 2498 version = "0.2.19" 2125 2499 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2434 2808 ] 2435 2809 2436 2810 [[package]] 2811 + name = "paste" 2812 + version = "1.0.15" 2813 + source = "registry+https://github.com/rust-lang/crates.io-index" 2814 + checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 2815 + 2816 + [[package]] 2437 2817 name = "pathdiff" 2438 2818 version = "0.2.1" 2439 2819 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2724 3104 ] 2725 3105 2726 3106 [[package]] 3107 + name = "profiling" 3108 + version = "1.0.15" 3109 + source = "registry+https://github.com/rust-lang/crates.io-index" 3110 + checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" 3111 + dependencies = [ 3112 + "profiling-procmacros", 3113 + ] 3114 + 3115 + [[package]] 3116 + name = "profiling-procmacros" 3117 + version = "1.0.15" 3118 + source = "registry+https://github.com/rust-lang/crates.io-index" 3119 + checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" 3120 + dependencies = [ 3121 + "quote", 3122 + "syn 2.0.75", 3123 + ] 3124 + 3125 + [[package]] 2727 3126 name = "psl-types" 2728 3127 version = "2.0.11" 2729 3128 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2738 3137 "idna 0.3.0", 2739 3138 "psl-types", 2740 3139 ] 3140 + 3141 + [[package]] 3142 + name = "qoi" 3143 + version = "0.4.1" 3144 + source = "registry+https://github.com/rust-lang/crates.io-index" 3145 + checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" 3146 + dependencies = [ 3147 + "bytemuck", 3148 + ] 3149 + 3150 + [[package]] 3151 + name = "quick-error" 3152 + version = "2.0.1" 3153 + source = "registry+https://github.com/rust-lang/crates.io-index" 3154 + checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" 2741 3155 2742 3156 [[package]] 2743 3157 name = "quick-xml" ··· 2887 3301 ] 2888 3302 2889 3303 [[package]] 3304 + name = "rav1e" 3305 + version = "0.7.1" 3306 + source = "registry+https://github.com/rust-lang/crates.io-index" 3307 + checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" 3308 + dependencies = [ 3309 + "arbitrary", 3310 + "arg_enum_proc_macro", 3311 + "arrayvec", 3312 + "av1-grain", 3313 + "bitstream-io", 3314 + "built", 3315 + "cfg-if", 3316 + "interpolate_name", 3317 + "itertools", 3318 + "libc", 3319 + "libfuzzer-sys", 3320 + "log", 3321 + "maybe-rayon", 3322 + "new_debug_unreachable", 3323 + "noop_proc_macro", 3324 + "num-derive", 3325 + "num-traits", 3326 + "once_cell", 3327 + "paste", 3328 + "profiling", 3329 + "rand 0.8.5", 3330 + "rand_chacha 0.3.1", 3331 + "simd_helpers", 3332 + "system-deps", 3333 + "thiserror", 3334 + "v_frame", 3335 + "wasm-bindgen", 3336 + ] 3337 + 3338 + [[package]] 3339 + name = "ravif" 3340 + version = "0.11.10" 3341 + source = "registry+https://github.com/rust-lang/crates.io-index" 3342 + checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd" 3343 + dependencies = [ 3344 + "avif-serialize", 3345 + "imgref", 3346 + "loop9", 3347 + "quick-error", 3348 + "rav1e", 3349 + "rgb", 3350 + ] 3351 + 3352 + [[package]] 2890 3353 name = "raw-window-handle" 2891 3354 version = "0.6.2" 2892 3355 source = "registry+https://github.com/rust-lang/crates.io-index" 2893 3356 checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" 3357 + 3358 + [[package]] 3359 + name = "rayon" 3360 + version = "1.10.0" 3361 + source = "registry+https://github.com/rust-lang/crates.io-index" 3362 + checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 3363 + dependencies = [ 3364 + "either", 3365 + "rayon-core", 3366 + ] 3367 + 3368 + [[package]] 3369 + name = "rayon-core" 3370 + version = "1.12.1" 3371 + source = "registry+https://github.com/rust-lang/crates.io-index" 3372 + checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 3373 + dependencies = [ 3374 + "crossbeam-deque", 3375 + "crossbeam-utils", 3376 + ] 2894 3377 2895 3378 [[package]] 2896 3379 name = "redox_syscall" ··· 3010 3493 ] 3011 3494 3012 3495 [[package]] 3496 + name = "rgb" 3497 + version = "0.8.48" 3498 + source = "registry+https://github.com/rust-lang/crates.io-index" 3499 + checksum = "0f86ae463694029097b846d8f99fd5536740602ae00022c0c50c5600720b2f71" 3500 + dependencies = [ 3501 + "bytemuck", 3502 + ] 3503 + 3504 + [[package]] 3013 3505 name = "ring" 3014 3506 version = "0.17.8" 3015 3507 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3410 3902 checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 3411 3903 3412 3904 [[package]] 3905 + name = "simd_helpers" 3906 + version = "0.1.0" 3907 + source = "registry+https://github.com/rust-lang/crates.io-index" 3908 + checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" 3909 + dependencies = [ 3910 + "quote", 3911 + ] 3912 + 3913 + [[package]] 3413 3914 name = "siphasher" 3414 3915 version = "0.3.11" 3415 3916 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3494 3995 version = "0.9.8" 3495 3996 source = "registry+https://github.com/rust-lang/crates.io-index" 3496 3997 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 3998 + dependencies = [ 3999 + "lock_api", 4000 + ] 3497 4001 3498 4002 [[package]] 3499 4003 name = "stable_deref_trait" ··· 3698 4202 "gtk", 3699 4203 "heck 0.5.0", 3700 4204 "http", 4205 + "image 0.24.9", 3701 4206 "jni", 3702 4207 "libc", 3703 4208 "log", ··· 4071 4576 ] 4072 4577 4073 4578 [[package]] 4579 + name = "tiff" 4580 + version = "0.9.1" 4581 + source = "registry+https://github.com/rust-lang/crates.io-index" 4582 + checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" 4583 + dependencies = [ 4584 + "flate2", 4585 + "jpeg-decoder", 4586 + "weezl", 4587 + ] 4588 + 4589 + [[package]] 4074 4590 name = "time" 4075 4591 version = "0.3.36" 4076 4592 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4484 5000 ] 4485 5001 4486 5002 [[package]] 5003 + name = "v_frame" 5004 + version = "0.3.8" 5005 + source = "registry+https://github.com/rust-lang/crates.io-index" 5006 + checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" 5007 + dependencies = [ 5008 + "aligned-vec", 5009 + "num-traits", 5010 + "wasm-bindgen", 5011 + ] 5012 + 5013 + [[package]] 4487 5014 name = "valuable" 4488 5015 version = "0.1.0" 4489 5016 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4512 5039 version = "0.1.7" 4513 5040 dependencies = [ 4514 5041 "dirs", 5042 + "fast_image_resize", 5043 + "image 0.25.2", 4515 5044 "mslnk", 4516 5045 "notify", 4517 5046 "open", ··· 4756 5285 "windows 0.58.0", 4757 5286 "windows-core 0.58.0", 4758 5287 ] 5288 + 5289 + [[package]] 5290 + name = "weezl" 5291 + version = "0.1.8" 5292 + source = "registry+https://github.com/rust-lang/crates.io-index" 5293 + checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" 4759 5294 4760 5295 [[package]] 4761 5296 name = "winapi" ··· 5233 5768 version = "1.8.1" 5234 5769 source = "registry+https://github.com/rust-lang/crates.io-index" 5235 5770 checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 5771 + 5772 + [[package]] 5773 + name = "zune-core" 5774 + version = "0.4.12" 5775 + source = "registry+https://github.com/rust-lang/crates.io-index" 5776 + checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" 5777 + 5778 + [[package]] 5779 + name = "zune-inflate" 5780 + version = "0.2.54" 5781 + source = "registry+https://github.com/rust-lang/crates.io-index" 5782 + checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" 5783 + dependencies = [ 5784 + "simd-adler32", 5785 + ] 5786 + 5787 + [[package]] 5788 + name = "zune-jpeg" 5789 + version = "0.4.13" 5790 + source = "registry+https://github.com/rust-lang/crates.io-index" 5791 + checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" 5792 + dependencies = [ 5793 + "zune-core", 5794 + ]
+3 -1
src-tauri/Cargo.toml
··· 11 11 tauri-build = { version = "2.0.0-rc", features = [] } 12 12 13 13 [dependencies] 14 - tauri = { version = "2.0.0-rc", features = ["tray-icon"] } 14 + tauri = { version = "2.0.0-rc", features = ["tray-icon", "image-png"] } 15 15 serde = { version = "1.0", features = ["derive"] } 16 16 serde_json = "1.0" 17 17 open = "5.1.2" ··· 24 24 tauri-plugin-shell = "2.0.0-rc.2" 25 25 tauri-plugin-http = "2.0.0-rc.0" 26 26 tauri-plugin-process = "2.0.0-rc.0" 27 + image = "0.25.2" 28 + fast_image_resize = { version = "4.2.1", features = [ "image" ] } 27 29 28 30 [features] 29 31 # this feature is used for production builds or when `devPath` points to the filesystem
+1
src-tauri/capabilities/migrated.json
··· 11 11 "core:window:allow-unmaximize", 12 12 "core:window:allow-minimize", 13 13 "core:window:allow-unminimize", 14 + "core:window:allow-toggle-maximize", 14 15 "core:window:allow-show", 15 16 "core:window:allow-hide", 16 17 "core:window:allow-close",
+1 -1
src-tauri/gen/schemas/capabilities.json
··· 1 - {"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-start-dragging","shell:allow-open",{"identifier":"http:default","allow":[{"url":"https://photos.phazed.xyz/*"},{"url":"https://photos-cdn.phazed.xyz/*"}]},"process:allow-restart","shell:default","http:default","process:default"]}} 1 + {"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-toggle-maximize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-start-dragging","shell:allow-open",{"identifier":"http:default","allow":[{"url":"https://photos.phazed.xyz/*"},{"url":"https://photos-cdn.phazed.xyz/*"}]},"process:allow-restart","shell:default","http:default","process:default"]}}
+419 -360
src-tauri/src/main.rs
··· 5 5 mod worldscraper; 6 6 7 7 use core::time; 8 + use image::{ codecs::png::{ PngDecoder, PngEncoder }, DynamicImage, ImageEncoder }; 9 + use fast_image_resize::{ images::Image, IntoImageView, ResizeOptions, Resizer }; 8 10 use mslnk::ShellLink; 9 11 use notify::{EventKind, RecursiveMode, Watcher}; 10 12 use pngmeta::PNGImage; 11 13 use regex::Regex; 12 14 use std::{ 13 - env, fs, 14 - io::Read, 15 - path, 16 - process::{self, Command}, 17 - thread, 18 - time::Duration, 15 + env, fs, 16 + io::{ BufReader, Read }, 17 + path, 18 + process::{self, Command}, 19 + thread, 20 + time::Duration, 19 21 }; 20 22 use tauri::{ 21 - http::Response, menu::{ MenuBuilder, MenuItemBuilder }, tray::{ MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent }, AppHandle, Emitter, Manager, WindowEvent 23 + http::Response, menu::{MenuBuilder, MenuItemBuilder}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, AppHandle, Emitter, Manager, WindowEvent 22 24 }; 23 25 use worldscraper::World; 26 + 27 + // TODO: for the love of fuck please seperate this file out into multiple files at some point 28 + // TODO: Linux support 24 29 25 30 // Scans all files under the "Pictures/VRChat" path 26 31 // then sends the list of photos to the frontend 27 32 #[derive(Clone, serde::Serialize)] 28 33 struct PhotosLoadedResponse { 29 - photos: Vec<path::PathBuf>, 30 - size: usize, 34 + photos: Vec<path::PathBuf>, 35 + size: usize, 31 36 } 32 37 33 38 const VERSION: &str = env!("CARGO_PKG_VERSION"); 34 39 35 40 pub fn get_photo_path() -> path::PathBuf { 36 - let config_path = dirs::home_dir() 37 - .unwrap() 38 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.photos_path"); 41 + let config_path = dirs::home_dir() 42 + .unwrap() 43 + .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.photos_path"); 39 44 40 - match fs::read_to_string(config_path) { 41 - Ok(path) => { 42 - if path 43 - != dirs::picture_dir() 44 - .unwrap() 45 - .join("VRChat") 46 - .to_str() 47 - .unwrap() 48 - .to_owned() 49 - { 50 - path::PathBuf::from(path) 51 - } else { 52 - dirs::picture_dir().unwrap().join("VRChat") 53 - } 54 - } 55 - Err(_) => dirs::picture_dir().unwrap().join("VRChat"), 45 + match fs::read_to_string(config_path) { 46 + Ok(path) => { 47 + if path 48 + != dirs::picture_dir() 49 + .unwrap() 50 + .join("VRChat") 51 + .to_str() 52 + .unwrap() 53 + .to_owned() 54 + { 55 + path::PathBuf::from(path) 56 + } else { 57 + dirs::picture_dir().unwrap().join("VRChat") 58 + } 56 59 } 60 + Err(_) => dirs::picture_dir().unwrap().join("VRChat"), 61 + } 57 62 } 58 63 59 64 #[tauri::command] 60 65 fn close_splashscreen(window: tauri::Window) { 61 - window.get_webview_window("main").unwrap().show().unwrap(); 66 + window.get_webview_window("main").unwrap().show().unwrap(); 62 67 } 63 68 64 69 #[tauri::command] 65 70 fn start_user_auth() { 66 - open::that("https://photos.phazed.xyz/api/v1/auth").unwrap(); 71 + open::that("https://photos.phazed.xyz/api/v1/auth").unwrap(); 67 72 } 68 73 69 74 #[tauri::command] 70 75 fn open_url(url: &str) { 71 - open::that(url).unwrap(); 76 + open::that(url).unwrap(); 77 + } 78 + 79 + #[tauri::command] 80 + fn open_folder(url: &str) { 81 + Command::new("explorer.exe") 82 + .arg(format!("/select,{}", url)) 83 + .spawn() 84 + .unwrap(); 72 85 } 73 86 74 87 // Check if the photo config file exists 75 88 // if not just return the default vrchat path 76 89 #[tauri::command] 77 90 fn get_user_photos_path() -> path::PathBuf { 78 - get_photo_path() 91 + get_photo_path() 79 92 } 80 93 81 94 // When the user changes the start with windows toggle 82 95 // create and delete the shortcut from the startup folder 83 96 #[tauri::command] 84 97 fn start_with_win(start: bool) { 85 - thread::spawn(move || { 86 - if start { 87 - let target = dirs::home_dir() 88 - .unwrap() 89 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\vrchat-photo-manager.exe"); 90 - match fs::metadata(&target) { 91 - Ok(_) => { 92 - let lnk = dirs::home_dir().unwrap().join("AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\VRChat Photo Manager.lnk"); 98 + thread::spawn(move || { 99 + if start { 100 + let target = dirs::home_dir() 101 + .unwrap() 102 + .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\vrchat-photo-manager.exe"); 93 103 94 - let sl = ShellLink::new(target).unwrap(); 95 - sl.create_lnk(lnk).unwrap(); 96 - } 97 - Err(_) => {} 98 - } 99 - } else { 100 - let lnk = dirs::home_dir().unwrap().join("AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\VRChat Photo Manager.lnk"); 101 - fs::remove_file(lnk).unwrap(); 104 + match fs::metadata(&target) { 105 + Ok(_) => { 106 + let lnk = dirs::home_dir().unwrap().join("AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\VRChat Photo Manager.lnk"); 107 + 108 + let sl = ShellLink::new(target).unwrap(); 109 + sl.create_lnk(lnk).unwrap(); 102 110 } 103 - }); 111 + Err(_) => {} 112 + } 113 + } else { 114 + let lnk = dirs::home_dir().unwrap().join("AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\VRChat Photo Manager.lnk"); 115 + fs::remove_file(lnk).unwrap(); 116 + } 117 + }); 104 118 } 105 119 106 120 // Load vrchat world data 107 121 #[tauri::command] 108 122 fn find_world_by_id(world_id: String, window: tauri::Window) { 109 - thread::spawn(move || { 110 - let world = World::new(world_id); 111 - window.emit("world_data", world).unwrap(); 112 - }); 123 + thread::spawn(move || { 124 + let world = World::new(world_id); 125 + window.emit("world_data", world).unwrap(); 126 + }); 113 127 } 114 128 115 129 // On requested sync the photos to the cloud 116 130 #[tauri::command] 117 131 fn sync_photos(token: String, window: tauri::Window) { 118 - thread::spawn(move || { 119 - photosync::sync_photos(token, get_photo_path(), window); 120 - }); 132 + thread::spawn(move || { 133 + photosync::sync_photos(token, get_photo_path(), window); 134 + }); 121 135 } 122 136 123 137 #[tauri::command] 124 138 fn load_photos(window: tauri::Window) { 125 - thread::spawn(move || { 126 - let base_dir = get_photo_path(); 139 + thread::spawn(move || { 140 + let base_dir = get_photo_path(); 127 141 128 - let mut photos: Vec<path::PathBuf> = Vec::new(); 129 - let mut size: usize = 0; 142 + let mut photos: Vec<path::PathBuf> = Vec::new(); 143 + let mut size: usize = 0; 130 144 131 - for folder in fs::read_dir(&base_dir).unwrap() { 132 - let f = folder.unwrap(); 145 + for folder in fs::read_dir(&base_dir).unwrap() { 146 + let f = folder.unwrap(); 133 147 134 - if f.metadata().unwrap().is_dir() { 135 - for photo in fs::read_dir(f.path()).unwrap() { 136 - let p = photo.unwrap(); 148 + if f.metadata().unwrap().is_dir() { 149 + for photo in fs::read_dir(f.path()).unwrap() { 150 + let p = photo.unwrap(); 137 151 138 - if p.metadata().unwrap().is_file() { 139 - let fname = p.path(); 152 + if p.metadata().unwrap().is_file() { 153 + let fname = p.path(); 140 154 141 - let re1 = Regex::new(r"(?m)VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}.png").unwrap(); 142 - let re2 = Regex::new( 143 - r"(?m)VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}_wrld_[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}.png").unwrap(); 155 + let re1 = Regex::new(r"(?m)VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}.png").unwrap(); 156 + let re2 = Regex::new( 157 + r"(?m)VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}_wrld_[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}.png").unwrap(); 144 158 145 - if re1.is_match(p.file_name().to_str().unwrap()) 146 - || re2.is_match(p.file_name().to_str().unwrap()) 147 - { 148 - let path = fname.to_path_buf().clone(); 149 - let metadata = fs::metadata(&path).unwrap(); 159 + if re1.is_match(p.file_name().to_str().unwrap()) 160 + || re2.is_match(p.file_name().to_str().unwrap()) 161 + { 162 + let path = fname.to_path_buf().clone(); 163 + let metadata = fs::metadata(&path).unwrap(); 150 164 151 - if metadata.is_file() { 152 - size += metadata.len() as usize; 165 + if metadata.is_file() { 166 + size += metadata.len() as usize; 153 167 154 - let path = path.strip_prefix(&base_dir).unwrap().to_path_buf(); 155 - photos.push(path); 156 - } 157 - } else { 158 - println!("Ignoring {:#?} as it doesn't match regex", p.file_name()); 159 - } 160 - } else { 161 - println!("Ignoring {:#?} as it is a directory", p.file_name()); 162 - } 163 - } 168 + let path = path.strip_prefix(&base_dir).unwrap().to_path_buf(); 169 + photos.push(path); 170 + } 164 171 } else { 165 - println!("Ignoring {:#?} as it isn't a directory", f.file_name()); 172 + println!("Ignoring {:#?} as it doesn't match regex", p.file_name()); 166 173 } 174 + } else { 175 + println!("Ignoring {:#?} as it is a directory", p.file_name()); 176 + } 167 177 } 178 + } else { 179 + println!("Ignoring {:#?} as it isn't a directory", f.file_name()); 180 + } 181 + } 168 182 169 - println!("Found {} photos", photos.len()); 170 - window 171 - .emit("photos_loaded", PhotosLoadedResponse { photos, size }) 172 - .unwrap(); 173 - }); 183 + println!("Found {} photos", photos.len()); 184 + window 185 + .emit("photos_loaded", PhotosLoadedResponse { photos, size }) 186 + .unwrap(); 187 + }); 174 188 } 175 189 176 190 // Reads the PNG file and loads the image metadata from it 177 191 // then sends the metadata to the frontend, returns width, height, colour depth and so on... more info "pngmeta.rs" 178 192 #[tauri::command] 179 193 fn load_photo_meta(photo: &str, window: tauri::Window) { 180 - let photo = photo.to_string(); 194 + let photo = photo.to_string(); 181 195 182 - thread::spawn(move || { 183 - let base_dir = get_photo_path().join(&photo); 196 + thread::spawn(move || { 197 + let base_dir = get_photo_path().join(&photo); 184 198 185 - let file = fs::File::open(base_dir.clone()); 199 + let file = fs::File::open(base_dir.clone()); 186 200 187 - match file { 188 - Ok(mut file) => { 189 - let mut buffer = Vec::new(); 201 + match file { 202 + Ok(mut file) => { 203 + let mut buffer = Vec::new(); 190 204 191 - let _out = file.read_to_end(&mut buffer); 192 - window 193 - .emit("photo_meta_loaded", PNGImage::new(buffer, photo)) 194 - .unwrap(); 195 - } 196 - Err(_) => { 197 - println!("Cannot read image file"); 198 - } 199 - } 200 - }); 205 + let _out = file.read_to_end(&mut buffer); 206 + window 207 + .emit("photo_meta_loaded", PNGImage::new(buffer, photo)) 208 + .unwrap(); 209 + } 210 + Err(_) => { 211 + println!("Cannot read image file"); 212 + } 213 + } 214 + }); 201 215 } 202 216 203 217 // Delete a photo when the users confirms the prompt in the ui 204 218 #[tauri::command] 205 219 fn delete_photo(path: String, token: String, is_syncing: bool) { 206 - thread::spawn(move || { 207 - let p = get_photo_path().join(&path); 208 - fs::remove_file(p).unwrap(); 220 + thread::spawn(move || { 221 + let p = get_photo_path().join(&path); 222 + fs::remove_file(p).unwrap(); 209 223 210 - let photo = path.split("\\").last().unwrap(); 224 + let photo = path.split("\\").last().unwrap(); 211 225 212 - if is_syncing { 213 - let client = reqwest::blocking::Client::new(); 214 - client 215 - .delete(format!( 216 - "https://photos-cdn.phazed.xyz/api/v1/photos?token={}&photo={}", 217 - token, photo 218 - )) 219 - .timeout(Duration::from_secs(120)) 220 - .send() 221 - .unwrap(); 222 - } 223 - }); 226 + if is_syncing { 227 + let client = reqwest::blocking::Client::new(); 228 + client 229 + .delete(format!( 230 + "https://photos-cdn.phazed.xyz/api/v1/photos?token={}&photo={}", 231 + token, photo 232 + )) 233 + .timeout(Duration::from_secs(120)) 234 + .send() 235 + .unwrap(); 236 + } 237 + }); 224 238 } 225 239 226 240 #[tauri::command] 227 241 fn change_final_path(new_path: &str) { 228 - let config_path = dirs::home_dir() 229 - .unwrap() 230 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.photos_path"); 231 - fs::write(&config_path, new_path.as_bytes()).unwrap(); 242 + let config_path = dirs::home_dir() 243 + .unwrap() 244 + .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.photos_path"); 232 245 233 - match fs::metadata(&new_path) { 234 - Ok(_) => {} 235 - Err(_) => { 236 - fs::create_dir(&new_path).unwrap(); 237 - } 238 - }; 246 + fs::write(&config_path, new_path.as_bytes()).unwrap(); 247 + 248 + match fs::metadata(&new_path) { 249 + Ok(_) => {} 250 + Err(_) => { 251 + fs::create_dir(&new_path).unwrap(); 252 + } 253 + }; 239 254 } 240 255 241 256 #[tauri::command] 242 257 fn get_version() -> String { 243 - String::from(VERSION) 258 + String::from(VERSION) 244 259 } 245 260 246 261 #[tauri::command] 247 262 fn relaunch() { 248 - let container_folder = dirs::home_dir() 249 - .unwrap() 250 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager"); 263 + let container_folder = dirs::home_dir() 264 + .unwrap() 265 + .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager"); 251 266 252 - let mut cmd = Command::new(&container_folder.join("./vrchat-photo-manager.exe")); 253 - cmd.current_dir(container_folder); 254 - cmd.spawn().expect("Cannot run updater"); 267 + let mut cmd = Command::new(&container_folder.join("./vrchat-photo-manager.exe")); 268 + cmd.current_dir(container_folder); 269 + cmd.spawn().expect("Cannot run updater"); 255 270 256 - process::exit(0); 271 + process::exit(0); 257 272 } 258 273 259 274 fn main() { 260 - std::env::set_var( 261 - "WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", 262 - "--ignore-gpu-blacklist", 263 - ); 264 - tauri_plugin_deep_link::prepare("uk.phaz.vrcpm"); 275 + tauri_plugin_deep_link::prepare("uk.phaz.vrcpm"); 276 + 277 + // Double check the app has an install directory 278 + let container_folder = dirs::home_dir() 279 + .unwrap() 280 + .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager"); 265 281 266 - // Double check the app has an install directory 267 - let container_folder = dirs::home_dir() 268 - .unwrap() 269 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager"); 270 - match fs::metadata(&container_folder) { 271 - Ok(meta) => { 272 - if meta.is_file() { 273 - panic!("Cannot launch app as the container path is a file not a directory"); 274 - } 275 - } 276 - Err(_) => { 277 - fs::create_dir(&container_folder).unwrap(); 278 - } 282 + match fs::metadata(&container_folder) { 283 + Ok(meta) => { 284 + if meta.is_file() { 285 + panic!("Cannot launch app as the container path is a file not a directory"); 286 + } 279 287 } 288 + Err(_) => { 289 + fs::create_dir(&container_folder).unwrap(); 290 + } 291 + } 280 292 281 - let sync_lock_path = dirs::home_dir() 282 - .unwrap() 283 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.sync_lock"); 284 - match fs::metadata(&sync_lock_path) { 285 - Ok(_) => { 286 - fs::remove_file(&sync_lock_path).unwrap(); 287 - } 288 - Err(_) => {} 293 + let sync_lock_path = dirs::home_dir() 294 + .unwrap() 295 + .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.sync_lock"); 296 + 297 + match fs::metadata(&sync_lock_path) { 298 + Ok(_) => { 299 + fs::remove_file(&sync_lock_path).unwrap(); 289 300 } 301 + Err(_) => {} 302 + } 290 303 291 - println!("Loading App..."); 292 - let photos_path = get_photo_path(); 304 + println!("Loading App..."); 305 + let photos_path = get_photo_path(); 293 306 294 - match fs::metadata(&photos_path) { 295 - Ok(_) => {} 296 - Err(_) => { 297 - fs::create_dir(&photos_path).unwrap(); 298 - } 299 - }; 307 + match fs::metadata(&photos_path) { 308 + Ok(_) => {} 309 + Err(_) => { 310 + fs::create_dir(&photos_path).unwrap(); 311 + } 312 + }; 300 313 301 - let args: Vec<String> = env::args().collect(); 314 + let args: Vec<String> = env::args().collect(); 302 315 303 - let mut update = true; 304 - for arg in args { 305 - if arg == "--no-update" { 306 - update = false; 307 - } 316 + let mut update = true; 317 + for arg in args { 318 + if arg == "--no-update" { 319 + update = false; 308 320 } 321 + } 309 322 310 - if update { 311 - // Auto update 312 - thread::spawn(move || { 313 - let client = reqwest::blocking::Client::new(); 323 + if update { 324 + // Auto update 325 + thread::spawn(move || { 326 + let client = reqwest::blocking::Client::new(); 314 327 315 - let latest_version = client 316 - .get("https://cdn.phaz.uk/vrcpm/latest") 317 - .send() 318 - .unwrap() 319 - .text() 320 - .unwrap(); 328 + let latest_version = client 329 + .get("https://cdn.phaz.uk/vrcpm/latest") 330 + .send() 331 + .unwrap() 332 + .text() 333 + .unwrap(); 321 334 322 - if latest_version != VERSION { 323 - match fs::metadata(&container_folder.join("./updater.exe")) { 324 - Ok(_) => {} 325 - Err(_) => { 326 - let latest_installer = client 327 - .get("https://cdn.phaz.uk/vrcpm/vrcpm-installer.exe") 328 - .timeout(Duration::from_secs(120)) 329 - .send() 330 - .unwrap() 331 - .bytes() 332 - .unwrap(); 335 + if latest_version != VERSION { 336 + match fs::metadata(&container_folder.join("./updater.exe")) { 337 + Ok(_) => {} 338 + Err(_) => { 339 + let latest_installer = client 340 + .get("https://cdn.phaz.uk/vrcpm/vrcpm-installer.exe") 341 + .timeout(Duration::from_secs(120)) 342 + .send() 343 + .unwrap() 344 + .bytes() 345 + .unwrap(); 333 346 334 - fs::write(&container_folder.join("./updater.exe"), latest_installer) 335 - .unwrap(); 336 - } 337 - } 347 + fs::write(&container_folder.join("./updater.exe"), latest_installer) 348 + .unwrap(); 349 + } 350 + } 338 351 339 - let mut cmd = Command::new(&container_folder.join("./updater.exe")); 340 - cmd.current_dir(container_folder); 341 - cmd.spawn().expect("Cannot run updater"); 352 + let mut cmd = Command::new(&container_folder.join("./updater.exe")); 353 + cmd.current_dir(container_folder); 354 + cmd.spawn().expect("Cannot run updater"); 342 355 343 - process::exit(0); 344 - } 345 - }); 346 - } 356 + process::exit(0); 357 + } 358 + }); 359 + } 347 360 348 - // Listen for file updates, store each update in an mpsc channel and send to the frontend 349 - let (sender, receiver) = std::sync::mpsc::channel(); 350 - let mut watcher = notify::recommended_watcher(move | res: Result<notify::Event, notify::Error> | { 361 + // Listen for file updates, store each update in an mpsc channel and send to the frontend 362 + let (sender, receiver) = std::sync::mpsc::channel(); 363 + let mut watcher = notify::recommended_watcher(move | res: Result<notify::Event, notify::Error> | { 351 364 match res { 352 - Ok(event) => { 365 + Ok(event) => { 353 366 match event.kind{ 354 367 EventKind::Remove(_) => { 355 368 let path = event.paths.first().unwrap(); ··· 385 398 } 386 399 }).unwrap(); 387 400 388 - watcher 389 - .watch(&get_photo_path(), RecursiveMode::Recursive) 390 - .unwrap(); 401 + watcher 402 + .watch(&get_photo_path(), RecursiveMode::Recursive) 403 + .unwrap(); 404 + 405 + tauri::Builder::default() 406 + .plugin(tauri_plugin_process::init()) 407 + .plugin(tauri_plugin_http::init()) 408 + .plugin(tauri_plugin_shell::init()) 409 + .register_asynchronous_uri_scheme_protocol("photo", move |_app, request, responder| { 410 + thread::spawn(move || { 411 + // Loads the requested image file, sends data back to the user 412 + let uri = request.uri(); 413 + 414 + if request.method() != "GET" { 415 + responder.respond( 416 + Response::builder() 417 + .status(404) 418 + .header("Access-Control-Allow-Origin", "*") 419 + .body(Vec::new()) 420 + .unwrap(), 421 + ); 422 + 423 + return; 424 + } 425 + 426 + let path = uri.path().split_at(1).1; 427 + let file = fs::File::open(path); 428 + 429 + match file { 430 + Ok(mut file) => { 431 + match uri.query().unwrap(){ 432 + "downscale" => { 433 + let decoder = PngDecoder::new(BufReader::new(&file)).unwrap(); 434 + let src_image = DynamicImage::from_decoder(decoder).unwrap(); 435 + 436 + let size_multiplier: f32 = 200.0 / src_image.height() as f32; 437 + 438 + let dst_width = (src_image.width() as f32 * size_multiplier).floor() as u32; 439 + let dst_height: u32 = 200; 440 + 441 + let mut dst_image = Image::new(dst_width, dst_height, src_image.pixel_type().unwrap()); 442 + let mut resizer = Resizer::new(); 391 443 392 - tauri::Builder::default() 393 - .plugin(tauri_plugin_process::init()) 394 - .plugin(tauri_plugin_http::init()) 395 - .plugin(tauri_plugin_shell::init()) 396 - .register_asynchronous_uri_scheme_protocol("photo", move |_app, request, responder| { 397 - thread::spawn(move || { 398 - // Loads the requested image file, sends data back to the user 399 - let uri = request.uri(); 444 + let opts = ResizeOptions::new() 445 + .resize_alg(fast_image_resize::ResizeAlg::Nearest); 400 446 401 - if request.method() != "GET" { 402 - responder.respond(Response::builder() 403 - .status(404) 404 - .header("Access-Control-Allow-Origin", "*") 405 - .body(Vec::new()) 406 - .unwrap()); 447 + resizer.resize(&src_image, &mut dst_image, Some(&opts)).unwrap(); 407 448 408 - return; 409 - } 449 + let mut buf = Vec::new(); 450 + let encoder = PngEncoder::new(&mut buf); 410 451 411 - let path = uri.path().split_at(1).1; 412 - let file = fs::File::open(path); 452 + encoder.write_image(dst_image.buffer(), dst_width, dst_height, src_image.color().into()).unwrap(); 413 453 414 - match file { 415 - Ok(mut file) => { 416 - let mut buffer = Vec::new(); 454 + let res = Response::builder() 455 + .status(200) 456 + .header("Access-Control-Allow-Origin", "*") 457 + .body(buf) 458 + .unwrap(); 417 459 418 - let _out = file.read_to_end(&mut buffer); 460 + responder.respond(res); 461 + }, 462 + _ => { 463 + let mut buf = Vec::new(); 464 + file.read_to_end(&mut buf).unwrap(); 419 465 420 - let res = Response::builder() 421 - .status(200) 422 - .header("Access-Control-Allow-Origin", "*") 423 - .body(buffer) 424 - .unwrap(); 466 + let res = Response::builder() 467 + .status(200) 468 + .header("Access-Control-Allow-Origin", "*") 469 + .body(buf) 470 + .unwrap(); 425 471 426 - responder.respond(res); 427 - } 428 - Err(_) => { 429 - responder.respond(Response::builder() 430 - .status(404) 431 - .header("Access-Control-Allow-Origin", "*") 432 - .body(b"File Not Found") 433 - .unwrap()); 434 - } 472 + responder.respond(res); 435 473 } 436 - }); 437 - }) 438 - .on_window_event(| window, event | match event { 439 - WindowEvent::CloseRequested { api, .. } => { 440 - window.hide().unwrap(); 441 - api.prevent_close(); 474 + } 442 475 } 443 - _ => {} 444 - }) 445 - .setup(|app| { 446 - let handle = app.handle().clone(); 476 + Err(_) => { 477 + responder.respond( 478 + Response::builder() 479 + .status(404) 480 + .header("Access-Control-Allow-Origin", "*") 481 + .body(b"File Not Found") 482 + .unwrap(), 483 + ); 484 + } 485 + } 486 + }); 487 + }) 488 + .on_window_event(|window, event| match event { 489 + WindowEvent::CloseRequested { api, .. } => { 490 + window.hide().unwrap(); 491 + api.prevent_close(); 492 + } 493 + _ => {} 494 + }) 495 + .setup(|app| { 496 + let handle = app.handle().clone(); 447 497 448 - // Setup the tray icon and menu buttons 449 - let quit = MenuItemBuilder::new("Quit").id("quit").build(&handle).unwrap(); 450 - let hide = MenuItemBuilder::new("Hide / Show").id("hide").build(&handle).unwrap(); 498 + // Setup the tray icon and menu buttons 499 + let quit = MenuItemBuilder::new("Quit") 500 + .id("quit") 501 + .build(&handle) 502 + .unwrap(); 451 503 452 - let tray_menu = MenuBuilder::new(&handle) 453 - .items(&[ &quit, &hide ]) 454 - .build().unwrap(); 504 + let hide = MenuItemBuilder::new("Hide / Show") 505 + .id("hide") 506 + .build(&handle) 507 + .unwrap(); 455 508 456 - TrayIconBuilder::with_id("vrcpm-tray") 457 - .menu(&tray_menu) 458 - .on_menu_event(move | app: &AppHandle, event |{ 459 - match event.id().as_ref() { 460 - "quit" => { 461 - std::process::exit(0); 462 - } 463 - "hide" => { 464 - let window = app.get_webview_window("main").unwrap(); 509 + let tray_menu = MenuBuilder::new(&handle) 510 + .items(&[&quit, &hide]) 511 + .build() 512 + .unwrap(); 465 513 466 - if window.is_visible().unwrap() { 467 - window.hide().unwrap(); 468 - } else { 469 - window.show().unwrap(); 470 - window.set_focus().unwrap(); 471 - } 472 - } 473 - _ => {} 474 - } 475 - }) 476 - .on_tray_icon_event(| tray, event |{ 477 - if let TrayIconEvent::Click { 478 - button: MouseButton::Left, 479 - button_state: MouseButtonState::Up, 480 - .. 481 - } = event{ 482 - let window = tray.app_handle().get_webview_window("main").unwrap(); 514 + TrayIconBuilder::with_id("main") 515 + .icon(tauri::image::Image::from_bytes(include_bytes!("../icons/32x32.png")).unwrap()) 516 + .menu(&tray_menu) 517 + .on_menu_event(move |app: &AppHandle, event| match event.id().as_ref() { 518 + "quit" => { 519 + std::process::exit(0); 520 + } 521 + "hide" => { 522 + let window = app.get_webview_window("main").unwrap(); 483 523 484 - window.show().unwrap(); 485 - window.set_focus().unwrap(); 486 - } 487 - }) 488 - .build(&handle).unwrap(); 524 + if window.is_visible().unwrap() { 525 + window.hide().unwrap(); 526 + } else { 527 + window.show().unwrap(); 528 + window.set_focus().unwrap(); 529 + } 530 + } 531 + _ => {} 532 + }) 533 + .on_tray_icon_event(|tray, event| { 534 + if let TrayIconEvent::Click { 535 + button: MouseButton::Left, 536 + button_state: MouseButtonState::Up, 537 + .. 538 + } = event 539 + { 540 + let window = tray.app_handle().get_webview_window("main").unwrap(); 489 541 490 - // Register "deep link" for authentication via vrcpm:// 491 - tauri_plugin_deep_link::register("vrcpm", move |request| { 492 - let mut command: u8 = 0; 493 - let mut index: u8 = 0; 542 + window.show().unwrap(); 543 + window.set_focus().unwrap(); 544 + } 545 + }) 546 + .build(&handle) 547 + .unwrap(); 548 + // Register "deep link" for authentication via vrcpm:// 549 + tauri_plugin_deep_link::register("vrcpm", move |request| { 550 + let mut command: u8 = 0; 551 + let mut index: u8 = 0; 494 552 495 - for part in request.split('/').into_iter() { 496 - index += 1; 553 + for part in request.split('/').into_iter() { 554 + index += 1; 497 555 498 - if index == 3 && part == "auth-callback" { 499 - command = 1; 500 - } 556 + if index == 3 && part == "auth-callback" { 557 + command = 1; 558 + } 501 559 502 - if index == 3 && part == "auth-denied" { 503 - handle.emit("auth-denied", "null").unwrap(); 504 - } 560 + if index == 3 && part == "auth-denied" { 561 + handle.emit("auth-denied", "null").unwrap(); 562 + } 505 563 506 - if index == 4 && command == 1 { 507 - handle.emit("auth-callback", part).unwrap(); 508 - } 509 - } 510 - }) 511 - .unwrap(); 564 + if index == 4 && command == 1 { 565 + handle.emit("auth-callback", part).unwrap(); 566 + } 567 + } 568 + }) 569 + .unwrap(); 512 570 513 - // I hate this approach but i have no clue how else to do this... 514 - // reads the mpsc channel and sends the events to the frontend 515 - let window = app.get_webview_window("main").unwrap(); 516 - thread::spawn(move || { 517 - thread::sleep(time::Duration::from_millis(100)); 571 + // I hate this approach but i have no clue how else to do this... 572 + // reads the mpsc channel and sends the events to the frontend 573 + let window = app.get_webview_window("main").unwrap(); 574 + thread::spawn(move || { 575 + thread::sleep(time::Duration::from_millis(100)); 518 576 519 - for event in receiver { 520 - match event.0 { 521 - 1 => { 522 - window.emit("photo_create", event.1).unwrap(); 523 - } 524 - 2 => { 525 - window.emit("photo_remove", event.1).unwrap(); 526 - } 527 - _ => {} 528 - } 529 - } 530 - }); 577 + for event in receiver { 578 + match event.0 { 579 + 1 => { 580 + window.emit("photo_create", event.1).unwrap(); 581 + } 582 + 2 => { 583 + window.emit("photo_remove", event.1).unwrap(); 584 + } 585 + _ => {} 586 + } 587 + } 588 + }); 531 589 532 - Ok(()) 533 - }) 534 - .invoke_handler(tauri::generate_handler![ 535 - start_user_auth, 536 - load_photos, 537 - close_splashscreen, 538 - load_photo_meta, 539 - delete_photo, 540 - open_url, 541 - find_world_by_id, 542 - start_with_win, 543 - get_user_photos_path, 544 - change_final_path, 545 - sync_photos, 546 - get_version, 547 - relaunch 548 - ]) 549 - .run(tauri::generate_context!()) 550 - .expect("error while running tauri application"); 590 + Ok(()) 591 + }) 592 + .invoke_handler(tauri::generate_handler![ 593 + start_user_auth, 594 + load_photos, 595 + close_splashscreen, 596 + load_photo_meta, 597 + delete_photo, 598 + open_url, 599 + open_folder, 600 + find_world_by_id, 601 + start_with_win, 602 + get_user_photos_path, 603 + change_final_path, 604 + sync_photos, 605 + get_version, 606 + relaunch 607 + ]) 608 + .run(tauri::generate_context!()) 609 + .expect("error while running tauri application"); 551 610 }
+204 -200
src-tauri/src/photosync.rs
··· 7 7 8 8 #[derive(Clone, Serialize)] 9 9 struct PhotoUploadMeta { 10 - photos_uploading: usize, 11 - photos_total: usize, 10 + photos_uploading: usize, 11 + photos_total: usize, 12 12 } 13 13 14 14 pub fn sync_photos(token: String, path: path::PathBuf, window: tauri::Window) { 15 - let sync_lock_path = dirs::home_dir() 16 - .unwrap() 17 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.sync_lock"); 18 - match fs::metadata(&sync_lock_path) { 19 - Ok(_) => { 20 - return; 21 - } 22 - Err(_) => {} 15 + let sync_lock_path = dirs::home_dir() 16 + .unwrap() 17 + .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.sync_lock"); 18 + 19 + match fs::metadata(&sync_lock_path) { 20 + Ok(_) => { 21 + return; 23 22 } 23 + Err(_) => {} 24 + } 24 25 25 - fs::write(&sync_lock_path, "Currently Syncing").unwrap(); 26 + fs::write(&sync_lock_path, "Currently Syncing").unwrap(); 26 27 27 - match fs::metadata(&path) { 28 - Ok(_) => {} 29 - Err(_) => { 30 - fs::create_dir(&path).unwrap(); 31 - } 32 - }; 28 + match fs::metadata(&path) { 29 + Ok(_) => {} 30 + Err(_) => { 31 + fs::create_dir(&path).unwrap(); 32 + } 33 + }; 33 34 34 - let mut photos: Vec<String> = Vec::new(); 35 + let mut photos: Vec<String> = Vec::new(); 35 36 36 - for folder in fs::read_dir(&path).unwrap() { 37 - let f = folder.unwrap(); 37 + for folder in fs::read_dir(&path).unwrap() { 38 + let f = folder.unwrap(); 38 39 39 - if f.metadata().unwrap().is_dir() { 40 - match fs::read_dir(f.path()) { 41 - Ok(dir) => { 42 - for photo in dir { 43 - let p = photo.unwrap(); 40 + if f.metadata().unwrap().is_dir() { 41 + match fs::read_dir(f.path()) { 42 + Ok(dir) => { 43 + for photo in dir { 44 + let p = photo.unwrap(); 44 45 45 - let re1 = Regex::new(r"(?m)VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}.png").unwrap(); 46 - let re2 = Regex::new( 47 - r"(?m)VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}_wrld_[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}.png").unwrap(); 46 + let re1 = Regex::new(r"(?m)VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}.png").unwrap(); 47 + let re2 = Regex::new( 48 + r"(?m)VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}_wrld_[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}.png").unwrap(); 48 49 49 - if re1.is_match(p.file_name().to_str().unwrap()) 50 - || re2.is_match(p.file_name().to_str().unwrap()) 51 - { 52 - photos.push(p.file_name().into_string().unwrap()); 53 - } 54 - } 55 - } 56 - Err(_) => {} 50 + if re1.is_match(p.file_name().to_str().unwrap()) 51 + || re2.is_match(p.file_name().to_str().unwrap()) 52 + { 53 + photos.push(p.file_name().into_string().unwrap()); 57 54 } 55 + } 58 56 } 57 + Err(_) => {} 58 + } 59 59 } 60 + } 60 61 61 - let body = reqwest::blocking::get(format!( 62 - "https://photos-cdn.phazed.xyz/api/v1/photos/exists?token={}", 63 - &token 64 - )) 65 - .unwrap() 66 - .text() 67 - .unwrap(); 62 + let body = reqwest::blocking::get(format!( 63 + "https://photos-cdn.phazed.xyz/api/v1/photos/exists?token={}", 64 + &token 65 + )) 66 + .unwrap() 67 + .text() 68 + .unwrap(); 68 69 69 - let body: Value = serde_json::from_str(&body).unwrap(); 70 + let body: Value = serde_json::from_str(&body).unwrap(); 70 71 71 - let mut photos_to_upload: Vec<String> = Vec::new(); 72 - let uploaded_photos = body["files"].as_array().unwrap(); 72 + let mut photos_to_upload: Vec<String> = Vec::new(); 73 + let uploaded_photos = body["files"].as_array().unwrap(); 73 74 74 - let photos_len = photos.len(); 75 + let photos_len = photos.len(); 75 76 76 - for photo in &photos { 77 - let mut found_photo = false; 77 + for photo in &photos { 78 + let mut found_photo = false; 78 79 79 - for uploaded_photo in uploaded_photos { 80 - if photo == uploaded_photo.as_str().unwrap() { 81 - found_photo = true; 82 - break; 83 - } 84 - } 80 + for uploaded_photo in uploaded_photos { 81 + if photo == uploaded_photo.as_str().unwrap() { 82 + found_photo = true; 83 + break; 84 + } 85 + } 85 86 86 - if !found_photo { 87 - photos_to_upload.push(photo.clone()); 88 - } 87 + if !found_photo { 88 + photos_to_upload.push(photo.clone()); 89 89 } 90 + } 90 91 91 - window 92 - .emit( 93 - "photos-upload-meta", 94 - PhotoUploadMeta { 95 - photos_uploading: photos_to_upload.len(), 96 - photos_total: photos_len, 97 - }, 98 - ) 99 - .unwrap(); 100 - let mut photos_left = photos_to_upload.len(); 92 + window 93 + .emit( 94 + "photos-upload-meta", 95 + PhotoUploadMeta { 96 + photos_uploading: photos_to_upload.len(), 97 + photos_total: photos_len, 98 + }, 99 + ) 100 + .unwrap(); 101 101 102 - let client = reqwest::blocking::Client::new(); 102 + let mut photos_left = photos_to_upload.len(); 103 103 104 - loop { 105 - match photos_to_upload.pop() { 106 - Some(photo) => { 107 - let folder_name = photo.clone().replace("VRChat_", ""); 108 - let mut folder_name = folder_name.split("-"); 109 - let folder_name = format!( 110 - "{}-{}", 111 - folder_name.nth(0).unwrap(), 112 - folder_name.nth(0).unwrap() 113 - ); 104 + let client = reqwest::blocking::Client::new(); 114 105 115 - let full_path = format!("{}\\{}\\{}", path.to_str().unwrap(), folder_name, photo); 116 - let file = fs::File::open(full_path); 106 + loop { 107 + match photos_to_upload.pop() { 108 + Some(photo) => { 109 + let folder_name = photo.clone().replace("VRChat_", ""); 110 + let mut folder_name = folder_name.split("-"); 111 + let folder_name = format!( 112 + "{}-{}", 113 + folder_name.nth(0).unwrap(), 114 + folder_name.nth(0).unwrap() 115 + ); 117 116 118 - match file { 119 - Ok(file) => { 120 - let res = client 121 - .put(format!( 122 - "https://photos-cdn.phazed.xyz/api/v1/photos?token={}", 123 - &token 124 - )) 125 - .header("Content-Type", "image/png") 126 - .header("filename", photo) 127 - .body(file) 128 - .timeout(Duration::from_secs(120)) 129 - .send() 130 - .unwrap() 131 - .text() 132 - .unwrap(); 117 + let full_path = format!("{}\\{}\\{}", path.to_str().unwrap(), folder_name, photo); 118 + let file = fs::File::open(full_path); 133 119 134 - let res: Result<Value, Error> = serde_json::from_str(&res); 120 + match file { 121 + Ok(file) => { 122 + let res = client 123 + .put(format!( 124 + "https://photos-cdn.phazed.xyz/api/v1/photos?token={}", 125 + &token 126 + )) 127 + .header("Content-Type", "image/png") 128 + .header("filename", photo) 129 + .body(file) 130 + .timeout(Duration::from_secs(120)) 131 + .send() 132 + .unwrap() 133 + .text() 134 + .unwrap(); 135 135 136 - match res { 137 - Ok(res) => { 138 - if !res["ok"].as_bool().unwrap() { 139 - println!( 140 - "Failed to upload: {}", 141 - res["error"].as_str().unwrap() 142 - ); 143 - window 144 - .emit("sync-failed", res["error"].as_str().unwrap()) 145 - .unwrap(); 146 - break; 147 - } 148 - } 149 - Err(err) => { 150 - dbg!(err); 151 - } 152 - } 136 + let res: Result<Value, Error> = serde_json::from_str(&res); 137 + 138 + match res { 139 + Ok(res) => { 140 + if !res["ok"].as_bool().unwrap() { 141 + println!( 142 + "Failed to upload: {}", 143 + res["error"].as_str().unwrap() 144 + ); 145 + 146 + window 147 + .emit("sync-failed", res["error"].as_str().unwrap()) 148 + .unwrap(); 149 + 150 + break; 153 151 } 154 - Err(_) => {} 152 + } 153 + Err(err) => { 154 + dbg!(err); 155 + } 155 156 } 156 - 157 - photos_left -= 1; 158 - window 159 - .emit( 160 - "photos-upload-meta", 161 - PhotoUploadMeta { 162 - photos_uploading: photos_left, 163 - photos_total: photos_len, 164 - }, 165 - ) 166 - .unwrap(); 167 157 } 168 - None => { 169 - break; 170 - } 158 + Err(_) => {} 171 159 } 160 + 161 + photos_left -= 1; 162 + window 163 + .emit( 164 + "photos-upload-meta", 165 + PhotoUploadMeta { 166 + photos_uploading: photos_left, 167 + photos_total: photos_len, 168 + }, 169 + ) 170 + .unwrap(); 171 + } 172 + None => { 173 + break; 174 + } 172 175 } 176 + } 173 177 174 - println!("Finished Uploading."); 175 - let mut photos_to_download: Vec<String> = Vec::new(); 178 + println!("Finished Uploading."); 179 + let mut photos_to_download: Vec<String> = Vec::new(); 176 180 177 - for photo in uploaded_photos { 178 - let mut found_photo = false; 179 - let photo = photo.as_str().unwrap().to_string(); 180 - 181 - for uploaded_photo in &photos { 182 - if &photo == uploaded_photo { 183 - found_photo = true; 184 - break; 185 - } 186 - } 181 + for photo in uploaded_photos { 182 + let mut found_photo = false; 183 + let photo = photo.as_str().unwrap().to_string(); 187 184 188 - if !found_photo { 189 - photos_to_download.push(photo); 190 - } 185 + for uploaded_photo in &photos { 186 + if &photo == uploaded_photo { 187 + found_photo = true; 188 + break; 189 + } 191 190 } 192 191 193 - photos_to_download.reverse(); 192 + if !found_photo { 193 + photos_to_download.push(photo); 194 + } 195 + } 194 196 195 - let photos_len = photos_to_download.len(); 196 - let mut photos_left = photos_to_download.len(); 197 - 198 - loop { 199 - match photos_to_download.pop() { 200 - Some(photo) => { 201 - let folder_name = photo.clone().replace("VRChat_", ""); 202 - let mut folder_name = folder_name.split("-"); 203 - let folder_name = format!( 204 - "{}-{}", 205 - folder_name.nth(0).unwrap(), 206 - folder_name.nth(0).unwrap() 207 - ); 197 + photos_to_download.reverse(); 208 198 209 - let full_path = format!("{}\\{}\\{}", path.to_str().unwrap(), folder_name, photo); 199 + let photos_len = photos_to_download.len(); 200 + let mut photos_left = photos_to_download.len(); 210 201 211 - let res = client 212 - .get(format!( 213 - "https://photos-cdn.phazed.xyz/api/v1/photos?token={}&photo={}", 214 - &token, &photo 215 - )) 216 - .timeout(Duration::from_secs(120)) 217 - .send() 218 - .unwrap() 219 - .bytes(); 202 + loop { 203 + match photos_to_download.pop() { 204 + Some(photo) => { 205 + let folder_name = photo.clone().replace("VRChat_", ""); 206 + let mut folder_name = folder_name.split("-"); 207 + let folder_name = format!( 208 + "{}-{}", 209 + folder_name.nth(0).unwrap(), 210 + folder_name.nth(0).unwrap() 211 + ); 220 212 221 - match res { 222 - Ok(res) => { 223 - let folder_path = format!("{}\\{}", path.to_str().unwrap(), folder_name); 224 - match fs::metadata(&folder_path) { 225 - Ok(_) => {} 226 - Err(_) => { 227 - fs::create_dir(folder_path).unwrap(); 228 - } 229 - } 213 + let full_path = format!("{}\\{}\\{}", path.to_str().unwrap(), folder_name, photo); 230 214 231 - let mut file = fs::File::create(full_path).unwrap(); 232 - file.write_all(&res).unwrap(); 233 - } 234 - Err(err) => { 235 - dbg!(err); 236 - } 237 - } 215 + let res = client 216 + .get(format!( 217 + "https://photos-cdn.phazed.xyz/api/v1/photos?token={}&photo={}", 218 + &token, &photo 219 + )) 220 + .timeout(Duration::from_secs(120)) 221 + .send() 222 + .unwrap() 223 + .bytes(); 238 224 239 - photos_left -= 1; 240 - window 241 - .emit( 242 - "photos-download-meta", 243 - PhotoUploadMeta { 244 - photos_uploading: photos_left, 245 - photos_total: photos_len, 246 - }, 247 - ) 248 - .unwrap(); 225 + match res { 226 + Ok(res) => { 227 + let folder_path = format!("{}\\{}", path.to_str().unwrap(), folder_name); 228 + match fs::metadata(&folder_path) { 229 + Ok(_) => {} 230 + Err(_) => { 231 + fs::create_dir(folder_path).unwrap(); 232 + } 249 233 } 250 - None => { 251 - break; 252 - } 234 + 235 + let mut file = fs::File::create(full_path).unwrap(); 236 + file.write_all(&res).unwrap(); 237 + } 238 + Err(err) => { 239 + dbg!(err); 240 + } 253 241 } 242 + 243 + photos_left -= 1; 244 + window 245 + .emit( 246 + "photos-download-meta", 247 + PhotoUploadMeta { 248 + photos_uploading: photos_left, 249 + photos_total: photos_len, 250 + }, 251 + ) 252 + .unwrap(); 253 + } 254 + None => { 255 + break; 256 + } 254 257 } 258 + } 255 259 256 - println!("Finished Downloading."); 260 + println!("Finished Downloading."); 257 261 258 - fs::remove_file(&sync_lock_path).unwrap(); 259 - window.emit("sync-finished", "h").unwrap(); 262 + fs::remove_file(&sync_lock_path).unwrap(); 263 + window.emit("sync-finished", "h").unwrap(); 260 264 }
+85 -84
src-tauri/src/pngmeta.rs
··· 3 3 4 4 #[derive(Clone)] 5 5 pub struct PNGImage { 6 - width: u32, 7 - height: u32, 8 - bit_depth: u8, 9 - colour_type: u8, 10 - compression_method: u8, 11 - filter_method: u8, 12 - interlace_method: u8, 13 - metadata: String, 14 - path: String, 6 + width: u32, 7 + height: u32, 8 + bit_depth: u8, 9 + colour_type: u8, 10 + compression_method: u8, 11 + filter_method: u8, 12 + interlace_method: u8, 13 + metadata: String, 14 + path: String, 15 15 } 16 16 17 17 impl PNGImage { 18 - pub fn new(buff: Vec<u8>, path: String) -> PNGImage { 19 - let mut img = PNGImage { 20 - width: 0, 21 - height: 0, 22 - bit_depth: 0, 23 - colour_type: 0, 24 - compression_method: 0, 25 - filter_method: 0, 26 - interlace_method: 0, 27 - metadata: "".to_string(), 28 - path: path, 29 - }; 18 + pub fn new(buff: Vec<u8>, path: String) -> PNGImage { 19 + let mut img = PNGImage { 20 + width: 0, 21 + height: 0, 22 + bit_depth: 0, 23 + colour_type: 0, 24 + compression_method: 0, 25 + filter_method: 0, 26 + interlace_method: 0, 27 + metadata: "".to_string(), 28 + path: path, 29 + }; 30 + 31 + if buff[0] != 0x89 32 + || buff[1] != 0x50 33 + || buff[2] != 0x4E 34 + || buff[3] != 0x47 35 + || buff[4] != 0x0D 36 + || buff[5] != 0x0A 37 + || buff[6] != 0x1A 38 + || buff[7] != 0x0A 39 + { 40 + panic!("Image is not a PNG file"); 41 + } 30 42 31 - if buff[0] != 0x89 32 - || buff[1] != 0x50 33 - || buff[2] != 0x4E 34 - || buff[3] != 0x47 35 - || buff[4] != 0x0D 36 - || buff[5] != 0x0A 37 - || buff[6] != 0x1A 38 - || buff[7] != 0x0A 39 - { 40 - panic!("Image is not a PNG file"); 41 - } 43 + img.read_png_chunk(8, buff); 44 + img 45 + } 42 46 43 - img.read_png_chunk(8, buff); 47 + fn read_png_chunk(&mut self, start_byte: usize, buff: Vec<u8>) { 48 + let data_buff = buff[start_byte..].to_vec(); 44 49 45 - img 46 - } 50 + let length = u32::from_le_bytes([data_buff[3], data_buff[2], data_buff[1], data_buff[0]]); 51 + let chunk_type = str::from_utf8(&data_buff[4..8]).unwrap(); 47 52 48 - fn read_png_chunk(&mut self, start_byte: usize, buff: Vec<u8>) { 49 - let data_buff = buff[start_byte..].to_vec(); 53 + match chunk_type { 54 + "IHDR" => { 55 + self.width = 56 + u32::from_le_bytes([data_buff[11], data_buff[10], data_buff[9], data_buff[8]]); 50 57 51 - let length = u32::from_le_bytes([data_buff[3], data_buff[2], data_buff[1], data_buff[0]]); 52 - let chunk_type = str::from_utf8(&data_buff[4..8]).unwrap(); 58 + self.height = u32::from_le_bytes([ 59 + data_buff[15], 60 + data_buff[14], 61 + data_buff[13], 62 + data_buff[12], 63 + ]); 53 64 54 - match chunk_type { 55 - "IHDR" => { 56 - self.width = 57 - u32::from_le_bytes([data_buff[11], data_buff[10], data_buff[9], data_buff[8]]); 58 - self.height = u32::from_le_bytes([ 59 - data_buff[15], 60 - data_buff[14], 61 - data_buff[13], 62 - data_buff[12], 63 - ]); 64 - self.bit_depth = data_buff[16]; 65 - self.colour_type = data_buff[17]; 66 - self.compression_method = data_buff[18]; 67 - self.filter_method = data_buff[19]; 68 - self.interlace_method = data_buff[20]; 65 + self.bit_depth = data_buff[16]; 66 + self.colour_type = data_buff[17]; 67 + self.compression_method = data_buff[18]; 68 + self.filter_method = data_buff[19]; 69 + self.interlace_method = data_buff[20]; 69 70 70 - self.read_png_chunk((length + 12) as usize, data_buff); 71 - } 72 - "iTXt" => { 73 - let end_byte = (8 + length) as usize; 74 - let d = str::from_utf8(&data_buff[8..end_byte]).unwrap(); 71 + self.read_png_chunk((length + 12) as usize, data_buff); 72 + } 73 + "iTXt" => { 74 + let end_byte = (8 + length) as usize; 75 + let d = str::from_utf8(&data_buff[8..end_byte]).unwrap(); 75 76 76 - self.metadata = d.to_string(); 77 + self.metadata = d.to_string(); 77 78 78 - self.read_png_chunk((length + 12) as usize, data_buff); 79 - } 80 - "IEND" => {} 81 - "IDAT" => {} 82 - _ => { 83 - self.read_png_chunk((length + 12) as usize, data_buff); 84 - } 85 - } 79 + self.read_png_chunk((length + 12) as usize, data_buff); 80 + } 81 + "IEND" => {} 82 + "IDAT" => {} 83 + _ => { 84 + self.read_png_chunk((length + 12) as usize, data_buff); 85 + } 86 86 } 87 + } 87 88 } 88 89 89 90 impl Serialize for PNGImage { 90 - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 91 - where 92 - S: Serializer, 93 - { 94 - let mut s = serializer.serialize_struct("PNGImage", 7)?; 95 - s.serialize_field("width", &self.width)?; 96 - s.serialize_field("height", &self.height)?; 97 - s.serialize_field("bit_depth", &self.bit_depth)?; 98 - s.serialize_field("colour_type", &self.colour_type)?; 99 - s.serialize_field("compression_method", &self.compression_method)?; 100 - s.serialize_field("filter_method", &self.filter_method)?; 101 - s.serialize_field("interlace_method", &self.interlace_method)?; 102 - s.serialize_field("metadata", &self.metadata)?; 103 - s.serialize_field("path", &self.path)?; 104 - s.end() 105 - } 91 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 92 + where 93 + S: Serializer, 94 + { 95 + let mut s = serializer.serialize_struct("PNGImage", 7)?; 96 + s.serialize_field("width", &self.width)?; 97 + s.serialize_field("height", &self.height)?; 98 + s.serialize_field("bit_depth", &self.bit_depth)?; 99 + s.serialize_field("colour_type", &self.colour_type)?; 100 + s.serialize_field("compression_method", &self.compression_method)?; 101 + s.serialize_field("filter_method", &self.filter_method)?; 102 + s.serialize_field("interlace_method", &self.interlace_method)?; 103 + s.serialize_field("metadata", &self.metadata)?; 104 + s.serialize_field("path", &self.path)?; 105 + s.end() 106 + } 106 107 }
+97 -96
src-tauri/src/worldscraper.rs
··· 3 3 4 4 #[derive(Clone)] 5 5 pub struct World { 6 - id: String, 7 - name: String, 8 - author: String, 9 - author_id: String, 10 - desc: String, 11 - img: String, 12 - max_users: u64, 13 - visits: u64, 14 - favourites: u64, 15 - tags: String, 16 - from: String, 17 - from_site: String, 18 - found: bool, 6 + id: String, 7 + name: String, 8 + author: String, 9 + author_id: String, 10 + desc: String, 11 + img: String, 12 + max_users: u64, 13 + visits: u64, 14 + favourites: u64, 15 + tags: String, 16 + from: String, 17 + from_site: String, 18 + found: bool, 19 19 } 20 20 21 21 impl World { 22 - pub fn new(world_id: String) -> World { 23 - println!("Fetching world data for {}", &world_id); 22 + pub fn new(world_id: String) -> World { 23 + println!("Fetching world data for {}", &world_id); 24 24 25 - let mut world = World { 26 - id: world_id.clone(), 27 - name: "".into(), 28 - author: "".into(), 29 - author_id: "".into(), 30 - desc: "".into(), 31 - img: "".into(), 32 - max_users: 0, 33 - visits: 0, 34 - favourites: 0, 35 - tags: "".into(), 36 - from: "https://vrclist.com/worlds/".into(), 37 - from_site: "vrclist.com".into(), 38 - found: false, 39 - }; 25 + let mut world = World { 26 + id: world_id.clone(), 27 + name: "".into(), 28 + author: "".into(), 29 + author_id: "".into(), 30 + desc: "".into(), 31 + img: "".into(), 32 + max_users: 0, 33 + visits: 0, 34 + favourites: 0, 35 + tags: "".into(), 36 + from: "https://vrclist.com/worlds/".into(), 37 + from_site: "vrclist.com".into(), 38 + found: false, 39 + }; 40 40 41 - let client = reqwest::blocking::Client::new(); 41 + let client = reqwest::blocking::Client::new(); 42 42 43 - let world_id_str = world_id.to_owned(); 44 - let fixed_id_req = client 45 - .post("https://api.vrclist.com/worlds/id-convert") 46 - .header("Content-Type", "application/json") 47 - .header("User-Agent", "VRChat-Photo-Manager-Rust/0.0.1") 48 - .body(json!({ "world_id": world_id_str }).to_string()) 49 - .send() 50 - .unwrap() 51 - .text() 52 - .unwrap(); 43 + let world_id_str = world_id.to_owned(); 44 + let fixed_id_req = client 45 + .post("https://api.vrclist.com/worlds/id-convert") 46 + .header("Content-Type", "application/json") 47 + .header("User-Agent", "VRChat-Photo-Manager-Rust/0.0.1") 48 + .body(json!({ "world_id": world_id_str }).to_string()) 49 + .send() 50 + .unwrap() 51 + .text() 52 + .unwrap(); 53 53 54 - if &fixed_id_req == "" { 55 - return world; 56 - } 54 + if &fixed_id_req == "" { 55 + println!("World {} not found", world_id); 56 + return world; 57 + } 57 58 58 - world.found = true; 59 + world.found = true; 59 60 60 - let fixed_id: serde_json::Value = serde_json::from_str(&fixed_id_req).unwrap(); 61 - world.from = format!("https://vrclist.com/worlds/{}", fixed_id["id"].to_string()); 61 + let fixed_id: serde_json::Value = serde_json::from_str(&fixed_id_req).unwrap(); 62 + world.from = format!("https://vrclist.com/worlds/{}", fixed_id["id"].to_string()); 62 63 63 - let world_data = client 64 - .post("https://api.vrclist.com/worlds/single") 65 - .header("Content-Type", "application/json") 66 - .header("User-Agent", "VRChat-Photo-Manager-Rust/0.0.1") 67 - .body(json!({ "id": fixed_id["id"].to_string() }).to_string()) 68 - .send() 69 - .unwrap() 70 - .text() 71 - .unwrap(); 72 - 73 - let world_data: serde_json::Value = serde_json::from_str(&world_data).unwrap(); 64 + let world_data = client 65 + .post("https://api.vrclist.com/worlds/single") 66 + .header("Content-Type", "application/json") 67 + .header("User-Agent", "VRChat-Photo-Manager-Rust/0.0.1") 68 + .body(json!({ "id": fixed_id["id"].to_string() }).to_string()) 69 + .send() 70 + .unwrap() 71 + .text() 72 + .unwrap(); 74 73 75 - world.name = world_data["name"].to_string(); 76 - world.author = world_data["authorName"].to_string(); 77 - world.author_id = world_data["authorId"].to_string(); 78 - world.desc = world_data["description"].to_string(); 79 - world.img = world_data["imageUrl"].to_string(); 80 - world.tags = world_data["tags"].to_string(); 74 + let world_data: serde_json::Value = serde_json::from_str(&world_data).unwrap(); 81 75 82 - match world_data["vrchat_visits"].as_u64() { 83 - Some(visits) => world.visits = visits, 84 - None => {} 85 - } 76 + world.name = world_data["name"].to_string(); 77 + world.author = world_data["authorName"].to_string(); 78 + world.author_id = world_data["authorId"].to_string(); 79 + world.desc = world_data["description"].to_string(); 80 + world.img = world_data["imageUrl"].to_string(); 81 + world.tags = world_data["tags"].to_string(); 86 82 87 - match world_data["capacity"].as_u64() { 88 - Some(cap) => { 89 - world.max_users = cap; 90 - } 91 - None => {} 92 - } 83 + match world_data["vrchat_visits"].as_u64() { 84 + Some(visits) => world.visits = visits, 85 + None => {} 86 + } 93 87 94 - println!("Fetched world data for {}", &world_id); 95 - world 88 + match world_data["capacity"].as_u64() { 89 + Some(cap) => { 90 + world.max_users = cap; 91 + } 92 + None => {} 96 93 } 94 + 95 + println!("Fetched world data for {}", &world_id); 96 + world 97 + } 97 98 } 98 99 99 100 impl Serialize for World { 100 - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 101 - where 102 - S: Serializer, 103 - { 104 - let mut s = serializer.serialize_struct("World", 7)?; 105 - s.serialize_field("id", &self.id)?; 106 - s.serialize_field("name", &self.name)?; 107 - s.serialize_field("author", &self.author)?; 108 - s.serialize_field("authorId", &self.author_id)?; 109 - s.serialize_field("desc", &self.desc)?; 110 - s.serialize_field("img", &self.img)?; 111 - s.serialize_field("maxUsers", &self.max_users)?; 112 - s.serialize_field("visits", &self.visits)?; 113 - s.serialize_field("favourites", &self.favourites)?; 114 - s.serialize_field("tags", &self.tags)?; 115 - s.serialize_field("from", &self.from)?; 116 - s.serialize_field("fromSite", &self.from_site)?; 117 - s.serialize_field("found", &self.found)?; 101 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 102 + where 103 + S: Serializer, 104 + { 105 + let mut s = serializer.serialize_struct("World", 7)?; 106 + s.serialize_field("id", &self.id)?; 107 + s.serialize_field("name", &self.name)?; 108 + s.serialize_field("author", &self.author)?; 109 + s.serialize_field("authorId", &self.author_id)?; 110 + s.serialize_field("desc", &self.desc)?; 111 + s.serialize_field("img", &self.img)?; 112 + s.serialize_field("maxUsers", &self.max_users)?; 113 + s.serialize_field("visits", &self.visits)?; 114 + s.serialize_field("favourites", &self.favourites)?; 115 + s.serialize_field("tags", &self.tags)?; 116 + s.serialize_field("from", &self.from)?; 117 + s.serialize_field("fromSite", &self.from_site)?; 118 + s.serialize_field("found", &self.found)?; 118 119 119 - s.end() 120 - } 120 + s.end() 121 + } 121 122 }
+1 -5
src-tauri/tauri.conf.json
··· 23 23 "security": { 24 24 "csp": "https://photos.phazed.xyz; connect-src ipc: http://ipc.localhost" 25 25 }, 26 - "trayIcon": { 27 - "iconPath": "./icons/icon.ico", 28 - "title": "VRChat Photo Manager" 29 - }, 30 26 "windows": [ 31 27 { 32 28 "fullscreen": false, 33 29 "resizable": true, 34 30 "title": "VRChat Photo Manager", 35 - "width": 1200, 31 + "width": 1220, 36 32 "height": 580, 37 33 "minWidth": 600, 38 34 "minHeight": 400,
+3 -3
src/Components/NavBar.tsx
··· 160 160 fetch('https://photos.phazed.xyz/api/v1/account?token='+localStorage.getItem('token')!) 161 161 .then(data => data.json()) 162 162 .then(data => { 163 - if(!data.data.ok){ 163 + if(!data.ok){ 164 164 console.error(data); 165 165 return; 166 166 } 167 167 168 - console.log(data.data); 169 - props.setStorageInfo({ storage: data.data.user.storage, used: data.data.user.used, sync: data.data.user.settings.enableSync }); 168 + console.log(data); 169 + props.setStorageInfo({ storage: data.user.storage, used: data.user.used, sync: data.user.settings.enableSync }); 170 170 }) 171 171 .catch(e => { 172 172 console.error(e);
+27 -60
src/Components/PhotoList.tsx
··· 5 5 import anime from "animejs"; 6 6 7 7 const PHOTO_HEIGHT = 200; 8 - const MAX_IMAGE_LOAD = 3; 8 + const MAX_IMAGE_LOAD = 10; 9 9 10 10 let months = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; 11 11 ··· 31 31 let imagesLoading = 0; 32 32 33 33 let photoTreeLoadingContainer: HTMLElement; 34 - let photoMetaDataLoadingContainer: HTMLElement; 35 - let photoMetaDataLoadingBar: HTMLElement; 36 34 37 35 let scrollToTop: HTMLElement; 38 36 let scrollToTopActive = false; ··· 51 49 52 50 let quitRender: boolean = false; 53 51 let photoPath: string; 54 - 55 - let finishedFirstLoad = false; 56 52 57 53 createEffect(() => { 58 54 if(props.requestPhotoReload()){ ··· 93 89 constructor( path: string ){ 94 90 this.path = path; 95 91 this.dateString = this.path.split('_')[1]; 96 - 97 - invoke('load_photo_meta', { photo: this.path }); 98 92 } 99 93 100 94 loadImage(){ 101 - if(this.loading || this.loaded || !this.metaLoaded || imagesLoading >= MAX_IMAGE_LOAD)return; 95 + if(this.loading || this.loaded || imagesLoading >= MAX_IMAGE_LOAD)return; 96 + 97 + invoke('load_photo_meta', { photo: this.path }); 98 + if(!this.metaLoaded)return; 99 + 102 100 this.loading = true; 103 101 104 102 imagesLoading++; ··· 108 106 this.imageEl = document.createElement('img'); 109 107 this.imageEl.crossOrigin = 'anonymous'; 110 108 111 - this.imageEl.src = "http://photo.localhost/" + photoPath + this.path; 109 + this.imageEl.src = "http://photo.localhost/" + photoPath + this.path + "?downscale"; 112 110 113 111 this.imageEl.onload = () => { 114 112 this.image!.width = this.scaledWidth!; ··· 222 220 currentRowIndex += 1.4; 223 221 } 224 222 225 - if(currentRowWidth + p.scaledWidth! + 10 < photoContainer.width - 20){ 223 + if(currentRowWidth + p.scaledWidth! + 10 < photoContainer.width - 100){ 226 224 currentRowWidth += p.scaledWidth! + 10; 227 225 currentRow.push(p); 228 226 } else{ ··· 324 322 photo.metadata = data.metadata.split('\u0000').filter(x => x !== '')[1]; 325 323 amountLoaded++; 326 324 327 - photoMetaDataLoadingBar.style.width = (amountLoaded / photos.length) * 100 + '%'; 328 325 photo.metaLoaded = true; 329 - 330 - if(amountLoaded / photos.length === 1 && !finishedFirstLoad){ 331 - finishedFirstLoad = true; 332 - render(); 333 - 334 - anime({ 335 - targets: photoMetaDataLoadingContainer, 336 - height: 0, 337 - easing: 'easeInOutQuad', 338 - duration: 500, 339 - opacity: 0, 340 - complete: () => { 341 - photoMetaDataLoadingContainer.style.display = 'none'; 342 - } 343 - }) 344 - 345 - anime({ 346 - targets: '.reload-photos', 347 - opacity: 1, 348 - duration: 150, 349 - easing: 'easeInOutQuad' 350 - }) 351 - } 352 326 }) 353 327 354 328 listen('photo_create', ( event: any ) => { ··· 377 351 photoTreeLoadingContainer.style.height = '100%'; 378 352 photoTreeLoadingContainer.style.display = 'flex'; 379 353 380 - photoMetaDataLoadingContainer.style.opacity = '1'; 381 - photoMetaDataLoadingContainer.style.height = '100%'; 382 - photoMetaDataLoadingContainer.style.display = 'flex'; 383 - 384 - photoMetaDataLoadingBar.style.width = '0%'; 385 - 386 354 quitRender = true; 387 - finishedFirstLoad = false; 388 355 amountLoaded = 0; 389 356 scroll = 0; 390 357 photos = []; ··· 415 382 photos.push(photo); 416 383 }) 417 384 418 - if(photoPaths.length == 0){ 419 - anime.set(photoMetaDataLoadingContainer, { height: 0, opacity: 0, display: 'none' }); 420 - render(); 421 - 422 - anime({ 423 - targets: '.reload-photos', 424 - opacity: 1, 425 - duration: 150, 426 - easing: 'easeInOutQuad' 427 - }) 428 - } 429 - 430 385 anime({ 431 386 targets: photoTreeLoadingContainer, 432 387 height: 0, ··· 437 392 photoTreeLoadingContainer.style.display = 'none'; 438 393 } 439 394 }) 395 + 396 + anime({ 397 + targets: '.reload-photos', 398 + opacity: 1, 399 + duration: 150, 400 + easing: 'easeInOutQuad' 401 + }) 402 + 403 + render(); 440 404 }) 441 405 } 442 406 ··· 488 452 return ( 489 453 <div class="photo-list"> 490 454 <div class="photo-tree-loading" ref={( el ) => photoTreeLoadingContainer = el}>Scanning Photo Tree...</div> 491 - <div class="photo-tree-loading" ref={( el ) => photoMetaDataLoadingContainer = el}> 492 - <div> 493 - Loading MetaData... 494 - <div class="loading-bar"><div class="loading-bar-inner" ref={( el ) => photoMetaDataLoadingBar = el}></div></div> 495 - </div> 496 - </div> 497 455 498 456 <div class="scroll-to-top" ref={( el ) => scrollToTop = el} onClick={() => targetScroll = 0}> 499 457 <div class="icon"> 500 458 <img draggable="false" src="/icon/angle-up-solid.svg"></img> 501 459 </div> 502 460 </div> 503 - <div class="reload-photos" onClick={() => props.setConfirmationBox("Are you sure you want to reload all photos? This can cause the application to slow down while it is loading...", reloadPhotos)}> 461 + <div class="reload-photos" onClick={() => props.setConfirmationBox("Are you sure you want to reload all photos? This can cause the application to slow down while it is loading...", () => window.location.reload())}> 504 462 <div class="icon" style={{ width: '17px' }}> 505 463 <img draggable="false" src="/icon/arrows-rotate-solid.svg"></img> 464 + </div> 465 + </div> 466 + 467 + <div class="filter-options"> 468 + <div class="icon" style={{ width: '20px', height: '5px', padding: '20px' }}> 469 + <img draggable="false" src="/icon/sliders-solid.svg"></img> 470 + </div> 471 + <div class="icon" style={{ width: '20px', height: '5px', padding: '20px' }}> 472 + <img draggable="false" src="/icon/clock-regular.svg"></img> 506 473 </div> 507 474 </div> 508 475
+129 -10
src/Components/PhotoViewer.tsx
··· 33 33 34 34 let worldCache: WorldCache[] = JSON.parse(localStorage.getItem('worldCache') || "[]"); 35 35 36 - // TODO: Context Menu, (Open file in explorer, Copy Image) 37 36 let PhotoViewer = ( props: PhotoViewerProps ) => { 38 37 let viewer: HTMLElement; 39 - let imageViewer: HTMLElement; 38 + let imageViewer: HTMLImageElement; 40 39 let isOpen = false; 41 40 let trayOpen = false; 42 41 ··· 49 48 let worldInfoContainer: HTMLElement; 50 49 let photoPath: string; 51 50 51 + let viewerContextMenu: HTMLElement; 52 + let viewerContextMenuButtons: HTMLElement[] = []; 53 + 52 54 let openTray = () => { 53 55 if(trayOpen)return; 54 56 trayOpen = true; 55 57 58 + window.CloseAllPopups.forEach(p => p()); 56 59 anime({ targets: photoTray, bottom: '0px', duration: 500 }); 57 60 58 61 anime({ ··· 79 82 let closeTray = () => { 80 83 if(!trayOpen)return; 81 84 85 + window.CloseAllPopups.forEach(p => p()); 82 86 anime({ targets: photoTray, bottom: '-150px', duration: 500 }); 83 87 84 88 anime({ ··· 107 111 anime.set(photoControls, { translateX: '-50%' }); 108 112 anime.set(photoTrayCloseBtn, { translateX: '-50%', opacity: 0, scale: '0.75', bottom: '10px' }); 109 113 114 + let contextMenuOpen = false; 115 + window.CloseAllPopups.push(() => { 116 + contextMenuOpen = false; 117 + anime.set(viewerContextMenu, { opacity: 1, rotate: '0deg' }); 118 + 119 + anime({ 120 + targets: viewerContextMenu, 121 + opacity: 0, 122 + easing: 'easeInOutQuad', 123 + rotate: '30deg', 124 + duration: 100, 125 + complete: () => { 126 + viewerContextMenu.style.display = 'none'; 127 + } 128 + }) 129 + }); 130 + 131 + viewerContextMenuButtons[0].onclick = async () => { 132 + window.CloseAllPopups.forEach(p => p()); 133 + // Context Menu -> Open file location 134 + 135 + let path = await invoke('get_user_photos_path') + '\\' + props.currentPhotoView().path; 136 + invoke('open_folder', { url: path }); 137 + } 138 + 139 + viewerContextMenuButtons[1].onclick = () => { 140 + window.CloseAllPopups.forEach(p => p()); 141 + // Context Menu -> Copy image 142 + 143 + let canvas = document.createElement('canvas'); 144 + let ctx = canvas.getContext('2d')!; 145 + 146 + canvas.width = props.currentPhotoView().width; 147 + canvas.height = props.currentPhotoView().height; 148 + 149 + ctx.drawImage(imageViewer, 0, 0); 150 + 151 + canvas.toBlob(( blob ) => { 152 + navigator.clipboard.write([ 153 + new ClipboardItem({ 154 + 'image/png': blob! 155 + }) 156 + ]); 157 + 158 + canvas.remove(); 159 + 160 + anime.set('.copy-notif', { translateX: '-50%', translateY: '-100px' }); 161 + anime({ 162 + targets: '.copy-notif', 163 + opacity: 1, 164 + translateY: '0px' 165 + }); 166 + 167 + setTimeout(() => { 168 + anime({ 169 + targets: '.copy-notif', 170 + opacity: 0, 171 + translateY: '-100px' 172 + }); 173 + }, 2000); 174 + }); 175 + } 176 + 177 + imageViewer.oncontextmenu = ( e ) => { 178 + if(contextMenuOpen){ 179 + contextMenuOpen = false; 180 + 181 + anime.set(viewerContextMenu, { opacity: 1, rotate: '0deg' }); 182 + 183 + anime({ 184 + targets: viewerContextMenu, 185 + opacity: 0, 186 + rotate: '30deg', 187 + easing: 'easeInOutQuad', 188 + duration: 100, 189 + complete: () => { 190 + viewerContextMenu.style.display = 'none'; 191 + } 192 + }) 193 + } else{ 194 + contextMenuOpen = true; 195 + 196 + viewerContextMenu.style.top = e.clientY + 'px'; 197 + viewerContextMenu.style.left = e.clientX + 'px'; 198 + viewerContextMenu.style.display = 'block'; 199 + 200 + anime.set(viewerContextMenu, { opacity: 0, rotate: '-30deg' }); 201 + 202 + anime({ 203 + targets: viewerContextMenu, 204 + opacity: 1, 205 + rotate: '0deg', 206 + easing: 'easeInOutQuad', 207 + duration: 100 208 + }) 209 + } 210 + } 211 + 110 212 createEffect(() => { 111 213 let photo = props.currentPhotoView(); 112 214 ··· 117 219 if(!photoPath) 118 220 photoPath = await invoke('get_user_photos_path') + '/'; 119 221 120 - imageViewer.style.background = 'url(\'http://photo.localhost/' + (photoPath + props.currentPhotoView().path).split('\\').join('/') +'\')'; 222 + imageViewer.src = 'http://photo.localhost/' + (photoPath + props.currentPhotoView().path).split('\\').join('/') + "?full"; 223 + imageViewer.crossOrigin = 'anonymous'; 121 224 })(); 122 225 123 226 anime({ ··· 157 260 </div> as Node 158 261 ); 159 262 160 - if(!worldData) 263 + 264 + if(!worldData){ 265 + console.log('Fetching new world data'); 266 + 161 267 invoke('find_world_by_id', { worldId: meta.world.id }); 162 - else if(worldData.expiresOn < Date.now()){ 268 + } else if(worldData.expiresOn < Date.now()){ 269 + console.log('Fetching new world data since cache has expired'); 270 + 163 271 worldCache = worldCache.filter(x => x !== worldData) 164 272 invoke('find_world_by_id', { worldId: meta.world.id }); 165 273 } else ··· 209 317 targets: '.navbar', 210 318 top: '0px' 211 319 }) 212 - 320 + 213 321 window.CloseAllPopups.forEach(p => p()); 214 322 215 323 anime({ targets: '.prev-button', top: '75%', easing: 'easeInOutQuad', duration: 100 }); ··· 278 386 279 387 return ( 280 388 <div class="photo-viewer" ref={( el ) => viewer = el}> 389 + <div class="photo-context-menu" ref={( el ) => viewerContextMenu = el}> 390 + <div ref={( el ) => viewerContextMenuButtons.push(el)}>Open file location</div> 391 + <div ref={( el ) => viewerContextMenuButtons.push(el)}>Copy image</div> 392 + </div> 393 + 281 394 <div class="viewer-close viewer-button" onClick={() => props.setCurrentPhotoView(null)}> 282 395 <div class="icon" style={{ width: '10px', margin: '0' }}> 283 396 <img draggable="false" src="/icon/x-solid.svg"></img> 284 397 </div> 285 398 </div> 286 - <div class="image-container" ref={( el ) => imageViewer = el}></div> 399 + <img class="image-container" ref={( el ) => imageViewer = el} /> 287 400 288 - <div class="prev-button" onClick={() => props.setPhotoNavChoice('prev')}> 401 + <div class="prev-button" onClick={() => { 402 + window.CloseAllPopups.forEach(p => p()); 403 + props.setPhotoNavChoice('prev'); 404 + }}> 289 405 <div class="icon" style={{ width: '15px', margin: '0' }}> 290 406 <img draggable="false" src="/icon/arrow-left-solid.svg"></img> 291 407 </div> 292 408 </div> 293 409 294 - <div class="next-button" onClick={() => props.setPhotoNavChoice('next')}> 410 + <div class="next-button" onClick={() => { 411 + window.CloseAllPopups.forEach(p => p()); 412 + props.setPhotoNavChoice('next'); 413 + }}> 295 414 <div class="icon" style={{ width: '15px', margin: '0' }}> 296 415 <img draggable="false" src="/icon/arrow-right-solid.svg"></img> 297 416 </div> ··· 319 438 canvas.width = props.currentPhotoView().width; 320 439 canvas.height = props.currentPhotoView().height; 321 440 322 - ctx.drawImage(props.currentPhotoView().imageEl, 0, 0); 441 + ctx.drawImage(imageViewer, 0, 0); 323 442 324 443 canvas.toBlob(( blob ) => { 325 444 navigator.clipboard.write([
+1 -1
src/Components/SettingsMenu.tsx
··· 179 179 return; 180 180 } 181 181 182 - console.log(data.data); 182 + console.log(data); 183 183 props.setLoggedIn({ loggedIn: true, username: data.user.username, avatar: data.user.avatar, id: data.user._id, serverVersion: data.user.serverVersion }); 184 184 props.setStorageInfo({ storage: data.user.storage, used: data.user.used, sync: data.user.settings.enableSync }); 185 185 })
+2
src/index.tsx
··· 9 9 10 10 window.CloseAllPopups = []; 11 11 12 + window.oncontextmenu = ( e ) => e.preventDefault(); 13 + 12 14 import "./styles.css"; 13 15 import App from "./Components/App"; 14 16
+34
src/styles.css
··· 164 164 overflow: hidden; 165 165 } 166 166 167 + .filter-options{ 168 + position: fixed; 169 + top: 55px; 170 + left: 5px; 171 + width: 40px; 172 + height: 50px; 173 + } 174 + 167 175 .photo-tree-loading{ 168 176 width: 100%; 169 177 height: 100%; ··· 220 228 backdrop-filter: blur(75px); 221 229 opacity: 0; 222 230 display: none; 231 + } 232 + 233 + .photo-context-menu{ 234 + position: fixed; 235 + top: 0; 236 + left: 0; 237 + padding: 10px; 238 + border-radius: 5px; 239 + backdrop-filter: blur(5px); 240 + background: #555a; 241 + color: #aaa; 242 + box-shadow: #0005 0 0 10px; 243 + opacity: 0; 244 + } 245 + 246 + .photo-context-menu > div{ 247 + padding: 2px 10px; 248 + width: calc(100% - 10px); 249 + text-align: center; 250 + transition: 0.1s; 251 + } 252 + 253 + .photo-context-menu > div:hover{ 254 + color: #fff; 255 + cursor: pointer; 256 + user-select: none; 223 257 } 224 258 225 259 .image-container{