diff --git a/package-lock.json b/package-lock.json index 3fcb12a..c899b04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,24 +8,26 @@ "name": "codename-launcher", "version": "0.1.0", "dependencies": { - "@tailwindcss/vite": "^4.1.13", - "@tauri-apps/api": "^2", + "@tailwindcss/vite": "^4.1.18", + "@tauri-apps/api": "^2.10.1", "@tauri-apps/plugin-dialog": "^2.6.0", - "@tauri-apps/plugin-opener": "^2", - "reka-ui": "^2.6.0", - "tailwindcss": "^4.1.13", - "vue": "^3.5.13" + "@tauri-apps/plugin-fs": "^2.4.5", + "@tauri-apps/plugin-opener": "^2.5.3", + "@tauri-apps/plugin-os": "~2.3.2", + "reka-ui": "^2.8.0", + "tailwindcss": "^4.1.18", + "vue": "^3.5.28" }, "devDependencies": { - "@tauri-apps/cli": "^2", + "@tauri-apps/cli": "^2.10.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2", - "@types/node": "^24.5.2", - "@vitejs/plugin-vue": "^5.2.1", + "@types/node": "^24.10.13", + "@vitejs/plugin-vue": "^5.2.4", "prettier": "3.6.2", - "prettier-plugin-css-order": "^2.1.2", - "typescript": "~5.6.2", - "vite": "^6.0.3", - "vue-tsc": "^2.1.10" + "prettier-plugin-css-order": "^2.2.0", + "typescript": "~5.6.3", + "vite": "^6.4.1", + "vue-tsc": "^2.2.12" } }, "node_modules/@babel/code-frame": { @@ -1544,6 +1546,15 @@ "@tauri-apps/api": "^2.8.0" } }, + "node_modules/@tauri-apps/plugin-fs": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.4.5.tgz", + "integrity": "sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, "node_modules/@tauri-apps/plugin-opener": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz", @@ -1553,6 +1564,15 @@ "@tauri-apps/api": "^2.8.0" } }, + "node_modules/@tauri-apps/plugin-os": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.3.2.tgz", + "integrity": "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, "node_modules/@trivago/prettier-plugin-sort-imports": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz", @@ -1595,12 +1615,11 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.12.tgz", - "integrity": "sha512-68e+T28EbdmLSTkPgs3+UacC6rzmqrcWFPQs1C8mwJhI/r5Uxr0yEuQotczNRROd1gq30NGxee+fo0rSIxpyAw==", + "version": "24.10.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", + "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1655,40 +1674,39 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", - "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.28.tgz", + "integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/shared": "3.5.27", - "entities": "^7.0.0", + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.28", + "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", - "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz", + "integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-core": "3.5.28", + "@vue/shared": "3.5.28" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", - "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz", + "integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==", "license": "MIT", - "peer": true, "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/compiler-core": "3.5.27", - "@vue/compiler-dom": "3.5.27", - "@vue/compiler-ssr": "3.5.27", - "@vue/shared": "3.5.27", + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.28", + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", @@ -1696,13 +1714,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", - "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz", + "integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-dom": "3.5.28", + "@vue/shared": "3.5.28" } }, "node_modules/@vue/compiler-vue2": { @@ -1742,53 +1760,53 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", - "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.28.tgz", + "integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.27" + "@vue/shared": "3.5.28" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", - "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.28.tgz", + "integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/reactivity": "3.5.28", + "@vue/shared": "3.5.28" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", - "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz", + "integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.27", - "@vue/runtime-core": "3.5.27", - "@vue/shared": "3.5.27", + "@vue/reactivity": "3.5.28", + "@vue/runtime-core": "3.5.28", + "@vue/shared": "3.5.28", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", - "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.28.tgz", + "integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28" }, "peerDependencies": { - "vue": "3.5.27" + "vue": "3.5.28" } }, "node_modules/@vue/shared": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", - "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz", + "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==", "license": "MIT" }, "node_modules/@vueuse/core": { @@ -2416,7 +2434,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2443,7 +2460,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2499,7 +2515,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -2649,7 +2664,6 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2670,7 +2684,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -2748,17 +2761,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", - "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", + "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", "license": "MIT", - "peer": true, "dependencies": { - "@vue/compiler-dom": "3.5.27", - "@vue/compiler-sfc": "3.5.27", - "@vue/runtime-dom": "3.5.27", - "@vue/server-renderer": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-sfc": "3.5.28", + "@vue/runtime-dom": "3.5.28", + "@vue/server-renderer": "3.5.28", + "@vue/shared": "3.5.28" }, "peerDependencies": { "typescript": "*" diff --git a/package.json b/package.json index d03e3e5..aefb8ea 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "@tailwindcss/vite": "^4.1.18", "@tauri-apps/api": "^2.10.1", "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-fs": "^2.4.5", "@tauri-apps/plugin-opener": "^2.5.3", + "@tauri-apps/plugin-os": "~2.3.2", "reka-ui": "^2.8.0", "tailwindcss": "^4.1.18", "vue": "^3.5.28" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59c7120..b51f85f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,9 +17,15 @@ importers: '@tauri-apps/plugin-dialog': specifier: ^2.6.0 version: 2.6.0 + '@tauri-apps/plugin-fs': + specifier: ^2.4.5 + version: 2.4.5 '@tauri-apps/plugin-opener': specifier: ^2.5.3 version: 2.5.3 + '@tauri-apps/plugin-os': + specifier: ~2.3.2 + version: 2.3.2 reka-ui: specifier: ^2.8.0 version: 2.8.0(vue@3.5.28(typescript@5.6.3)) @@ -612,9 +618,15 @@ packages: '@tauri-apps/plugin-dialog@2.6.0': resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==} + '@tauri-apps/plugin-fs@2.4.5': + resolution: {integrity: sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==} + '@tauri-apps/plugin-opener@2.5.3': resolution: {integrity: sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==} + '@tauri-apps/plugin-os@2.3.2': + resolution: {integrity: sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==} + '@trivago/prettier-plugin-sort-imports@5.2.2': resolution: {integrity: sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==} engines: {node: '>18.12'} @@ -1422,10 +1434,18 @@ snapshots: dependencies: '@tauri-apps/api': 2.10.1 + '@tauri-apps/plugin-fs@2.4.5': + dependencies: + '@tauri-apps/api': 2.10.1 + '@tauri-apps/plugin-opener@2.5.3': dependencies: '@tauri-apps/api': 2.10.1 + '@tauri-apps/plugin-os@2.3.2': + dependencies: + '@tauri-apps/api': 2.10.1 + '@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.28)(prettier@3.6.2)': dependencies: '@babel/generator': 7.29.1 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index da758d4..fe23980 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,6 +8,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0453232ace82dee0dd0b4c87a59bd90f7b53b314f3e0f61fe2ee7c8a16482289" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -290,6 +296,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -479,6 +491,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.43" @@ -491,6 +509,36 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + [[package]] name = "colog" version = "1.4.0" @@ -552,6 +600,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -568,6 +626,19 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + [[package]] name = "core-graphics" version = "0.24.0" @@ -575,12 +646,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.10.0", - "core-foundation", - "core-graphics-types", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", "foreign-types", "libc", ] +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.2.0" @@ -588,7 +670,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -813,6 +895,15 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "dlv-list" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b" +dependencies = [ + "rand 0.8.5", +] + [[package]] name = "dpi" version = "0.1.2" @@ -875,6 +966,19 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" +[[package]] +name = "enigo" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802e4b2ae123615659085369b453cba87c5562e46ed8050a909fee18a9bc3157" +dependencies = [ + "core-graphics 0.23.2", + "libc", + "objc", + "pkg-config", + "windows 0.51.1", +] + [[package]] name = "enumflags2" version = "0.7.12" @@ -992,6 +1096,15 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "file-locker" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ae8b5984a4863d8a32109a848d038bd6d914f20f010cc141375f7a183c41cf" +dependencies = [ + "nix 0.29.0", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1056,6 +1169,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "freedesktop_entry_parser" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db9c27b72f19a99a895f8ca89e2d26e4ef31013376e56fdafef697627306c3e4" +dependencies = [ + "nom", + "thiserror 1.0.69", +] + [[package]] name = "futf" version = "0.1.5" @@ -1269,6 +1392,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -1464,6 +1597,15 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1554,6 +1696,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.10.1" @@ -2017,6 +2165,29 @@ dependencies = [ "libc", ] +[[package]] +name = "linicon" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee8c5653188a809616c97296180a0547a61dba205bcdcbdd261dbd022a25fd9" +dependencies = [ + "file-locker", + "freedesktop_entry_parser", + "linicon-theme", + "memmap2", + "thiserror 1.0.69", +] + +[[package]] +name = "linicon-theme" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f8240c33bb08c5d8b8cdea87b683b05e61037aa76ff26bef40672cc6ecbb80" +dependencies = [ + "freedesktop_entry_parser", + "rust-ini", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -2050,6 +2221,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "markup5ever" version = "0.14.1" @@ -2087,6 +2267,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -2102,6 +2291,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2134,8 +2329,11 @@ dependencies = [ "serini", "tauri", "tauri-build", + "tauri-plugin-decorum", "tauri-plugin-dialog", + "tauri-plugin-fs", "tauri-plugin-opener", + "tauri-plugin-os", ] [[package]] @@ -2195,12 +2393,46 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nodrop" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-conv" version = "0.2.0" @@ -2238,6 +2470,15 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + [[package]] name = "objc2" version = "0.6.3" @@ -2325,6 +2566,16 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + [[package]] name = "objc2-core-text" version = "0.3.2" @@ -2429,8 +2680,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.10.0", + "block2", "objc2", + "objc2-cloud-kit", + "objc2-core-data", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", "objc2-foundation", ] @@ -2480,6 +2750,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-multimap" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485" +dependencies = [ + "dlv-list", + "hashbrown 0.9.1", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -2490,6 +2770,22 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_info" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +dependencies = [ + "android_system_properties", + "log", + "nix 0.30.1", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys 0.61.2", +] + [[package]] name = "pango" version = "0.18.3" @@ -3120,6 +3416,16 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rust-ini" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -3615,6 +3921,15 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -3636,8 +3951,8 @@ checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" dependencies = [ "bitflags 2.10.0", "block2", - "core-foundation", - "core-graphics", + "core-foundation 0.10.1", + "core-graphics 0.24.0", "crossbeam-channel", "dispatch", "dlopen2", @@ -3662,7 +3977,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -3702,6 +4017,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", + "http-range", "jni", "libc", "log", @@ -3733,7 +4049,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.61.3", ] [[package]] @@ -3816,6 +4132,23 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-decorum" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db925c61a04a937028bc91ad8ae64a93b84a1715b964530925a54e793d494999" +dependencies = [ + "anyhow", + "cocoa", + "enigo", + "linicon", + "objc", + "rand 0.8.5", + "serde", + "tauri", + "tauri-plugin", +] + [[package]] name = "tauri-plugin-dialog" version = "2.6.0" @@ -3874,10 +4207,28 @@ dependencies = [ "tauri-plugin", "thiserror 2.0.18", "url", - "windows", + "windows 0.61.3", "zbus", ] +[[package]] +name = "tauri-plugin-os" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997" +dependencies = [ + "gethostname", + "log", + "os_info", + "serde", + "serde_json", + "serialize-to-javascript", + "sys-locale", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-runtime" version = "2.10.0" @@ -3900,7 +4251,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", ] [[package]] @@ -3926,7 +4277,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", "wry", ] @@ -4697,7 +5048,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-implement", "windows-interface", @@ -4721,7 +5072,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", - "windows", + "windows 0.61.3", "windows-core 0.61.2", ] @@ -4771,6 +5122,16 @@ dependencies = [ "windows-version", ] +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core 0.51.1", + "windows-targets 0.48.5", +] + [[package]] name = "windows" version = "0.61.3" @@ -4793,6 +5154,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -4961,6 +5331,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -5018,6 +5403,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5036,6 +5427,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5054,6 +5451,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5084,6 +5487,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5102,6 +5511,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5120,6 +5535,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5138,6 +5559,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5311,7 +5738,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2cbce49..50f0505 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -12,7 +12,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2.10.1", features = [] } +tauri = { version = "2.10.1", features = ["protocol-asset"] } tauri-plugin-opener = "2" tauri-plugin-dialog = "2.6.0" serde = { version = "1", features = ["derive"] } @@ -20,3 +20,6 @@ serde_json = "1" log = "0.4.29" colog = "1.4.0" serini = "0.2.2" +tauri-plugin-decorum = "1.1.1" +tauri-plugin-os = "2" +tauri-plugin-fs = "2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index d238f3d..892b6a7 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -2,13 +2,23 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", - "windows": [ - "main" - ], + "windows": ["main"], "permissions": [ "core:default", "opener:default", "core:window:default", - "dialog:default" + "dialog:default", + "core:window:allow-close", + "core:window:allow-center", + "core:window:allow-minimize", + "core:window:allow-maximize", + "core:window:allow-set-size", + "core:window:allow-set-focus", + "core:window:allow-is-maximized", + "core:window:allow-start-dragging", + "core:window:allow-toggle-maximize", + "decorum:allow-show-snap-overlay", + "os:default", + "fs:default" ] -} \ No newline at end of file +} diff --git a/src-tauri/src/engines.rs b/src-tauri/src/engines.rs index a7c7fe2..3437bcf 100644 --- a/src-tauri/src/engines.rs +++ b/src-tauri/src/engines.rs @@ -1,12 +1,13 @@ -use crate::mods::ModKind; +use crate::mods::{ModKind, MOD_REGISTERY}; use crate::paths::{ - check_for_any_executable, check_for_specific_executable, get_folders_in_folder, + check_for_any_executable, check_for_specific_executable, get_folders_in_folder, ExecutableType, + WINDOWS_EXE_EXTENSION, }; use crate::ContextStore; use serde::Serialize; use std::cell::RefCell; use std::collections::HashMap; -use std::env::consts::EXE_SUFFIX; +use std::env::consts::EXE_EXTENSION; use std::path::Path; use std::sync::Mutex; use tauri::State; @@ -20,7 +21,7 @@ use tauri::State; /// let engine_context = registery.get_engine_context(Path::new("path/to/engine/folder")) /// /// // Get the detector for a context -/// let detector = registery.get_ctx_detector(&engine_context); +/// let detector = registery.get_context_detector(&engine_context); /// /// // Get info from the detector /// let display_name = detector.display_name(); @@ -36,6 +37,7 @@ pub struct EngineContext { pub folder_path: String, pub exe_path: String, pub engine_kind: EngineKind, + pub exe_type: ExecutableType, } /// Acesss the registery using: @@ -46,7 +48,7 @@ pub struct EngineContext { /// }); /// ``` pub struct EngineRegistery { - pub detectors: HashMap>, + pub detectors: HashMap>, } thread_local! { @@ -55,7 +57,7 @@ thread_local! { impl EngineRegistery { pub fn new() -> Self { - let mut detectors: HashMap> = HashMap::new(); + let mut detectors: HashMap> = HashMap::new(); detectors.insert(EngineKind::Executable, Box::new(ExecutableDetector)); detectors.insert(EngineKind::Psych, Box::new(PsychDetector)); @@ -67,28 +69,51 @@ impl EngineRegistery { } pub fn get_engine_context(&self, folder_path: &Path) -> Option { - let mut found_kinds: Vec<&EngineKind> = vec![]; + let mut found_kinds: Vec<(&EngineKind, ExecutableType)> = vec![]; let string_path = folder_path.to_str().unwrap().to_string(); + let mut base_context = EngineContext { folder_path: string_path.clone(), exe_path: string_path, engine_kind: EngineKind::Executable, + exe_type: ExecutableType::Platform, }; - for (kind, detector) in &self.detectors { - if detector.detect_engine(&mut base_context) { - found_kinds.push(kind); + for (engine_kind, detector) in &self.detectors { + if let Some(exe_type) = detector.detect_engine(&mut base_context) { + found_kinds.push((engine_kind, exe_type)); } } - found_kinds.sort(); + // Sort by engine kind, the highest one wins if we have multiple + found_kinds.sort_by_key(|a| *a.0); - if let Some(final_kind) = found_kinds.last() { + if let Some((final_kind, exe_type)) = found_kinds.last() { base_context.engine_kind = **final_kind; + base_context.exe_type = *exe_type; // Update any context properties that may change from detector to detector + // Executable detector sets the exe_path internally due to the nature of how it detects + let final_detector = self.get_context_detector(&base_context); - final_detector.get_executable_name(&mut base_context); + let executable_name = final_detector.get_executable_name(&mut base_context); + + // Manually set the exe_path for other engines + + if base_context.engine_kind != EngineKind::Executable { + let exe_extenstion = if base_context.exe_type == ExecutableType::Platform { + EXE_EXTENSION + } else { + WINDOWS_EXE_EXTENSION + }; + + base_context.exe_path = folder_path + .join(executable_name) + .with_added_extension(exe_extenstion) + .to_str() + .unwrap() + .to_string(); + } Some(base_context) } else { @@ -96,9 +121,9 @@ impl EngineRegistery { } } - pub fn get_context_detector(&self, ctx: &EngineContext) -> &dyn EngineSoftcode { + pub fn get_context_detector(&self, context: &EngineContext) -> &dyn EngineImageInfo { self.detectors - .get(&ctx.engine_kind) + .get(&context.engine_kind) .expect("Detectors should always be registered") .as_ref() } @@ -123,23 +148,31 @@ pub enum EngineKind { pub trait EngineInfo { fn display_name(&self) -> String; fn authors(&self) -> String; + fn color_hex(&self) -> String; } /// Detection logic pub trait EngineDetector: EngineInfo { - fn detect_engine(&self, ctx: &mut EngineContext) -> bool; - fn get_executable_name(&self, ctx: &mut EngineContext) -> String; + fn detect_engine(&self, context: &mut EngineContext) -> Option; + fn get_executable_name(&self, context: &mut EngineContext) -> String; } /// For mods and whatever else (such as cne addons) pub trait EngineSoftcode: EngineDetector { fn supports_softcode(&self) -> bool; - fn softcode_list(&self, ctx: &EngineContext) -> Vec; + fn softcode_list(&self, context: &EngineContext) -> Vec; fn softcode_mod_kind(&self) -> Option { None } } +/// Image info, hard coded for engines base images +pub trait EngineImageInfo: EngineSoftcode { + fn detect_icon(&self) -> Option; + #[allow(dead_code)] + fn detect_banner(&self) -> Option; +} + /// Source Code Mods pub struct ExecutableDetector; @@ -151,21 +184,31 @@ impl EngineInfo for ExecutableDetector { fn authors(&self) -> String { "".to_string() } + + fn color_hex(&self) -> String { + "#FFFFFF".to_string() + } } impl EngineDetector for ExecutableDetector { - fn detect_engine(&self, ctx: &mut EngineContext) -> bool { - let folder_path = Path::new(&ctx.folder_path); - check_for_any_executable(folder_path).is_some() + fn detect_engine(&self, context: &mut EngineContext) -> Option { + let folder_path = Path::new(&context.folder_path); + + if let Some((exe_type, _)) = check_for_any_executable(folder_path) { + Some(exe_type) + } else { + None + } } - fn get_executable_name(&self, ctx: &mut EngineContext) -> String { - let folder_path = Path::new(&ctx.folder_path); + fn get_executable_name(&self, context: &mut EngineContext) -> String { + let folder_path = Path::new(&context.folder_path); let mut exe_filename = "".to_owned(); - if let Some(exe_path) = check_for_any_executable(folder_path) { - ctx.exe_path = exe_path.to_string_lossy().to_string(); - exe_filename = exe_path.file_name().unwrap().to_string_lossy().to_string(); + if let Some((exe_type, exe_path)) = check_for_any_executable(folder_path) { + context.exe_path = exe_path.to_string_lossy().to_string(); + context.exe_type = exe_type; + exe_filename = exe_path.file_stem().unwrap().to_string_lossy().to_string(); } exe_filename @@ -177,7 +220,7 @@ impl EngineSoftcode for ExecutableDetector { false } - fn softcode_list(&self, _ctx: &EngineContext) -> Vec { + fn softcode_list(&self, _context: &EngineContext) -> Vec { vec![] } @@ -186,6 +229,16 @@ impl EngineSoftcode for ExecutableDetector { } } +impl EngineImageInfo for ExecutableDetector { + fn detect_icon(&self) -> Option { + Some("assets/images/engine/codename.png".to_string()) + } + + fn detect_banner(&self) -> Option { + None + } +} + pub struct PsychDetector; impl EngineInfo for PsychDetector { @@ -196,18 +249,22 @@ impl EngineInfo for PsychDetector { fn authors(&self) -> String { "Shadow Mario & Riveren".to_string() } + + fn color_hex(&self) -> String { + "#FFFFFF".to_string() + } } impl EngineDetector for PsychDetector { - fn detect_engine(&self, ctx: &mut EngineContext) -> bool { - let executable_name = self.get_executable_name(ctx); - let folder_path = Path::new(&ctx.folder_path); + fn detect_engine(&self, context: &mut EngineContext) -> Option { + let executable_name = self.get_executable_name(context); + let folder_path = Path::new(&context.folder_path); check_for_specific_executable(folder_path, executable_name) } - fn get_executable_name(&self, _ctx: &mut EngineContext) -> String { - "PsychEngine".to_owned() + EXE_SUFFIX + fn get_executable_name(&self, _context: &mut EngineContext) -> String { + "PsychEngine".to_owned() } } @@ -216,8 +273,8 @@ impl EngineSoftcode for PsychDetector { true } - fn softcode_list(&self, ctx: &EngineContext) -> Vec { - let folder_path = Path::new(&ctx.folder_path); + fn softcode_list(&self, context: &EngineContext) -> Vec { + let folder_path = Path::new(&context.folder_path); let mods_path = folder_path.join("mods"); get_folders_in_folder(&mods_path) @@ -228,6 +285,16 @@ impl EngineSoftcode for PsychDetector { } } +impl EngineImageInfo for PsychDetector { + fn detect_icon(&self) -> Option { + Some("assets/images/engine/codename.png".to_string()) + } + + fn detect_banner(&self) -> Option { + None + } +} + pub struct CodenameDetector; impl EngineInfo for CodenameDetector { @@ -238,18 +305,22 @@ impl EngineInfo for CodenameDetector { fn authors(&self) -> String { "Codename Crew".to_string() } + + fn color_hex(&self) -> String { + "#FFFFFF".to_string() + } } impl EngineDetector for CodenameDetector { - fn detect_engine(&self, ctx: &mut EngineContext) -> bool { - let executable_name = self.get_executable_name(ctx); - let folder_path = Path::new(&ctx.folder_path); + fn detect_engine(&self, context: &mut EngineContext) -> Option { + let executable_name = self.get_executable_name(context); + let folder_path = Path::new(&context.folder_path); check_for_specific_executable(folder_path, executable_name) } - fn get_executable_name(&self, _ctx: &mut EngineContext) -> String { - "CodenameEngine".to_owned() + EXE_SUFFIX + fn get_executable_name(&self, _context: &mut EngineContext) -> String { + "CodenameEngine".to_owned() } } @@ -258,8 +329,8 @@ impl EngineSoftcode for CodenameDetector { true } - fn softcode_list(&self, ctx: &EngineContext) -> Vec { - let folder_path = Path::new(&ctx.folder_path); + fn softcode_list(&self, context: &EngineContext) -> Vec { + let folder_path = Path::new(&context.folder_path); let mods_path = folder_path.join("mods"); get_folders_in_folder(&mods_path) @@ -270,6 +341,16 @@ impl EngineSoftcode for CodenameDetector { } } +impl EngineImageInfo for CodenameDetector { + fn detect_icon(&self) -> Option { + Some("assets/images/engine/codename.png".to_string()) + } + + fn detect_banner(&self) -> Option { + None + } +} + pub struct VSliceDetector; impl EngineInfo for VSliceDetector { @@ -280,18 +361,22 @@ impl EngineInfo for VSliceDetector { fn authors(&self) -> String { "Funkin Crew".to_string() } + + fn color_hex(&self) -> String { + "#FFFFFF".to_string() + } } impl EngineDetector for VSliceDetector { - fn detect_engine(&self, ctx: &mut EngineContext) -> bool { - let executable_name = self.get_executable_name(ctx); - let folder_path = Path::new(&ctx.folder_path); + fn detect_engine(&self, context: &mut EngineContext) -> Option { + let executable_name = self.get_executable_name(context); + let folder_path = Path::new(&context.folder_path); check_for_specific_executable(folder_path, executable_name) } - fn get_executable_name(&self, _ctx: &mut EngineContext) -> String { - "Funkin".to_owned() + EXE_SUFFIX + fn get_executable_name(&self, _context: &mut EngineContext) -> String { + "Funkin".to_owned() } } @@ -300,8 +385,8 @@ impl EngineSoftcode for VSliceDetector { true } - fn softcode_list(&self, ctx: &EngineContext) -> Vec { - let folder_path = Path::new(&ctx.folder_path); + fn softcode_list(&self, context: &EngineContext) -> Vec { + let folder_path = Path::new(&context.folder_path); let mods_path = folder_path.join("mods"); get_folders_in_folder(&mods_path) @@ -312,6 +397,16 @@ impl EngineSoftcode for VSliceDetector { } } +impl EngineImageInfo for VSliceDetector { + fn detect_icon(&self) -> Option { + Some("assets/images/engine/codename.png".to_string()) + } + + fn detect_banner(&self) -> Option { + None + } +} + pub struct FPSPlusDetector; impl EngineInfo for FPSPlusDetector { @@ -322,18 +417,22 @@ impl EngineInfo for FPSPlusDetector { fn authors(&self) -> String { "Rozebud".to_string() } + + fn color_hex(&self) -> String { + "#FFFFFF".to_string() + } } impl EngineDetector for FPSPlusDetector { - fn detect_engine(&self, ctx: &mut EngineContext) -> bool { - let executable_name = self.get_executable_name(ctx); - let folder_path = Path::new(&ctx.folder_path); + fn detect_engine(&self, context: &mut EngineContext) -> Option { + let executable_name = self.get_executable_name(context); + let folder_path = Path::new(&context.folder_path); check_for_specific_executable(folder_path, executable_name) } - fn get_executable_name(&self, _ctx: &mut EngineContext) -> String { - "FunkinFPSPlus".to_owned() + EXE_SUFFIX + fn get_executable_name(&self, _context: &mut EngineContext) -> String { + "FunkinFPSPlus".to_owned() } } @@ -342,7 +441,7 @@ impl EngineSoftcode for FPSPlusDetector { false } - fn softcode_list(&self, _ctx: &EngineContext) -> Vec { + fn softcode_list(&self, _context: &EngineContext) -> Vec { vec![] } @@ -351,23 +450,47 @@ impl EngineSoftcode for FPSPlusDetector { } } +impl EngineImageInfo for FPSPlusDetector { + fn detect_icon(&self) -> Option { + Some("assets/images/engine/codename.png".to_string()) + } + + fn detect_banner(&self) -> Option { + None + } +} + #[derive(serde::Serialize)] /// This is what we send to the frontend, it flattens the EngineContext fields and adds display info for the ui pub struct EngineCanonInfo { #[serde(flatten)] pub context: EngineContext, + + pub supports_softcode: bool, + pub softcode_list: Vec, + pub canonical_display_name: String, pub canonical_author: String, + + pub canonical_color_hex: String, + + pub canonical_icon_path: Option, + pub canonical_banner_path: Option, + pub canonical_launch_args: Option, + pub canonical_command: Option, } #[tauri::command] /// Gets all local engine contexts, then uses the registery to get display info for each context and returns it /// #### Important: If a engine is a executbale then it's author will be the executable name -pub fn get_engine_contexts(ctx_store: State>) -> Vec { - log::info!("Getting engine contexts"); +pub fn get_engine_contexts(context_store: State>) -> Vec { + let store = context_store.lock().unwrap(); + get_engine_contexts_internal(&store) +} - let store = ctx_store.lock().unwrap(); - store +/// Interal implmentation to call from rust, look at non internal for docs +pub fn get_engine_contexts_internal(context_store: &ContextStore) -> Vec { + context_store .engine_list .values() .map(|context| { @@ -375,6 +498,13 @@ pub fn get_engine_contexts(ctx_store: State>) -> Vec>) -> Vec>) -> Vec>, + context_store: State>, folder_path: String, ) -> Result { - let mut store = ctx_store.lock().unwrap(); - if store.engine_list.contains_key(&folder_path) { + let mut store = context_store.lock().unwrap(); + add_engine_context_internal(&mut store, folder_path) +} + +/// Interal implmentation to call from rust, look at non internal for docs +pub fn add_engine_context_internal( + context_store: &mut ContextStore, + folder_path: String, +) -> Result { + if context_store.engine_list.contains_key(&folder_path) { return Err("Engine already exists!".to_string()); } @@ -408,15 +570,58 @@ pub fn add_engine_context( let registery = refrence.borrow(); let path = Path::new(&folder_path); - if let Some(engine_ctx) = registery.get_engine_context(path) { - log::info!("Added engine at path: {}", folder_path); - - store + if let Some(engine_context) = registery.get_engine_context(path) { + context_store .engine_list - .insert(folder_path.clone(), engine_ctx.clone()); - Ok(engine_ctx) + .insert(folder_path.clone(), engine_context.clone()); + + // Add mods to context store + let detector = registery.get_context_detector(&engine_context); + + if detector.supports_softcode() { + let mut found_mod_contexts = vec![]; + + MOD_REGISTERY.with(|refrence| { + let registery = refrence.borrow(); + + for mod_name in detector.softcode_list(&engine_context) { + let mod_context = + registery.get_mod_context(&engine_context, mod_name.clone()); + if let Some(mod_context) = mod_context { + found_mod_contexts.push(mod_context); + } + } + }); + + context_store + .mods_list + .insert(folder_path.clone(), found_mod_contexts); + } + + Ok(engine_context) } else { Err("No engine detected in the specified folder!".to_string()) } }) } + +#[tauri::command] +pub fn remove_engine_context( + context_store: State>, + folder_path: String, +) -> Result<(), String> { + let mut store = context_store.lock().unwrap(); + remove_engine_context_internal(&mut store, folder_path) +} + +pub fn remove_engine_context_internal( + context_store: &mut ContextStore, + folder_path: String, +) -> Result<(), String> { + if let Some(_) = context_store.engine_list.remove(&folder_path) { + context_store.mods_list.remove(&folder_path); + Ok(()) + } else { + Err("No engine found at the specified folder!".to_string()) + } +} diff --git a/src-tauri/src/instances.rs b/src-tauri/src/instances.rs index 01d476a..582c2c1 100644 --- a/src-tauri/src/instances.rs +++ b/src-tauri/src/instances.rs @@ -1,9 +1,13 @@ -use std::{collections::HashMap, path::Path, sync::Mutex}; +use std::{collections::HashMap, sync::Mutex}; use serde::{Deserialize, Serialize}; use tauri::State; -use crate::{engines::ENGINE_REGISTERY, ContextStore, InstanceStore}; +use crate::{ + engines::{add_engine_context_internal, ENGINE_REGISTERY}, + mods::MOD_REGISTERY, + ContextStore, InstanceStore, +}; /// Instance is the persistent and savable version of a engine or mod (containg editable info). /// @@ -19,6 +23,15 @@ use crate::{engines::ENGINE_REGISTERY, ContextStore, InstanceStore}; /// "version": "1.0.1", /// "author": "CodenameCrew", /// "description": "This is a description of my engine." +/// }, +/// "children": [ +/// "path/to/mod/folder" +/// ] +/// }, +/// { +/// "folder_path": "path/to/mod/folder", +/// "info": { +/// "display_name": "Dustin but cooler???" /// } /// } /// ], @@ -38,6 +51,8 @@ use crate::{engines::ENGINE_REGISTERY, ContextStore, InstanceStore}; pub struct Instance { pub folder_path: String, pub edited_info: Option, + /// The list of children instance paths, for mods + pub children: Option>, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] @@ -47,6 +62,11 @@ pub struct EditableInstanceInfo { pub version: Option, pub author: Option, pub description: Option, + pub color_hex: Option, + pub icon_path: Option, + pub banner_path: Option, + pub launch_command: Option, + pub launch_args: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] @@ -59,23 +79,47 @@ pub struct InstanceList { #[tauri::command] /// Gets all instances and their editable info for UI, these are in the correct order as the user ordered them /// #### Important: Make sure you load the instances before you call this -pub fn get_instances(instances_store: State>) -> Vec { - log::info!("Getting instances"); +pub fn get_instances( + instances_store: State>, + from_display_list: bool, +) -> Vec { + let instances_store = instances_store.lock().unwrap(); + get_instances_internal(&instances_store, from_display_list) +} - let store = instances_store.lock().unwrap(); - store - .instance_list - .order - .iter() - .map(|folder_path| { - store - .instance_list - .instances - .get(folder_path) - .expect("Instance path in order list not found in instances map?") - .clone() - }) - .collect() +/// Interal implmentation to call from rust, look at non internal for docs +pub fn get_instances_internal( + instances_store: &InstanceStore, + from_display_list: bool, +) -> Vec { + if from_display_list { + instances_store + .instance_list + .order + .iter() + .map(|folder_path| { + instances_store + .instance_list + .instances + .get(folder_path) + .expect("Instance path in order list not found in instances map?") + .clone() + }) + .collect() + } else { + instances_store + .instance_list + .instances + .values() + .filter(|instance| { + !instances_store + .instance_list + .order + .contains(&instance.folder_path) + }) + .cloned() + .collect() + } } #[tauri::command] @@ -84,8 +128,22 @@ pub fn add_instance( instances_store: State>, folder_path: String, ) -> Result<(), String> { - let mut store = instances_store.lock().unwrap(); - if store.instance_list.instances.contains_key(&folder_path) { + let mut instances_store = instances_store.lock().unwrap(); + add_instance_internal(&mut instances_store, folder_path, true) +} + +/// Interal implmentation to call from rust, look at non internal for docs +/// add_to_display_list is for mods, since they are added as children and should not be in the main display list +pub fn add_instance_internal( + instances_store: &mut InstanceStore, + folder_path: String, + add_to_display_list: bool, +) -> Result<(), String> { + if instances_store + .instance_list + .instances + .contains_key(&folder_path) + { return Err("Instance already exists!".to_string()); } @@ -94,15 +152,169 @@ pub fn add_instance( let instance = Instance { folder_path: folder_path.clone(), edited_info: None, + children: None, }; - store + instances_store .instance_list .instances .insert(folder_path.clone(), instance); - store.instance_list.order.push(folder_path); - save_instances_to_file(&store) + if add_to_display_list { + instances_store.instance_list.order.push(folder_path); + } + + save_instances_to_file(instances_store) +} + +#[tauri::command] +/// Scans a folder for mods and adds them as child instances to the parent instance, saves the appdata +pub fn scan_mod_instances( + instances_store: State>, + context_store: State>, + folder_path: String, +) -> Result<(), String> { + let mut instances_store = instances_store.lock().unwrap(); + let context_store = context_store.lock().unwrap(); + + scan_mod_instances_internal(&mut instances_store, &context_store, folder_path) +} + +/// Interal implmentation to call from rust, look at non internal for docs +pub fn scan_mod_instances_internal( + instances_store: &mut InstanceStore, + context_store: &ContextStore, + folder_path: String, +) -> Result<(), String> { + ENGINE_REGISTERY.with(|refrence| { + let registery = refrence.borrow(); + let Some(engine_context) = context_store.engine_list.get(&folder_path) else { + return Err("No engine detected in the specified folder!".to_string()); + }; + + let detector = registery.get_context_detector(engine_context); + if detector.supports_softcode() { + for mod_name in detector.softcode_list(engine_context) { + MOD_REGISTERY.with(|refrence| { + let registery = refrence.borrow(); + if let Some(mod_context) = + registery.get_mod_context(engine_context, mod_name.clone()) + { + // Add the mod as an instance, then add it as a child to the parent instance + if let Err(error) = add_instance_internal( + instances_store, + mod_context.folder_path.clone(), + false, + ) { + log::error!( + "Failed to add mod instance {}: {}", + mod_context.folder_path, + error + ); + } + + if let Err(error) = add_child_instance_internal( + instances_store, + folder_path.clone(), + mod_context.folder_path.clone(), + ) { + log::error!( + "Failed to add child instance {}: {}", + mod_context.folder_path, + error + ); + } + } else { + log::error!( + "Mod context for mod {} not found in engine context {}", + mod_name, + folder_path + ); + } + }); + } + } + + save_instances_to_file(instances_store) + }) +} + +#[tauri::command] +/// Adds a child instance to a parent instance and saves the appdata +pub fn add_child_instance( + instances_store: State>, + parent_path: String, + child_path: String, +) -> Result<(), String> { + let mut instances_store = instances_store.lock().unwrap(); + add_child_instance_internal(&mut instances_store, parent_path, child_path) +} + +/// Interal implmentation to call from rust, look at non internal for docs +pub fn add_child_instance_internal( + instances_store: &mut InstanceStore, + parent_path: String, + child_path: String, +) -> Result<(), String> { + if let Some(parent_instance) = instances_store + .instance_list + .instances + .get_mut(&parent_path) + { + if parent_instance.children.is_none() { + parent_instance.children = Some(vec![]); + } + + if let Some(children) = &mut parent_instance.children { + if !children.contains(&child_path) { + children.push(child_path.clone()); + } else { + return Err("Child instance already exists in parent instance".to_string()); + } + } + } else { + return Err("Instance with this folder path not found".to_string()); + } + + save_instances_to_file(instances_store) +} + +#[tauri::command] +/// Removes a child instance from a parent instance and saves the appdata +pub fn remove_child_instance( + instances_store: State>, + parent_path: String, + child_path: String, +) -> Result<(), String> { + let mut instances_store = instances_store.lock().unwrap(); + remove_child_instance_internal(&mut instances_store, parent_path, child_path) +} + +/// Interal implmentation to call from rust, look at non internal for docs +pub fn remove_child_instance_internal( + instances_store: &mut InstanceStore, + parent_path: String, + child_path: String, +) -> Result<(), String> { + if let Some(parent_instance) = instances_store + .instance_list + .instances + .get_mut(&parent_path) + { + if let Some(children) = &mut parent_instance.children { + if children.contains(&child_path) { + children.retain(|path| path != &child_path); + } else { + return Err("Child instance does not exist in parent instance".to_string()); + } + } else { + return Err("Parent instance has no children".to_string()); + } + } else { + return Err("Instance with this folder path not found".to_string()); + } + + save_instances_to_file(instances_store) } #[tauri::command] @@ -111,14 +323,27 @@ pub fn remove_instance( instances_store: State>, folder_path: String, ) -> Result<(), String> { - let mut store = instances_store.lock().unwrap(); - if store.instance_list.instances.remove(&folder_path).is_some() { - store + let mut instances_store = instances_store.lock().unwrap(); + remove_instance_internal(&mut instances_store, folder_path) +} + +/// Interal implmentation to call from rust, look at non internal for docs +pub fn remove_instance_internal( + instances_store: &mut InstanceStore, + folder_path: String, +) -> Result<(), String> { + if instances_store + .instance_list + .instances + .remove(&folder_path) + .is_some() + { + instances_store .instance_list .order .retain(|path| path != &folder_path); - save_instances_to_file(&store) + save_instances_to_file(instances_store) } else { Err("Instance with this folder path not found".to_string()) } @@ -126,15 +351,29 @@ pub fn remove_instance( #[tauri::command] /// Edits an instance in the list and saves the appdata +/// Same code for both parents (engines) and mods (children) pub fn edit_instance( instances_store: State>, folder_path: String, new_info: Option, ) -> Result<(), String> { - let mut store = instances_store.lock().unwrap(); - if let Some(instance) = store.instance_list.instances.get_mut(&folder_path) { + let mut instances_store = instances_store.lock().unwrap(); + edit_instance_internal(&mut instances_store, folder_path, new_info) +} + +/// Interal implmentation to call from rust, look at non internal for docs +pub fn edit_instance_internal( + instances_store: &mut InstanceStore, + folder_path: String, + new_info: Option, +) -> Result<(), String> { + if let Some(instance) = instances_store + .instance_list + .instances + .get_mut(&folder_path) + { instance.edited_info = new_info; - save_instances_to_file(&store) + save_instances_to_file(instances_store) } else { Err("Instance with this folder path not found".to_string()) } @@ -155,24 +394,17 @@ pub fn load_instances_from_file( instances_store.instance_list = instance_list; // Check if instantces are still valid by checking if their context is valid. - ENGINE_REGISTERY.with(|refrence| { - let registery = refrence.borrow(); - - for instance_folder in &instances_store.instance_list.order { - let instance_folder_path = Path::new(&instance_folder); - let engine_context = registery.get_engine_context(instance_folder_path); - if let Some(context) = engine_context { - context_store - .engine_list - .insert(instance_folder.clone(), context); - } else { - log::warn!( - "TODO: Show if instance is missing (has no canon info) {}, Should happen on typescript side!!!", - instance_folder + for instance_folder in &instances_store.instance_list.order { + if let Err(error) = + add_engine_context_internal(context_store, instance_folder.to_string()) + { + log::warn!( + "TODO: Show if instance is missing (has no canon info) {}, Should happen on typescript side!!! {}", + instance_folder, + error ); - } } - }); + } Ok(()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cce7ee4..85530e1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2,6 +2,9 @@ use crate::{engines::EngineContext, instances::InstanceList, mods::ModContext}; use std::{collections::HashMap, sync::Mutex}; use tauri::Manager; +#[cfg(target_os = "windows")] +use tauri_plugin_decorum::WebviewWindowExt; + mod engines; mod instances; mod mods; @@ -27,14 +30,24 @@ struct InstanceStore { json_save_path: String, instance_list: InstanceList, } - #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { colog::init(); tauri::Builder::default() + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_decorum::init()) .setup(|app| { + // Linux overlay title bar removal is temporary until decorum plugin is fixed + #[cfg(target_os = "windows")] + { + let main_window = app.get_webview_window("main").unwrap(); + + main_window.create_overlay_titlebar().unwrap(); + } + let app_data_path = app .path() .app_data_dir() @@ -73,14 +86,23 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ // utils.rs utils::show_main_window, + utils::get_dir_size, + utils::launch_executable, + utils::has_non_directory_path, // engines.rs engines::get_engine_contexts, engines::add_engine_context, + engines::remove_engine_context, + // mods.rs + mods::get_mod_contexts_in_folder, // instances.rs instances::get_instances, instances::add_instance, + instances::scan_mod_instances, + instances::add_child_instance, + instances::remove_child_instance, instances::edit_instance, - instances::remove_instance, + instances::remove_instance ]) .plugin(tauri_plugin_opener::init()) .run(tauri::generate_context!()) diff --git a/src-tauri/src/mods.rs b/src-tauri/src/mods.rs index f8ac80f..9390312 100644 --- a/src-tauri/src/mods.rs +++ b/src-tauri/src/mods.rs @@ -1,79 +1,18 @@ -use std::{cell::RefCell, collections::HashMap, path::Path}; +use crate::{ + engines::{EngineContext, ENGINE_REGISTERY}, + ContextStore, +}; use serde::{Deserialize, Serialize}; -use crate::engines::{EngineContext, ENGINE_REGISTERY}; - -#[allow(dead_code)] -pub fn mods_api_demo_1() { - let example_folder = r"C:\Users\lunar\Downloads\PsychEngine-Windows64\PsychEngine"; - - ENGINE_REGISTERY.with(|refrence| { - let registery = refrence.borrow(); - let mods_path = Path::new(example_folder); - - let Some(eng_context) = registery.get_engine_context(mods_path) else { - log::info!("No engine detected!"); - return; - }; - - let engine = registery.get_context_detector(&eng_context); - - let display_name = engine.display_name(); - let authors = engine.authors(); - let executuable_name = eng_context.exe_path.clone(); - - log::info!( - "Display Name: {}, Exe Name: {}, Authors: {}", - display_name, - executuable_name, - authors - ); - - if engine.supports_softcode() { - log::info!("Supports Softcode!!!"); - let mut softcode_list = vec![]; - - for mod_name in engine.softcode_list(&eng_context) { - softcode_list.push(mod_name.clone()); - log::info!("Mod: {}", mod_name); - } - - MOD_REGISTERY.with(|refrence| { - let registery = refrence.borrow(); - - for mod_name in softcode_list { - let mod_ctx = registery.get_mod_context(&eng_context, mod_name.clone()); - let mod_detector = registery.get_context_detector(&mod_ctx); - - let display_name = mod_detector.display_name(&mod_ctx); - let description = mod_detector.description(&mod_ctx); - let version = mod_detector.version(&mod_ctx); - let authors = mod_detector.authors(&mod_ctx); - let color = mod_detector.color(&mod_ctx); - - let icon_path = mod_detector.detect_icon(&mut mod_ctx.clone()); - - log::info!( - "Mod Display Name: {}, Description: {}, Version: {}, Authors: {}, Color: {}, Icon Path: {:#?}", - display_name, - description, - version, - authors, - color, - icon_path - ); - } - }); - } - }); -} +use std::{cell::RefCell, collections::HashMap, path::Path, sync::Mutex}; +use tauri::State; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ModContext { pub folder_path: String, pub mod_kind: ModKind, - pub name: String, + pub mod_name: String, #[serde(skip)] - pub data: EngineModData, + pub mod_data: EngineModData, } pub struct ModRegistery { @@ -108,7 +47,7 @@ impl ModRegistery { /// let registery = refrence.borrow(); /// /// let engine_context = registery.get_engine_context(Path::new("path/to/engine/folder")) - /// let detector = registery.get_ctx_detector(&engine_context); + /// let detector = registery.get_context_detector(&engine_context); /// /// if engine.supports_softcode() { /// log::info!("Supports Softcode!!!"); @@ -119,68 +58,85 @@ impl ModRegistery { /// } /// }); /// ``` - pub fn get_mod_context(&self, engine_ctx: &EngineContext, mod_name: String) -> ModContext { - let mod_path = Path::new(&engine_ctx.folder_path) - .join("mods/") + pub fn get_mod_context( + &self, + engine_context: &EngineContext, + mod_name: String, + ) -> Option { + let mod_path = Path::new(&engine_context.folder_path) + .join("mods") .join(&mod_name); let string_mod_path = mod_path.to_string_lossy().into_owned(); - let mod_kind = ENGINE_REGISTERY.with(|engine_refrence| { + let mod_kind_detected = ENGINE_REGISTERY.with(|engine_refrence| { let engine_registery = engine_refrence.borrow(); - let detector = engine_registery.get_context_detector(engine_ctx); + let detector = engine_registery.get_context_detector(engine_context); if detector.supports_softcode() { if let Some(mod_kind) = detector.softcode_mod_kind() { - return mod_kind; + return Some(mod_kind); } } - log::warn!("Engine does not support softcode mods, defaulting to PsychModDetector"); - ModKind::Psych + None }); + let Some(engine_mod_kind) = mod_kind_detected else { + log::error!( + "Engine does not support softcode or failed to detect mod kind for mod: {}", + mod_name + ); + return None; + }; + let mut base_context = ModContext { folder_path: string_mod_path, - mod_kind, - name: mod_name, - data: EngineModData::None, + mod_kind: engine_mod_kind, + mod_name, + mod_data: EngineModData::None, }; - let data = self.get_mod_data(&base_context); - base_context.data = data; + let mod_data = self.get_mod_data(&base_context); + if let EngineModData::None = mod_data { + log::error!( + "No mod detected in the specified folder: {}", + base_context.mod_name + ); + return None; + } - base_context + base_context.mod_data = mod_data; + Some(base_context) } - pub fn get_context_detector(&self, ctx: &ModContext) -> &dyn ModImageInfo { + pub fn get_context_detector(&self, context: &ModContext) -> &dyn ModImageInfo { self.detectors - .get(&ctx.mod_kind) + .get(&context.mod_kind) .expect("Detectors should always be registered") .as_ref() } - pub fn get_mod_data(&self, ctx: &ModContext) -> EngineModData { - match ctx.mod_kind { + pub fn get_mod_data(&self, context: &ModContext) -> EngineModData { + match context.mod_kind { ModKind::Psych => { - let pack_json_path = Path::new(&ctx.folder_path).join("pack.json"); + let pack_json_path = Path::new(&context.folder_path).join("pack.json"); if pack_json_path.exists() { if let Ok(json_str) = std::fs::read_to_string(pack_json_path) { match serde_json::from_str::(&json_str) { Ok(data) => return EngineModData::Psych(data), Err(e) => log::error!( "Failed to parse pack.json for mod: {}, error: {}", - ctx.name, + context.mod_name, e ), } } else { - log::warn!("Failed to read pack.json for mod: {}", ctx.name); + log::warn!("Failed to read pack.json for mod: {}", context.mod_name); } } - } ModKind::Codename => { - let config_ini_path = Path::new(&ctx.folder_path) + let config_ini_path = Path::new(&context.folder_path) .join("data") .join("config") .join("modpack.ini"); @@ -190,12 +146,12 @@ impl ModRegistery { Ok(data) => return EngineModData::Codename(data), Err(e) => log::error!( "Failed to parse modpack.ini for mod: {}, error: {}", - ctx.name, + context.mod_name, e ), } } else { - log::warn!("Failed to read modpack.ini for mod: {}", ctx.name); + log::warn!("Failed to read modpack.ini for mod: {}", context.mod_name); } } else { log::warn!( @@ -205,19 +161,22 @@ impl ModRegistery { } } ModKind::VSlice => { - let meta_json_path = Path::new(&ctx.folder_path).join("_polymod_meta.json"); + let meta_json_path = Path::new(&context.folder_path).join("_polymod_meta.json"); if meta_json_path.exists() { if let Ok(json_str) = std::fs::read_to_string(meta_json_path) { match serde_json::from_str::(&json_str) { Ok(data) => return EngineModData::VSlice(data), Err(e) => log::error!( "Failed to parse _polymod_meta.json for mod: {}, error: {}", - ctx.name, + context.mod_name, e ), } } else { - log::warn!("Failed to read _polymod_meta.json for mod: {}", ctx.name); + log::warn!( + "Failed to read _polymod_meta.json for mod: {}", + context.mod_name + ); } } } @@ -245,18 +204,18 @@ pub enum EngineModData { /// Basic info pub trait ModInfo { - fn display_name(&self, ctx: &ModContext) -> String; - fn description(&self, ctx: &ModContext) -> String; - fn version(&self, ctx: &ModContext) -> String; - fn authors(&self, ctx: &ModContext) -> String; - fn color(&self, ctx: &ModContext) -> String; + fn display_name(&self, context: &ModContext) -> String; + fn description(&self, context: &ModContext) -> String; + fn version(&self, context: &ModContext) -> String; + fn authors(&self, context: &ModContext) -> String; + fn color_hex(&self, context: &ModContext) -> String; } /// Image info, maybe we can search the internet for banners one day? pub trait ModImageInfo: ModInfo { - fn detect_icon(&self, ctx: &mut ModContext) -> Option; + fn detect_icon(&self, context: &mut ModContext) -> Option; #[allow(dead_code)] - fn detect_banner(&self, ctx: &mut ModContext) -> Option; + fn detect_banner(&self, context: &mut ModContext) -> Option; } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -269,23 +228,23 @@ pub struct PsychPackJson { pub struct PsychModDetector; impl ModInfo for PsychModDetector { - fn display_name(&self, ctx: &ModContext) -> String { - if let EngineModData::Psych(data) = &ctx.data { + fn display_name(&self, context: &ModContext) -> String { + if let EngineModData::Psych(data) = &context.mod_data { return data.name.clone(); } "".to_string() } - fn description(&self, ctx: &ModContext) -> String { - if let EngineModData::Psych(data) = &ctx.data { - return data.description.clone(); + fn description(&self, context: &ModContext) -> String { + if let EngineModData::Psych(mod_data) = &context.mod_data { + return mod_data.description.clone(); } "".to_string() } - fn version(&self, ctx: &ModContext) -> String { + fn version(&self, context: &ModContext) -> String { log::info!("TODO: Psych mods do not have version data"); - self.display_name(ctx) + self.display_name(context) } fn authors(&self, _: &ModContext) -> String { @@ -293,12 +252,12 @@ impl ModInfo for PsychModDetector { "Unknown".to_string() } - fn color(&self, ctx: &ModContext) -> String { - if let EngineModData::Psych(data) = &ctx.data { - if data.color.len() >= 3 { - let r = (data.color[0] * 255.0) as u8; - let g = (data.color[1] * 255.0) as u8; - let b = (data.color[2] * 255.0) as u8; + fn color_hex(&self, context: &ModContext) -> String { + if let EngineModData::Psych(mod_data) = &context.mod_data { + if mod_data.color.len() >= 3 { + let r = (mod_data.color[0] * 255.0) as u8; + let g = (mod_data.color[1] * 255.0) as u8; + let b = (mod_data.color[2] * 255.0) as u8; return format!("#{:02X}{:02X}{:02X}", r, g, b); } } @@ -307,10 +266,10 @@ impl ModInfo for PsychModDetector { } impl ModImageInfo for PsychModDetector { - fn detect_icon(&self, ctx: &mut ModContext) -> Option { - let icon_path = Path::new(&ctx.folder_path).join("pack.png"); + fn detect_icon(&self, context: &mut ModContext) -> Option { + let icon_path = Path::new(&context.folder_path).join("pack.png"); if icon_path.exists() { - Some(icon_path.canonicalize().unwrap().to_string_lossy().to_string()) + Some(icon_path.to_string_lossy().to_string()) } else { None } @@ -340,45 +299,47 @@ pub struct CodenameConfigIni { pub struct CodenameModDetector; impl ModInfo for CodenameModDetector { - fn display_name(&self, ctx: &ModContext) -> String { - if let EngineModData::Codename(data) = &ctx.data { - return data.name.clone(); + fn display_name(&self, context: &ModContext) -> String { + if let EngineModData::Codename(mod_data) = &context.mod_data { + return mod_data.name.clone(); } "".to_string() } - fn description(&self, ctx: &ModContext) -> String { - if let EngineModData::Codename(data) = &ctx.data { - return data.description.clone(); + fn description(&self, context: &ModContext) -> String { + if let EngineModData::Codename(mod_data) = &context.mod_data { + return mod_data.description.clone(); } "".to_string() } - fn version(&self, ctx: &ModContext) -> String { - if let EngineModData::Codename(data) = &ctx.data { - return data.version.clone(); + fn version(&self, context: &ModContext) -> String { + if let EngineModData::Codename(mod_data) = &context.mod_data { + return mod_data.version.clone(); } "".to_string() } - fn authors(&self, ctx: &ModContext) -> String { - if let EngineModData::Codename(data) = &ctx.data { - return data.author.clone(); + fn authors(&self, context: &ModContext) -> String { + if let EngineModData::Codename(mod_data) = &context.mod_data { + return mod_data.author.clone(); } "".to_string() } - fn color(&self, _: &ModContext) -> String { + fn color_hex(&self, _: &ModContext) -> String { log::info!("TODO: Codename mods do not have color data"); "#FFFFFF".to_string() } } impl ModImageInfo for CodenameModDetector { - fn detect_icon(&self, ctx: &mut ModContext) -> Option { - if let EngineModData::Codename(data) = &ctx.data { - if let Some(icon_path) = &data.icon_path { - let full_icon_path = Path::new(&ctx.folder_path).join("images").join(icon_path); + fn detect_icon(&self, context: &mut ModContext) -> Option { + if let EngineModData::Codename(mod_data) = &context.mod_data { + if let Some(icon_path) = &mod_data.icon_path { + let full_icon_path = Path::new(&context.folder_path) + .join("images") + .join(icon_path); if full_icon_path.exists() { return Some(full_icon_path.to_string_lossy().to_string()); } @@ -406,43 +367,43 @@ pub struct VSlicePolymodMetaJson { pub struct VSliceModDetector; impl ModInfo for VSliceModDetector { - fn display_name(&self, ctx: &ModContext) -> String { - if let EngineModData::VSlice(data) = &ctx.data { - return data.name.clone(); + fn display_name(&self, context: &ModContext) -> String { + if let EngineModData::VSlice(mod_data) = &context.mod_data { + return mod_data.name.clone(); } "".to_string() } - fn description(&self, ctx: &ModContext) -> String { - if let EngineModData::VSlice(data) = &ctx.data { - return data.description.clone(); + fn description(&self, context: &ModContext) -> String { + if let EngineModData::VSlice(mod_data) = &context.mod_data { + return mod_data.description.clone(); } "".to_string() } - fn version(&self, ctx: &ModContext) -> String { - if let EngineModData::VSlice(data) = &ctx.data { - return data.version.clone(); + fn version(&self, context: &ModContext) -> String { + if let EngineModData::VSlice(mod_data) = &context.mod_data { + return mod_data.version.clone(); } "".to_string() } - fn authors(&self, ctx: &ModContext) -> String { - if let EngineModData::VSlice(data) = &ctx.data { - return data.author.clone(); + fn authors(&self, context: &ModContext) -> String { + if let EngineModData::VSlice(mod_data) = &context.mod_data { + return mod_data.author.clone(); } "".to_string() } - fn color(&self, _: &ModContext) -> String { + fn color_hex(&self, _: &ModContext) -> String { log::info!("TODO: VSlice mods do not have color data"); "#FFFFFF".to_string() } } impl ModImageInfo for VSliceModDetector { - fn detect_icon(&self, ctx: &mut ModContext) -> Option { - let full_icon_path = Path::new(&ctx.folder_path).join("_polymod_icon"); + fn detect_icon(&self, context: &mut ModContext) -> Option { + let full_icon_path = Path::new(&context.folder_path).join("_polymod_icon"); if full_icon_path.exists() { return Some(full_icon_path.to_string_lossy().to_string()); } @@ -454,3 +415,63 @@ impl ModImageInfo for VSliceModDetector { None } } + +#[derive(serde::Serialize)] +pub struct ModCanonInfo { + #[serde(flatten)] + pub context: ModContext, + + pub canonical_display_name: String, + pub canonical_description: String, + pub canonical_version: String, + pub canonical_author: String, + pub canonical_color_hex: String, + + pub canonical_icon_path: Option, + pub canonical_banner_path: Option, +} + +#[tauri::command] +/// Gets all local mod contexts, then uses the registery to get display info for each context and returns it +/// #### Important: If a engine is a executbale then it's author will be the executable name +pub fn get_mod_contexts_in_folder( + context_store: State>, + folder_path: String, +) -> Vec { + let context_store = context_store.lock().unwrap(); + get_mod_contexts_in_folder_internal(&context_store, folder_path) +} + +/// Interal implmentation to call from rust, look at non internal for docs +pub fn get_mod_contexts_in_folder_internal( + context_store: &ContextStore, + folder_path: String, +) -> Vec { + if let Some(mod_contexts) = context_store.mods_list.get(&folder_path) { + MOD_REGISTERY.with(|refrence| { + let registery = refrence.borrow(); + mod_contexts + .iter() + .map(|mod_context| { + let detector = registery.get_context_detector(mod_context); + + ModCanonInfo { + context: mod_context.clone(), + + canonical_display_name: detector.display_name(mod_context), + canonical_description: detector.description(mod_context), + canonical_version: detector.version(mod_context), + canonical_author: detector.authors(mod_context), + canonical_color_hex: detector.color_hex(mod_context), + + canonical_icon_path: detector.detect_icon(&mut mod_context.clone()), + canonical_banner_path: detector.detect_banner(&mut mod_context.clone()), + } + }) + .collect() + }) + } else { + log::warn!("No mod contexts found for folder {}", folder_path); + vec![] + } +} diff --git a/src-tauri/src/paths.rs b/src-tauri/src/paths.rs index e1c51a7..2167c25 100644 --- a/src-tauri/src/paths.rs +++ b/src-tauri/src/paths.rs @@ -1,9 +1,24 @@ +use serde::Serialize; use std::{ - env::consts::EXE_EXTENSION, fs, path::{Path, PathBuf}, }; +/// Windows EXE suffix for unix systems where EXE_SUFFIX is empty, used for wine support +pub const WINDOWS_EXE_EXTENSION: &str = "exe"; + +// This is used to exclude certain file types from being detected as executables. This will match any part of the file extension! +// For example, "file.so.lol" will be excluded because it contains "so" +pub const EXCLUDED_EXE_FILE_EXTENSIONS: [&str; 2] = ["ndll", "so"]; + +/// Allows engine detectors to find out what type of executable they found, used for wine support +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum ExecutableType { + Platform, + #[allow(dead_code)] + WindowsOnUnix, +} + pub fn get_paths_in_folder(folder_path: &Path) -> Vec { if let Ok(directory) = fs::read_dir(folder_path) { let mut found_paths = vec![]; @@ -19,6 +34,63 @@ pub fn get_paths_in_folder(folder_path: &Path) -> Vec { vec![] } +fn get_executable_type(path: &Path) -> Option { + if path.is_dir() { + return None; + } + + #[cfg(target_family = "unix")] + { + // First check if it's an ELF binary (with exclusions) + let valid_elf = loop { + if let Some(ext) = path.extension() { + if let Some(ext_str) = ext.to_str() { + if EXCLUDED_EXE_FILE_EXTENSIONS.contains(&ext_str) { + log::info!( + "Excluded file extension found: {} ({}), skipping ELF check", + ext_str, + path.display() + ); + break false; + } + } + } + break true; + }; + + if valid_elf { + if let Ok(mut file) = fs::File::open(path) { + use std::io::Read; + let mut magic_buf = [0u8; 4]; + if file.read_exact(&mut magic_buf).is_ok() && &magic_buf == b"\x7FELF" { + log::info!("ELF executable found: {}", path.display()); + return Some(ExecutableType::Platform); + } + } + } + + // if not an ELF, check if it has a windows exe suffix for wine support + if let Some(ext) = path.extension() { + if ext == WINDOWS_EXE_EXTENSION { + log::info!("Windows executable found: {}", path.display()); + return Some(ExecutableType::WindowsOnUnix); + } + } + } + + #[cfg(target_family = "windows")] + { + if let Some(ext) = path.extension() { + if ext == "exe" { + log::info!("Windows executable found: {}", path.display()); + return Some(ExecutableType::Platform); + } + } + } + + None +} + pub fn get_folders_in_folder(folder_path: &Path) -> Vec { let folders = get_paths_in_folder(folder_path); let mut found_folders = vec![]; @@ -33,40 +105,44 @@ pub fn get_folders_in_folder(folder_path: &Path) -> Vec { found_folders } -pub fn check_for_specific_executable(folder_path: &Path, name: String) -> bool { +/// Provide file stem instead of full name, the banna in banna.exe +pub fn check_for_specific_executable(folder_path: &Path, name: String) -> Option { let paths = get_paths_in_folder(folder_path); for file in paths { - if let Some(stem) = file.file_name() { - if !file.is_dir() && *stem == *name { - return true; + if let Some(file_stem) = file.file_stem() { + if !file.is_dir() && *file_stem == *name { + if let Some(executable_type) = get_executable_type(file.as_path()) { + return Some(executable_type); + } } } } - false + None } -pub fn check_for_any_executable(folder_path: &Path) -> Option { +/// Provide file stem instead of full name, the banna in banna.exe +pub fn check_for_any_executable(folder_path: &Path) -> Option<(ExecutableType, PathBuf)> { let paths = get_paths_in_folder(folder_path); - let mut found_executables: Vec = vec![]; + let mut found_executables: Vec<(ExecutableType, PathBuf)> = vec![]; for file in paths { - if let Some(ext) = file.extension() { - if !file.is_dir() && *ext == *EXE_EXTENSION { - found_executables.push(file); - } + if let Some(executable_type) = get_executable_type(file.as_path()) { + found_executables.push((executable_type, file)); } } // Biggest one most likely to be the game exe if !found_executables.is_empty() { found_executables.sort_by_key(|path_buf| { - let path = path_buf.as_path(); + let path = path_buf.1.as_path(); fs::metadata(path).map(|m| m.len()).unwrap_or(0) }); - Some(found_executables.last().unwrap().to_path_buf()) + let biggest_executable = &found_executables.last().unwrap(); + + Some((biggest_executable.0, biggest_executable.1.clone())) } else { None } diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 647f5da..b1abe2a 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -4,3 +4,85 @@ pub fn show_main_window(window: tauri::Window) { window.show().unwrap(); } + +#[tauri::command] +pub async fn get_dir_size(folder_path: String) -> u64 { + fn walk(path: &std::path::Path) -> u64 { + match std::fs::read_dir(path) { + Err(e) => { + log::warn!("get_dir_size: cannot read {:?}: {}", path, e); + 0 + } + Ok(entries) => entries + .filter_map(|e| e.ok()) + .map(|e| { + let p = e.path(); + if p.is_dir() { + walk(&p) + } else { + e.metadata().map(|m| m.len()).unwrap_or(0) + } + }) + .sum(), + } + } + let path = std::path::Path::new(&folder_path); + log::info!( + "get_dir_size called with: {:?} (exists: {})", + folder_path, + path.exists() + ); + walk(path) +} + +#[tauri::command] +pub fn launch_executable( + working_dir: String, + executable_path: String, + launcher_command: Option, + launch_args: Option, +) -> Result<(), String> { + let working_dir = std::path::Path::new(working_dir.trim()); + let executable_path = std::path::Path::new(executable_path.trim()); + let command = launcher_command + .as_ref() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()); + let args = launch_args + .as_ref() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.split_whitespace().collect::>()) + .unwrap_or_default(); + + log::info!( + "launch_executable called with: working_dir={:?}, executable_path={:?}, launcher_command={:?}, launch_args={:?}", + working_dir, + executable_path, + command, + args + ); + + let mut process = if let Some(launcher) = command { + let mut cmd = std::process::Command::new(launcher); + cmd.arg(executable_path); + cmd + } else { + std::process::Command::new(executable_path) + }; + + process + .args(args) + .current_dir(working_dir) + .spawn() + .map_err(|e| format!("Failed to launch executable: {}", e))?; + return Ok(()); +} + +#[tauri::command] +pub fn has_non_directory_path(paths: Vec) -> bool { + paths.iter().any(|raw_path| { + let path = std::path::Path::new(raw_path.trim()); + !path.exists() || !path.is_dir() + }) +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0cff240..127df39 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,5 +1,4 @@ { - "$schema": "https://schema.tauri.app/config/2", "productName": "Modfriend", "version": "0.1.0", "identifier": "com.CodenameCrew.modfriend", @@ -13,15 +12,31 @@ "windows": [ { "title": "Modfriend", + "label": "main", "minWidth": 1000, "minHeight": 600, "visible": false, - "backgroundColor": "#2d1e32" + "backgroundColor": "#2d1e32", + "titleBarStyle": "Overlay", + "hiddenTitle": true, + "trafficLightPosition": { + "x": 22, + "y": 24 + } } ], "security": { - "csp": null - } + "csp": { + "default-src": [ + "'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost" + ] + }, + "assetProtocol": { + "enable": true, + "scope": ["**"] + } + }, + "withGlobalTauri": true }, "bundle": { "active": true, diff --git a/src/Root.vue b/src/Root.vue index bd4fef5..a0f72c5 100644 --- a/src/Root.vue +++ b/src/Root.vue @@ -1,14 +1,63 @@ + + diff --git a/src/assets/fonts/BeVietnamPro-ExtraBold.ttf b/src/assets/fonts/BeVietnamPro-ExtraBold.ttf new file mode 100644 index 0000000..c6ab04d Binary files /dev/null and b/src/assets/fonts/BeVietnamPro-ExtraBold.ttf differ diff --git a/src/assets/fonts/IBMPlexSans-Italic.ttf b/src/assets/fonts/IBMPlexSans-Italic.ttf new file mode 100644 index 0000000..6232aaa Binary files /dev/null and b/src/assets/fonts/IBMPlexSans-Italic.ttf differ diff --git a/src/assets/fonts/IBMPlexSans.ttf b/src/assets/fonts/IBMPlexSans.ttf new file mode 100644 index 0000000..9add875 Binary files /dev/null and b/src/assets/fonts/IBMPlexSans.ttf differ diff --git a/src/assets/fonts/PhantomMuff Full Letters 1.1.5.ttf b/src/assets/fonts/PhantomMuff.ttf similarity index 100% rename from src/assets/fonts/PhantomMuff Full Letters 1.1.5.ttf rename to src/assets/fonts/PhantomMuff.ttf diff --git a/src/assets/fonts/Roboto-Italic-VariableFont_wdth,wght.ttf b/src/assets/fonts/Roboto-Italic-VariableFont_wdth,wght.ttf deleted file mode 100644 index 978e53a..0000000 Binary files a/src/assets/fonts/Roboto-Italic-VariableFont_wdth,wght.ttf and /dev/null differ diff --git a/src/assets/fonts/Roboto-VariableFont_wdth,wght.ttf b/src/assets/fonts/Roboto-VariableFont_wdth,wght.ttf deleted file mode 100644 index bba55f6..0000000 Binary files a/src/assets/fonts/Roboto-VariableFont_wdth,wght.ttf and /dev/null differ diff --git a/src/assets/img/bg/bg-codename.png b/src/assets/images/bg/bg-codename.png similarity index 100% rename from src/assets/img/bg/bg-codename.png rename to src/assets/images/bg/bg-codename.png diff --git a/src/assets/images/bg/codename.png b/src/assets/images/bg/codename.png new file mode 100644 index 0000000..c9585fe Binary files /dev/null and b/src/assets/images/bg/codename.png differ diff --git a/src/assets/images/bg/default.png b/src/assets/images/bg/default.png new file mode 100644 index 0000000..9b917a0 Binary files /dev/null and b/src/assets/images/bg/default.png differ diff --git a/src/assets/images/bg/top-diamond-gradient.svg b/src/assets/images/bg/top-diamond-gradient.svg new file mode 100644 index 0000000..28ca9fc --- /dev/null +++ b/src/assets/images/bg/top-diamond-gradient.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/img/engine/codename.png b/src/assets/images/engine/codename.png similarity index 100% rename from src/assets/img/engine/codename.png rename to src/assets/images/engine/codename.png diff --git a/src/assets/images/icon/add.png b/src/assets/images/icon/add.png new file mode 100644 index 0000000..9334066 Binary files /dev/null and b/src/assets/images/icon/add.png differ diff --git a/src/assets/img/icon/check.png b/src/assets/images/icon/check.png similarity index 100% rename from src/assets/img/icon/check.png rename to src/assets/images/icon/check.png diff --git a/src/assets/img/icon/checkbox.png b/src/assets/images/icon/checkbox.png similarity index 100% rename from src/assets/img/icon/checkbox.png rename to src/assets/images/icon/checkbox.png diff --git a/src/assets/img/icon/checkbox_outline.png b/src/assets/images/icon/checkbox_outline.png similarity index 100% rename from src/assets/img/icon/checkbox_outline.png rename to src/assets/images/icon/checkbox_outline.png diff --git a/src/assets/img/icon/close.png b/src/assets/images/icon/close.png similarity index 100% rename from src/assets/img/icon/close.png rename to src/assets/images/icon/close.png diff --git a/src/assets/images/icon/delete.png b/src/assets/images/icon/delete.png new file mode 100644 index 0000000..8cb3f17 Binary files /dev/null and b/src/assets/images/icon/delete.png differ diff --git a/src/assets/img/icon/expand.png b/src/assets/images/icon/expand.png similarity index 100% rename from src/assets/img/icon/expand.png rename to src/assets/images/icon/expand.png diff --git a/src/assets/img/icon/folder.png b/src/assets/images/icon/folder.png similarity index 100% rename from src/assets/img/icon/folder.png rename to src/assets/images/icon/folder.png diff --git a/src/assets/img/icon/open_in_new.png b/src/assets/images/icon/open_in_new.png similarity index 100% rename from src/assets/img/icon/open_in_new.png rename to src/assets/images/icon/open_in_new.png diff --git a/src/assets/img/icon/play.png b/src/assets/images/icon/play.png similarity index 100% rename from src/assets/img/icon/play.png rename to src/assets/images/icon/play.png diff --git a/src/assets/images/icon/search.png b/src/assets/images/icon/search.png new file mode 100644 index 0000000..6e74d1f Binary files /dev/null and b/src/assets/images/icon/search.png differ diff --git a/src/assets/images/icon/sort.png b/src/assets/images/icon/sort.png new file mode 100644 index 0000000..e3da078 Binary files /dev/null and b/src/assets/images/icon/sort.png differ diff --git a/src/assets/img/icon/wrench.png b/src/assets/images/icon/wrench.png similarity index 100% rename from src/assets/img/icon/wrench.png rename to src/assets/images/icon/wrench.png diff --git a/src/components/Background.vue b/src/components/Background.vue new file mode 100644 index 0000000..5c76a54 --- /dev/null +++ b/src/components/Background.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/src/components/Categories.vue b/src/components/Categories.vue new file mode 100644 index 0000000..288cad4 --- /dev/null +++ b/src/components/Categories.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/src/components/Titlebar.vue b/src/components/Titlebar.vue new file mode 100644 index 0000000..24cef64 --- /dev/null +++ b/src/components/Titlebar.vue @@ -0,0 +1,14 @@ + + + + diff --git a/src/components/library/InstanceSettingsPopover.vue b/src/components/library/InstanceSettingsPopover.vue new file mode 100644 index 0000000..8ac6b35 --- /dev/null +++ b/src/components/library/InstanceSettingsPopover.vue @@ -0,0 +1,185 @@ + + +