mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 03:02:22 +01:00
@@ -4,6 +4,16 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const cookieParser = require("cookie-parser");
|
||||
|
||||
const audit = require('express-requests-logger')
|
||||
express.use(audit({
|
||||
request: {
|
||||
maxBodyLength: 150
|
||||
},
|
||||
response: {
|
||||
maxBodyLength: 150
|
||||
}
|
||||
}));
|
||||
|
||||
express.use(Express.json());
|
||||
express.use(Express.urlencoded({ extended: true }));
|
||||
express.use(Express.static('public'));
|
||||
|
||||
248
backend/package-lock.json
generated
248
backend/package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"cookie-parser": "^1.4.6",
|
||||
"express": "^4.18.2",
|
||||
"express-requests-logger": "^4.0.0",
|
||||
"ini": "^2.0.0",
|
||||
"nexe": "^4.0.0-rc.6",
|
||||
"path": "^0.12.7",
|
||||
@@ -388,6 +389,23 @@
|
||||
"node": ">v0.4.12"
|
||||
}
|
||||
},
|
||||
"node_modules/bunyan": {
|
||||
"version": "1.8.15",
|
||||
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz",
|
||||
"integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==",
|
||||
"engines": [
|
||||
"node >=0.10.0"
|
||||
],
|
||||
"bin": {
|
||||
"bunyan": "bin/bunyan"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dtrace-provider": "~0.8",
|
||||
"moment": "^2.19.3",
|
||||
"mv": "~2",
|
||||
"safe-json-stringify": "~1"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -1063,6 +1081,19 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/dtrace-provider": {
|
||||
"version": "0.8.8",
|
||||
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
|
||||
"integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"nan": "^2.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexer3": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz",
|
||||
@@ -1168,6 +1199,16 @@
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-requests-logger": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/express-requests-logger/-/express-requests-logger-4.0.0.tgz",
|
||||
"integrity": "sha512-NHQptnDY0fceiTSWLnW0dbJSFlrvbFpCGHmY6LsTMmJLgkyO3x8qAJ+EsryQRMga20YH8Ynt/vnmg23QP07h1Q==",
|
||||
"dependencies": {
|
||||
"bunyan": "^1.8.14",
|
||||
"flat": "^5.0.2",
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
@@ -1292,6 +1333,14 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/flat": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
|
||||
"integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
|
||||
"bin": {
|
||||
"flat": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/fn.name": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||
@@ -1788,6 +1837,11 @@
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
|
||||
@@ -1973,6 +2027,15 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/moo-server": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/moo-server/-/moo-server-1.3.0.tgz",
|
||||
@@ -2009,6 +2072,77 @@
|
||||
"readable-stream": "^3.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mv": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
|
||||
"integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"mkdirp": "~0.5.1",
|
||||
"ncp": "~2.0.0",
|
||||
"rimraf": "~2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mv/node_modules/glob": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
|
||||
"integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "2 || 3",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mv/node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mv/node_modules/rimraf": {
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
|
||||
"integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==",
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"glob": "^6.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.20.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz",
|
||||
"integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/ncp": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
|
||||
"integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"ncp": "bin/ncp"
|
||||
}
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
@@ -2534,6 +2668,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/safe-json-stringify": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
|
||||
"integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz",
|
||||
@@ -3405,6 +3545,17 @@
|
||||
"wrench": "1.3.x"
|
||||
}
|
||||
},
|
||||
"bunyan": {
|
||||
"version": "1.8.15",
|
||||
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz",
|
||||
"integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==",
|
||||
"requires": {
|
||||
"dtrace-provider": "~0.8",
|
||||
"moment": "^2.19.3",
|
||||
"mv": "~2",
|
||||
"safe-json-stringify": "~1"
|
||||
}
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -3941,6 +4092,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dtrace-provider": {
|
||||
"version": "0.8.8",
|
||||
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
|
||||
"integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"nan": "^2.14.0"
|
||||
}
|
||||
},
|
||||
"duplexer3": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz",
|
||||
@@ -4038,6 +4198,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"express-requests-logger": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/express-requests-logger/-/express-requests-logger-4.0.0.tgz",
|
||||
"integrity": "sha512-NHQptnDY0fceiTSWLnW0dbJSFlrvbFpCGHmY6LsTMmJLgkyO3x8qAJ+EsryQRMga20YH8Ynt/vnmg23QP07h1Q==",
|
||||
"requires": {
|
||||
"bunyan": "^1.8.14",
|
||||
"flat": "^5.0.2",
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"ext-list": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz",
|
||||
@@ -4130,6 +4300,11 @@
|
||||
"unpipe": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"flat": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
|
||||
"integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="
|
||||
},
|
||||
"fn.name": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||
@@ -4496,6 +4671,11 @@
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"log-symbols": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
|
||||
@@ -4626,6 +4806,12 @@
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"optional": true
|
||||
},
|
||||
"moo-server": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/moo-server/-/moo-server-1.3.0.tgz",
|
||||
@@ -4645,6 +4831,62 @@
|
||||
"readable-stream": "^3.6.0"
|
||||
}
|
||||
},
|
||||
"mv": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
|
||||
"integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"mkdirp": "~0.5.1",
|
||||
"ncp": "~2.0.0",
|
||||
"rimraf": "~2.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"glob": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
|
||||
"integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "2 || 3",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.6"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
|
||||
"integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"glob": "^6.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.20.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz",
|
||||
"integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==",
|
||||
"optional": true
|
||||
},
|
||||
"ncp": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
|
||||
"integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==",
|
||||
"optional": true
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
@@ -5000,6 +5242,12 @@
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
},
|
||||
"safe-json-stringify": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
|
||||
"integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
|
||||
"optional": true
|
||||
},
|
||||
"safe-stable-stringify": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz",
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"uuid": "^8.3.2",
|
||||
"ws": "^8.5.0",
|
||||
"xml-parser": "^1.2.1",
|
||||
"xmlbuilder": "^15.1.1"
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"express-requests-logger": "^4.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -75,5 +75,12 @@
|
||||
"direction": "OUTBOUND",
|
||||
"created": "2024-05-31T19:50:04.738Z",
|
||||
"favorite": false
|
||||
},
|
||||
{
|
||||
"accountId": "Player724",
|
||||
"status": "ACCEPTED",
|
||||
"direction": "OUTBOUND",
|
||||
"created": "2024-06-24T20:15:48.062Z",
|
||||
"favorite": false
|
||||
}
|
||||
]
|
||||
@@ -98,6 +98,15 @@
|
||||
"note": "",
|
||||
"favorite": false,
|
||||
"created": "2024-05-31T19:50:04.738Z"
|
||||
},
|
||||
{
|
||||
"accountId": "Player724",
|
||||
"groups": [],
|
||||
"mutual": 0,
|
||||
"alias": "",
|
||||
"note": "",
|
||||
"favorite": false,
|
||||
"created": "2024-06-24T20:15:48.062Z"
|
||||
}
|
||||
],
|
||||
"incoming": [],
|
||||
|
||||
@@ -10,6 +10,7 @@ export 'package:reboot_common/src/model/server_result.dart';
|
||||
export 'package:reboot_common/src/model/server_type.dart';
|
||||
export 'package:reboot_common/src/model/update_status.dart';
|
||||
export 'package:reboot_common/src/model/update_timer.dart';
|
||||
export 'package:reboot_common/src/model/fortnite_server.dart';
|
||||
export 'package:reboot_common/src/model/dll.dart';
|
||||
export 'package:reboot_common/src/util/backend.dart';
|
||||
export 'package:reboot_common/src/util/build.dart';
|
||||
@@ -18,3 +19,4 @@ export 'package:reboot_common/src/util/network.dart';
|
||||
export 'package:reboot_common/src/util/patcher.dart';
|
||||
export 'package:reboot_common/src/util/path.dart';
|
||||
export 'package:reboot_common/src/util/process.dart';
|
||||
export 'package:reboot_common/src/util/log.dart';
|
||||
@@ -1,2 +1,3 @@
|
||||
const String kDefaultBackendHost = "127.0.0.1";
|
||||
const int kDefaultBackendPort = 3551;
|
||||
const int kDefaultXmppPort = 80;
|
||||
@@ -5,9 +5,9 @@ import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
extension FortniteVersionExtension on FortniteVersion {
|
||||
static DateTime _marker = DateTime.fromMicrosecondsSinceEpoch(0);
|
||||
static String _marker = "FortniteClient.mod";
|
||||
|
||||
static File? findExecutable(Directory directory, String name) {
|
||||
static File? findFile(Directory directory, String name) {
|
||||
try{
|
||||
final result = directory.listSync(recursive: true)
|
||||
.firstWhere((element) => path.basename(element.path) == name);
|
||||
@@ -18,39 +18,24 @@ extension FortniteVersionExtension on FortniteVersion {
|
||||
}
|
||||
|
||||
Future<File?> get shippingExecutable async {
|
||||
final result = findExecutable(location, "FortniteClient-Win64-Shipping.exe");
|
||||
final result = findFile(location, "FortniteClient-Win64-Shipping.exe");
|
||||
if(result == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final lastModified = await _getLastModifiedTime(result);
|
||||
if(lastModified != _marker) {
|
||||
await Isolate.run(() => patchHeadless(result));
|
||||
await _setLastModifiedTime(result);
|
||||
final marker = findFile(location, _marker);
|
||||
if(marker != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
await Isolate.run(() => patchHeadless(result));
|
||||
await File("${location.path}\\$_marker").create();
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> _setLastModifiedTime(File result) async {
|
||||
try {
|
||||
await result.setLastModified(_marker);
|
||||
}catch(_) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
File? get launcherExecutable => findFile(location, "FortniteLauncher.exe");
|
||||
|
||||
Future<DateTime?> _getLastModifiedTime(File result) async {
|
||||
try {
|
||||
return await result.lastModified();
|
||||
}catch(_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
File? get eacExecutable => findFile(location, "FortniteClient-Win64-Shipping_EAC.exe");
|
||||
|
||||
File? get launcherExecutable => findExecutable(location, "FortniteLauncher.exe");
|
||||
|
||||
File? get eacExecutable => findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");
|
||||
|
||||
File? get splashBitmap => findExecutable(location, "Splash.bmp");
|
||||
File? get splashBitmap => findFile(location, "Splash.bmp");
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:version/version.dart';
|
||||
|
||||
class FortniteBuild {
|
||||
final String identifier;
|
||||
final String version;
|
||||
final Version version;
|
||||
final String link;
|
||||
final bool available;
|
||||
|
||||
FortniteBuild({
|
||||
required this.identifier,
|
||||
required this.version,
|
||||
required this.link
|
||||
required this.link,
|
||||
required this.available
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
47
common/lib/src/model/fortnite_server.dart
Normal file
47
common/lib/src/model/fortnite_server.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
class FortniteServer {
|
||||
final String id;
|
||||
final String name;
|
||||
final String description;
|
||||
final String author;
|
||||
final String ip;
|
||||
final String version;
|
||||
final String? password;
|
||||
final DateTime timestamp;
|
||||
final bool discoverable;
|
||||
|
||||
FortniteServer({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.author,
|
||||
required this.ip,
|
||||
required this.version,
|
||||
required this.password,
|
||||
required this.timestamp,
|
||||
required this.discoverable
|
||||
});
|
||||
|
||||
factory FortniteServer.fromJson(json) => FortniteServer(
|
||||
id: json["id"],
|
||||
name: json["name"],
|
||||
description: json["description"],
|
||||
author: json["author"],
|
||||
ip: json["ip"],
|
||||
version: json["version"],
|
||||
password: json["password"],
|
||||
timestamp: json.containsKey("json") ? DateTime.parse(json["timestamp"]) : DateTime.now(),
|
||||
discoverable: json["discoverable"] ?? false
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"author": author,
|
||||
"ip": ip,
|
||||
"version": version,
|
||||
"password": password,
|
||||
"timestamp": timestamp.toString(),
|
||||
"discoverable": discoverable
|
||||
};
|
||||
}
|
||||
@@ -1,17 +1,22 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:version/version.dart';
|
||||
|
||||
class FortniteVersion {
|
||||
String name;
|
||||
Version content;
|
||||
Directory location;
|
||||
|
||||
FortniteVersion.fromJson(json)
|
||||
: name = json["name"],
|
||||
: content = Version.parse(json["content"]),
|
||||
location = Directory(json["location"]);
|
||||
|
||||
FortniteVersion({required this.name, required this.location});
|
||||
FortniteVersion({required this.content, required this.location});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'name': name,
|
||||
'content': content.toString(),
|
||||
'location': location.path
|
||||
};
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is FortniteVersion && this.content == other.content;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ class GameInstance {
|
||||
bool launched;
|
||||
bool movedToVirtualDesktop;
|
||||
bool tokenError;
|
||||
bool killed;
|
||||
GameInstance? child;
|
||||
|
||||
GameInstance({
|
||||
@@ -22,9 +23,19 @@ class GameInstance {
|
||||
required this.eacPid,
|
||||
required this.serverType,
|
||||
required this.child
|
||||
}): tokenError = false, launched = false, movedToVirtualDesktop = false, injectedDlls = [];
|
||||
}): tokenError = false, killed = false, launched = false, movedToVirtualDesktop = false, injectedDlls = [];
|
||||
|
||||
void kill() {
|
||||
GameInstance? child = this;
|
||||
while(child != null) {
|
||||
child._kill();
|
||||
child = child.child;
|
||||
}
|
||||
}
|
||||
|
||||
void _kill() {
|
||||
launched = true;
|
||||
killed = true;
|
||||
Process.killPid(gamePid, ProcessSignal.sigabrt);
|
||||
if(launcherPid != null) {
|
||||
Process.killPid(launcherPid!, ProcessSignal.sigabrt);
|
||||
@@ -33,19 +44,6 @@ class GameInstance {
|
||||
Process.killPid(eacPid!, ProcessSignal.sigabrt);
|
||||
}
|
||||
}
|
||||
|
||||
bool get nestedHosting {
|
||||
GameInstance? child = this;
|
||||
while(child != null) {
|
||||
if(child.serverType != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
child = child.child;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
enum GameServerType {
|
||||
|
||||
@@ -4,6 +4,11 @@ class ServerResult {
|
||||
final StackTrace? stackTrace;
|
||||
|
||||
ServerResult(this.type, {this.error, this.stackTrace});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ServerResult{type: $type, error: $error, stackTrace: $stackTrace}';
|
||||
}
|
||||
}
|
||||
|
||||
enum ServerResultType {
|
||||
@@ -21,7 +26,8 @@ enum ServerResultType {
|
||||
freePortError,
|
||||
pingingRemote,
|
||||
pingingLocal,
|
||||
pingError;
|
||||
pingError,
|
||||
processError;
|
||||
|
||||
bool get isError => name.contains("Error");
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ Future<bool> isBackendPortFree() async => await pingBackend(kDefaultBackendHost,
|
||||
|
||||
Future<bool> freeBackendPort() async {
|
||||
await killProcessByPort(kDefaultBackendPort);
|
||||
await killProcessByPort(kDefaultXmppPort);
|
||||
final standardResult = await isBackendPortFree();
|
||||
if(standardResult) {
|
||||
return true;
|
||||
@@ -35,21 +36,24 @@ Future<bool> freeBackendPort() async {
|
||||
}
|
||||
|
||||
Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
|
||||
var hostName = host.replaceFirst("http://", "").replaceFirst("https://", "");
|
||||
var declaredScheme = host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
||||
final hostName = host.replaceFirst("http://", "").replaceFirst("https://", "");
|
||||
final declaredScheme = host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
||||
try{
|
||||
var uri = Uri(
|
||||
final uri = Uri(
|
||||
scheme: declaredScheme ?? (https ? "https" : "http"),
|
||||
host: hostName,
|
||||
port: port,
|
||||
path: "unknown"
|
||||
);
|
||||
var client = HttpClient()
|
||||
..connectionTimeout = const Duration(seconds: 5);
|
||||
var request = await client.getUrl(uri);
|
||||
var response = await request.close();
|
||||
return response.statusCode == 200 || response.statusCode == 404 ? uri : null;
|
||||
}catch(_){
|
||||
log("[BACKEND] Pinging $uri...");
|
||||
final client = HttpClient()
|
||||
..connectionTimeout = const Duration(seconds: 10);
|
||||
final request = await client.getUrl(uri);
|
||||
await request.close().timeout(const Duration(seconds: 10));
|
||||
log("[BACKEND] Ping successful");
|
||||
return uri;
|
||||
}catch(error){
|
||||
log("[BACKEND] Cannot ping backend: $error");
|
||||
return https || declaredScheme != null || isLocalHost(host) ? null : await pingBackend(host, port, true);
|
||||
}
|
||||
}
|
||||
@@ -59,16 +63,16 @@ Stream<String?> watchMatchmakingIp() async* {
|
||||
return;
|
||||
}
|
||||
|
||||
var observer = matchmakerConfigFile.parent.watch(events: FileSystemEvent.modify);
|
||||
final observer = matchmakerConfigFile.parent.watch(events: FileSystemEvent.modify);
|
||||
yield* observer.where((event) => event.path == matchmakerConfigFile.path).asyncMap((event) async {
|
||||
try {
|
||||
var config = Config.fromString(await matchmakerConfigFile.readAsString());
|
||||
var ip = config.get("GameServer", "ip");
|
||||
final config = Config.fromString(await matchmakerConfigFile.readAsString());
|
||||
final ip = config.get("GameServer", "ip");
|
||||
if(ip == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var port = config.get("GameServer", "port");
|
||||
final port = config.get("GameServer", "port");
|
||||
if(port == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -89,14 +93,14 @@ Stream<String?> watchMatchmakingIp() async* {
|
||||
}
|
||||
|
||||
Future<void> writeMatchmakingIp(String text) async {
|
||||
var exists = await matchmakerConfigFile.exists();
|
||||
final exists = await matchmakerConfigFile.exists();
|
||||
if(!exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
_semaphore.acquire();
|
||||
var splitIndex = text.indexOf(":");
|
||||
var ip = splitIndex != -1 ? text.substring(0, splitIndex) : text;
|
||||
final splitIndex = text.indexOf(":");
|
||||
final ip = splitIndex != -1 ? text.substring(0, splitIndex) : text;
|
||||
var port = splitIndex != -1 ? text.substring(splitIndex + 1) : kDefaultGameServerPort;
|
||||
if(port.isBlank) {
|
||||
port = kDefaultGameServerPort;
|
||||
@@ -104,7 +108,7 @@ Future<void> writeMatchmakingIp(String text) async {
|
||||
|
||||
_lastIp = ip;
|
||||
_lastPort = port;
|
||||
var config = Config.fromString(await matchmakerConfigFile.readAsString());
|
||||
final config = Config.fromString(await matchmakerConfigFile.readAsString());
|
||||
config.set("GameServer", "ip", ip);
|
||||
config.set("GameServer", "port", port);
|
||||
await matchmakerConfigFile.writeAsString(config.toString(), flush: true);
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:dio/io.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_common/src/extension/types.dart';
|
||||
import 'package:version/version.dart';
|
||||
|
||||
const String kStopBuildDownloadSignal = "kill";
|
||||
|
||||
@@ -23,7 +24,7 @@ Dio _buildDioInstance() {
|
||||
return dio;
|
||||
}
|
||||
|
||||
final String _archiveSourceUrl = "http://185.203.216.3/versions.json";
|
||||
final String _archiveSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md";
|
||||
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
|
||||
const String _deniedConnectionError = "The connection was denied: your firewall might be blocking the download";
|
||||
const String _unavailableError = "The build downloader is not available right now";
|
||||
@@ -41,15 +42,35 @@ Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
||||
return [];
|
||||
}
|
||||
|
||||
final data = jsonDecode(response.data ?? "{}");
|
||||
var results = <FortniteBuild>[];
|
||||
for(final entry in data.entries) {
|
||||
results.add(FortniteBuild(
|
||||
identifier: entry.key,
|
||||
version: "${entry.value["title"]} (${entry.key})",
|
||||
link: entry.value["url"]
|
||||
));
|
||||
for (final line in response.data?.split("\n") ?? []) {
|
||||
if (!line.startsWith("|")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var parts = line.substring(1, line.length - 1).split("|");
|
||||
if (parts.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var versionName = parts.first.trim();
|
||||
final separator = versionName.indexOf("-");
|
||||
if(separator != -1) {
|
||||
versionName = versionName.substring(0, separator);
|
||||
}
|
||||
|
||||
final link = parts.last.trim();
|
||||
try {
|
||||
results.add(FortniteBuild(
|
||||
version: Version.parse(versionName),
|
||||
link: link,
|
||||
available: link.endsWith(".zip") || link.endsWith(".rar")
|
||||
));
|
||||
} on FormatException {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:io';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
|
||||
final File _loggingFile = _createLoggingFile();
|
||||
final File launcherLogFile = _createLoggingFile();
|
||||
final Semaphore _semaphore = Semaphore(1);
|
||||
|
||||
File _createLoggingFile() {
|
||||
@@ -20,9 +20,9 @@ void log(String message) async {
|
||||
try {
|
||||
await _semaphore.acquire();
|
||||
print(message);
|
||||
await _loggingFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
|
||||
await launcherLogFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
|
||||
}catch(error) {
|
||||
print(error);
|
||||
print("[LOGGER_ERROR] An error occurred while logging: $error");
|
||||
}finally {
|
||||
_semaphore.release();
|
||||
}
|
||||
@@ -6,7 +6,7 @@ Directory get installationDirectory =>
|
||||
Directory get dllsDirectory => Directory("${installationDirectory.path}\\dlls");
|
||||
|
||||
Directory get assetsDirectory {
|
||||
var directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
|
||||
final directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
|
||||
if(directory.existsSync()) {
|
||||
return directory;
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ Future<bool> startElevatedProcess({required String executable, required String a
|
||||
return shellResult == 1;
|
||||
}
|
||||
|
||||
Future<Process> startProcess({required File executable, List<String>? args, bool useTempBatch = true, bool window = false, String? name}) async {
|
||||
Future<Process> startProcess({required File executable, List<String>? args, bool useTempBatch = true, bool window = false, String? name, Map<String, String>? environment}) async {
|
||||
final argsOrEmpty = args ?? [];
|
||||
if(useTempBatch) {
|
||||
final tempScriptDirectory = await tempDirectory.createTemp("reboot_launcher_process");
|
||||
@@ -115,6 +115,7 @@ Future<Process> startProcess({required File executable, List<String>? args, bool
|
||||
tempScriptFile.path,
|
||||
[],
|
||||
workingDirectory: executable.parent.path,
|
||||
environment: environment,
|
||||
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
|
||||
runInShell: window
|
||||
);
|
||||
@@ -202,6 +203,7 @@ Future<bool> watchProcess(int pid) async {
|
||||
return await completer.future;
|
||||
}
|
||||
|
||||
// TODO: Template
|
||||
List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool log, String additionalArgs) {
|
||||
if(password.isEmpty) {
|
||||
username = '${_parseUsername(username, host)}@projectreboot.dev';
|
||||
|
||||
@@ -19,6 +19,7 @@ dependencies:
|
||||
sync: ^0.3.0
|
||||
uuid: ^3.0.6
|
||||
shelf_web_socket: ^2.0.0
|
||||
version: ^3.0.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^2.0.1
|
||||
Binary file not shown.
@@ -1,3 +0,0 @@
|
||||
Project Reboot is a game server for Fortnite that aims to support as many seasons as possible.
|
||||
The project was started on Discord by Milxnor, while the launcher is developed by Auties00.
|
||||
Both are open source on GitHub, anyone can easily contribute or audit the code!"
|
||||
@@ -1 +0,0 @@
|
||||
Your version of Fortnite is corrupted, download it again from the launcher, or use another build.
|
||||
@@ -1,3 +0,0 @@
|
||||
Support for LawinV2 is available in the launcher.
|
||||
To use the backend, select a local or remote backend from the "Backend" tab in the launcher.
|
||||
To use the credentials, click on the avatar on the top left of the launcher and enter your email and password
|
||||
@@ -1,2 +0,0 @@
|
||||
No, skins don't work in Reboot.
|
||||
This is because Epic asked us to remove them.
|
||||
@@ -1,7 +0,0 @@
|
||||
If you have ever played Minecraft multiplayer, you might know that the servers you join are hosted on a computer running a program, called Minecraft Game Server.
|
||||
While the Minecraft Game server is written by the creators of Minecraft, Mojang, Epic Games doesn't provide an equivalent for Fortnite.
|
||||
By exploiting the Fortnite internals, though, it's possible to create a game server just like in Minecraft: this is in easy terms what Project Reboot does.
|
||||
Some Fortnite versions support running this game server in the background without rendering the game("headless"), while others still require the full game to be open.
|
||||
Just like in Minecraft, you need a game client to play the game and one to host the server.
|
||||
By default, a game server is automatically started on your PC when you start a Fortnite version from the "Play" section in the launcher.
|
||||
If you want to play in another way, for example by joining a server hosted by one of your friends instead of running one yourself, you can checkout the "Multiplayer" section in the "Play" tab of the launcher.
|
||||
@@ -1,4 +0,0 @@
|
||||
Some Fortnite versions support running this game server in the background without rendering the game: this type of server is called "headless" as the game is running, but you can't see it on your screen.
|
||||
If headless is not supported by the Fortnite version you want to play, or if you disabled it manually from the "Configuration" section in the "Host" tab of the launcher, you will see an instance of Fortnite open on your screen.
|
||||
For convenience, this window will be opened on a new Virtual Desktop, if your Windows version supports it. This feature can be disabled as well from from the "Configuration" section in the "Host" tab of the launcher.
|
||||
Just like in Minecraft, you need a game client to play the game and one to host the server.
|
||||
@@ -1,22 +0,0 @@
|
||||
For others to join your game server, port 7777 must be accessible on your PC.
|
||||
One option is to use a private VPN service like Hamachi or Radmin, but all of the players will need to download this software.
|
||||
The best solution is to use port forwarding:
|
||||
1. Set a static IP
|
||||
If you don't have already a static IP set, set one by following any tutorial on Google
|
||||
2. Log into your router's admin panel
|
||||
Usually this can be accessed on any web browser by going to http://192.168.1.1/
|
||||
You might need a username and a password to log in: refer to your router's manual for precise instructions
|
||||
3. Find the port forwarding section
|
||||
Once logged in into the admin panel, navigate to the port forwarding section of your router's settings
|
||||
This location may vary from router to router, but it's typically labelled as "Port Forwarding," "Port Mapping" or "Virtual Server"
|
||||
Refer to your router's manual for precise instructions
|
||||
4. Add a port forwarding rule
|
||||
Now, you'll need to create a new port forwarding rule. Here's what you'll typically need to specify:
|
||||
- Service Name: Choose a name for your port forwarding rule (e.g., "Fortnite Game Server")
|
||||
- Port Number: Enter 7777 for both the external and internal ports
|
||||
- Protocol: Select the UDP protocol
|
||||
- Internal IP Address: Enter the static IP address you set earlier
|
||||
- Enable: Make sure the port forwarding rule is enabled
|
||||
5. Save and apply the changes
|
||||
After configuring the port forwarding rule, save your changes and apply them
|
||||
This step may involve clicking a "Save" or "Apply" button on your router's web interface
|
||||
@@ -1,6 +0,0 @@
|
||||
A backend is a piece of software that emulates the Epic Games server responsible for authentication and related features.
|
||||
By default, the Reboot Launcher ships with a slightly customized version of LawinV1, an open source implementation available on Github.
|
||||
If you are having any problems with the built in backend, enable the "Detached" option in the "Backend" tab of the Reboot Laucher to troubleshoot the issue.
|
||||
LawinV1 was chosen to allow users to log into Fortnite and join games easily, but keep in mind that if you want to use features such as parties, voice chat or skins, you will need to use a custom backend.
|
||||
Other popular options are LawinV2 and Momentum, both available on Github, but it's not recommended to use them if you are not an advanced user.
|
||||
You can run these alternatives either on your PC or on a server by selecting respectively "Local" or "Remote" from the "Type" section in the "Backend" tab of the Reboot Launcher.
|
||||
@@ -1,4 +0,0 @@
|
||||
Many Fortnite versions don't support entering in game by clicking the \"Play\" button.
|
||||
Instead, you need to click the key assigned to the Unreal Engine console, by default F8 or the tilde(the button above tab), and type open 127.0.0.1
|
||||
Keep in mind that the Unreal Engine console key is controlled by the backend, so this is true only if you are using the embedded backend: custom backends might use different keys.
|
||||
When using the embedded backend, you can customize the key used to open the console in the \"Backend\" tab of the Reboot Launcher.
|
||||
@@ -1,4 +0,0 @@
|
||||
To resolve this issue:
|
||||
- Check that your backend is working correctly from the "Backend" tab
|
||||
- If you are using a custom backend, try to use the embedded one
|
||||
- Try to run the backend as detached by enabling the "Detached" option in the "Backend" tab
|
||||
@@ -1,2 +0,0 @@
|
||||
As explained in the "What is a Fortnite game server?" section, one instance of Fortnite is used to host the game server, while the other is used to let you play.
|
||||
The Fortnite instance used up by the game server is usually frozen, so it should be hard to use the wrong one to try to play.
|
||||
@@ -1,3 +0,0 @@
|
||||
As explained in the "What is the Unreal Engine console?" section, the "Play" button doesn't work in many Fortnite versions.
|
||||
Instead, you need to click the key assigned to the Unreal Engine console, by default F8 or the tilde(the button above tab), and type open 127.0.0.1
|
||||
If that doesn't work, go to the Host page and make sure that you are running a game server by clicking "Start Hosting".
|
||||
@@ -1,3 +0,0 @@
|
||||
A Fortnite game server created by Milxnor
|
||||
A Minecraft game server created by Chief Keef
|
||||
I don't know
|
||||
@@ -1,3 +0,0 @@
|
||||
A version I downloaded from the launcher crashes
|
||||
A version of Fortnite I downloaded from the internet crashes
|
||||
I can't enter in game from the lobby
|
||||
@@ -1,3 +0,0 @@
|
||||
Click the Unreal Engine Key(F8 or the tilde by default) and type open 127.0.0.1
|
||||
Report a bug on Discord because there is a clearly a bug
|
||||
Cry
|
||||
@@ -1,3 +0,0 @@
|
||||
Switch my backend to embedded and try again before reporting a bug on Discord
|
||||
Report a bug on Discord immediately and flood the chat
|
||||
Cry
|
||||
@@ -1,3 +0,0 @@
|
||||
No, it's not possible
|
||||
Yes, I just have to send Auties a message
|
||||
Yes, let me ask on Discord how to get them
|
||||
@@ -1,3 +0,0 @@
|
||||
Only the ones I can download from the launcher
|
||||
Any season between season 0 and 15 works
|
||||
Depends on the day of the week
|
||||
@@ -1,3 +0,0 @@
|
||||
The address of your local machine where the game server will be hosted
|
||||
Playboi Carti's hiding spot
|
||||
I don't know
|
||||
@@ -1,3 +0,0 @@
|
||||
A Fortnite window used to host matches instead of playing the game
|
||||
A standalone piece of software to host matches
|
||||
A stolen Epic games server we got by paying Tim Apple
|
||||
@@ -1,3 +0,0 @@
|
||||
No, I will be kicked back to the lobby, unless I've joined someone else's server
|
||||
Yes, one will be started automatically as soon as I click play
|
||||
Yes, it's not needed to play
|
||||
@@ -1,3 +0,0 @@
|
||||
A game server that doesn't render the Fortnite window to save memory
|
||||
A game server hosted on someone else's PC
|
||||
A game server running in Milxnor's mind
|
||||
@@ -1,3 +0,0 @@
|
||||
Some seasons don't support a headless server, so the other frozen Fortnite is the game server
|
||||
The launcher is bugged: I must report a bug on Discord
|
||||
Auties hardcoded a crypto miner in Fortnite: by opening two games he gets more cash
|
||||
@@ -1,3 +0,0 @@
|
||||
I can open port 7777 on my router or use a private VPN service
|
||||
I don't have to do anything, it's all automatic
|
||||
What are you talking about?
|
||||
@@ -1,3 +0,0 @@
|
||||
A piece of software to emulate Epic Games' servers for authentication
|
||||
A piece of software that makes the launcher work correctly
|
||||
I don't know
|
||||
@@ -129,7 +129,7 @@
|
||||
"importVersionDescription": "Import a new version of Fortnite into the launcher",
|
||||
"addLocalBuildName": "Add a version from this PC's local storage",
|
||||
"addLocalBuildDescription": "Versions coming from your local disk are not guaranteed to work",
|
||||
"addLocalBuildContent": "Add local build",
|
||||
"addVersion": "Add version",
|
||||
"downloadBuildName": "Download any version from the cloud",
|
||||
"downloadBuildDescription": "Download any Fortnite build easily from the cloud",
|
||||
"downloadBuildContent": "Download build",
|
||||
@@ -151,8 +151,10 @@
|
||||
"downloadDllError": "An error occurred while downloading {name}: {error}",
|
||||
"downloadDllRetry": "Retry",
|
||||
"uncaughtErrorMessage": "An uncaught error was thrown: {error}",
|
||||
"launchingHeadlessServer": "Launching the game server...",
|
||||
"launchingGameClient": "Launching the game client...",
|
||||
"launchingGameServer": "Launching the game server...",
|
||||
"launchingGameClientOnly": "Launching the game client without a server...",
|
||||
"launchingGameClientAndServer": "Launching the game client and server...",
|
||||
"startGameServer": "Start a game server",
|
||||
"usernameOrEmail": "Username/Email",
|
||||
"usernameOrEmailPlaceholder": "Type your username or email",
|
||||
"password": "Password",
|
||||
@@ -167,16 +169,16 @@
|
||||
"stoppingServer": "Stopping the backend...",
|
||||
"stoppedServer": "The backend was stopped successfully",
|
||||
"stopServerError": "An error occurred while stopping the backend: {error}",
|
||||
"missingHostNameError": "Missing hostname in the {name} configuration",
|
||||
"missingHostNameError": "Missing hostname in the backend configuration",
|
||||
"missingPortError": "Missing port in the backend configuration",
|
||||
"illegalPortError": "Invalid port in the backend configuration",
|
||||
"freeingPort": "Freeing the backend port...",
|
||||
"freedPort": "The backend port was freed successfully",
|
||||
"freePortError": "An error occurred while freeing the backend port: {error}",
|
||||
"pingingRemoteServer": "Pinging the remote backend...",
|
||||
"pingingLocalServer": "Pinging the {type} backend...",
|
||||
"pingingServer": "Pinging the {type} backend...",
|
||||
"pingError": "Cannot ping the {type} backend",
|
||||
"joinSelfServer": "You can't join your own server",
|
||||
"cannotJoinServerVersion": "You can't join this server: download Fortnite {version}",
|
||||
"wrongServerPassword": "Wrong password: please try again",
|
||||
"offlineServer": "This server isn't online right now: please try again later",
|
||||
"serverPassword": "Password",
|
||||
@@ -192,12 +194,12 @@
|
||||
"deleteVersionCancel": "Keep",
|
||||
"deleteVersionConfirm": "Delete",
|
||||
"versionName": "Name",
|
||||
"versionNameLabel": "Type the new version name",
|
||||
"versionNameLabel": "Type the version name",
|
||||
"newVersionNameConfirm": "Save",
|
||||
"newVersionNameLabel": "Type the new version name",
|
||||
"gameFolderTitle": "Game folder",
|
||||
"gameFolderPlaceholder": "Type the new game folder",
|
||||
"gameFolderPlaceWindowTitle": "Select game folder",
|
||||
"newVersionNameLabel": "Type the version name",
|
||||
"gameFolderTitle": "Game directory",
|
||||
"gameFolderPlaceholder": "Type the game directory",
|
||||
"gameFolderPlaceWindowTitle": "Select game directory",
|
||||
"gameFolderLabel": "Path",
|
||||
"openInExplorer": "Open in explorer",
|
||||
"modify": "Modify",
|
||||
@@ -220,7 +222,7 @@
|
||||
"buildInstallationDirectoryWindowTitle": "Select installation directory",
|
||||
"timeLeft": "Time left: {timeLeft, plural, =0{less than a minute} =1{about {timeLeft} minute} other{about {timeLeft} minutes}}",
|
||||
"localBuildsWarning": "Local builds are not guaranteed to work",
|
||||
"saveLocalVersion": "Save",
|
||||
"saveLocalVersion": "Add",
|
||||
"embedded": "Embedded",
|
||||
"remote": "Remote",
|
||||
"local": "Local",
|
||||
@@ -244,7 +246,7 @@
|
||||
"versionAlreadyExists": "This version already exists",
|
||||
"emptyGamePath": "Empty game path",
|
||||
"directoryDoesNotExist": "Directory doesn't exist",
|
||||
"missingShippingExe": "Invalid game path: missing FortniteClient-Win64-Shipping",
|
||||
"missingShippingExe": "Invalid game path: missing Fortnite executable",
|
||||
"invalidDownloadPath": "Invalid download path",
|
||||
"invalidDllPath": "Invalid dll path",
|
||||
"dllDoesNotExist": "The file doesn't exist",
|
||||
@@ -256,9 +258,9 @@
|
||||
"missingExecutableError": "Missing Fortnite executable: usually this means that the installation was moved or deleted",
|
||||
"corruptedVersionError": "Corrupted Fortnite installation: please download it again from the launcher or change version",
|
||||
"corruptedDllError": "Cannot inject dll: {error}",
|
||||
"missingCustomDllError": "The custom {dll}.dll doesn't exist: check your settings",
|
||||
"tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})",
|
||||
"unknownFortniteError": "An unknown error occurred while launching Fortnite: {error}",
|
||||
"serverNoLongerAvailable": "{owner}'s server is no longer available",
|
||||
"serverNoLongerAvailableUnnamed": "The previous server is no longer available",
|
||||
"noServerFound": "No server found: invalid or expired link",
|
||||
"settingsUtilsThemeName": "Theme",
|
||||
@@ -268,8 +270,6 @@
|
||||
"system": "System",
|
||||
"settingsUtilsLanguageName": "Language",
|
||||
"settingsUtilsLanguageDescription": "Select the language to use inside the launcher",
|
||||
"playAutomaticServerName": "Embedded game server",
|
||||
"playAutomaticServerDescription": "Whether a game server should be started automatically if none was configured",
|
||||
"infoDocumentationName": "Documentation",
|
||||
"infoDocumentationDescription": "Read some tutorials on how to use Reboot",
|
||||
"infoDocumentationContent": "Open GitHub",
|
||||
@@ -277,8 +277,8 @@
|
||||
"infoDiscordDescription": "Join the discord server to receive help",
|
||||
"infoDiscordContent": "Open Discord",
|
||||
"infoVideoName": "Tutorial",
|
||||
"infoVideoDescription": "Watch a tutorial to understand how to use the launcher",
|
||||
"infoVideoContent": "Open YouTube",
|
||||
"infoVideoDescription": "Show the tutorial again in the launcher",
|
||||
"infoVideoContent": "Start Tutorial",
|
||||
"dllDeletedTitle": "A critical dll was deleted. If you didn't delete it, your Antivirus probably flagged it. This is a false positive: please disable your Antivirus and try again",
|
||||
"dllDeletedSecondaryAction": "Close",
|
||||
"dllDeletedPrimaryAction": "Try again",
|
||||
@@ -307,5 +307,61 @@
|
||||
"gameServerTypeDescription": "The type of game server to use",
|
||||
"gameServerTypeHeadless": "Background process",
|
||||
"gameServerTypeVirtualWindow": "Virtual window",
|
||||
"gameServerTypeWindow": "Normal window"
|
||||
"gameServerTypeWindow": "Normal window",
|
||||
"localBuild": "This PC",
|
||||
"githubArchive": "Cloud archive",
|
||||
"all": "All",
|
||||
"accessible": "Accessible",
|
||||
"playable": "Playable",
|
||||
"timeDescending": "Time (from newest to oldest)",
|
||||
"timeAscending": "Time (from oldest to newest)",
|
||||
"nameAscending": "Name (from A to Z)",
|
||||
"nameDescending": "Name (from Z to A)",
|
||||
"none": "none",
|
||||
"openLog": "Open log",
|
||||
"backendProcessError": "The backend shut down unexpectedly",
|
||||
"welcomeTitle": "Welcome to Reboot Launcher",
|
||||
"welcomeDescription": "If you have never used a Fortnite game server, or this launcher in particular, please click on take a tour\nPlease don't ask for support on Discord without taking the tour: this helps me prioritize real bugs\nYou can always take the tour again in the Info tab",
|
||||
"welcomeAction": "Take the tour",
|
||||
"startOnboardingText": "Start by choosing a username: this will be visible to other players on Fortnite.\nIf you are advanced user, you can set the email and password here if the backend\nyou are using supports authentication.",
|
||||
"startOnboardingActionLabel": "Let's do it",
|
||||
"promptPlayPageText": "The Play tab is used to launch the version of Fortnite you want.\nBefore playing, you'll need to host or join a game server.\nYou will learn how to later.",
|
||||
"promptPlayPageActionLabel": "Next",
|
||||
"promptPlayVersionText": "Here you can download or import any Fortnite version\nAdd at least one to start using the launcher",
|
||||
"promptPlayVersionActionLabelHasBuilds": "Next",
|
||||
"promptPlayVersionActionLabelNoBuilds": "Let's do it",
|
||||
"promptServerBrowserPageText": "The Server Browser tab is used to find game servers hosted by other players\nServers can be free to join or password protected based on the settings set by the owner",
|
||||
"promptServerBrowserPageActionLabel": "Next",
|
||||
"promptHostPageText": "The Host tab is used to host a game server.\nWhen you usually play Fortnite, you connect to an Epic Games' game server.\nTo play using Reboot, you'll need to host the game server yourself, or join someone else's.\nOtherwise, you will be sent back to the lobby when trying to join a game.",
|
||||
"promptHostPageActionLabel": "Next",
|
||||
"promptHostInfoText": "This section is used to provide information about your game server for the Server Browser\nIf you don't want other players to join your server, you can skip this part",
|
||||
"promptHostInfoActionLabelSkip": "Skip",
|
||||
"promptHostInfoActionLabelConfigure": "Configure",
|
||||
"promptHostInformationText": "Choose the name for your server",
|
||||
"promptHostInformationActionLabel": "Next",
|
||||
"promptHostInformationDescriptionText": "Choose the description for your server",
|
||||
"promptHostInformationDescriptionActionLabel": "Next",
|
||||
"promptHostInformationPasswordText": "Set a password for your server, if you need one",
|
||||
"promptHostInformationPasswordActionLabel": "Next",
|
||||
"promptHostVersionText": "You can select the version of Fortnite to host here.\nThese are synchronized with the Play tab.",
|
||||
"promptHostVersionActionLabel": "Next",
|
||||
"promptHostShareText": "If you don't want to use the server browser, other players can join\nyou server by using your Reboot Launcher link or your public IP.",
|
||||
"promptHostShareActionLabel": "Next",
|
||||
"promptBackendPageText": "The Backend tab is used for authentication and queuing.\nWhen you usually play Fortnite, you connect to an Epic Games' backend.\nTo play using Reboot, you'll need to host the backend yourself, or join someone else's.\nIf the backend doesn't work correctly, an authentication error will be displayed.",
|
||||
"promptBackendPageActionLabel": "Next",
|
||||
"promptBackendTypePageText": "By default, an embedded LawinV1 backend is started.\nIf you want to run another backend on your PC, like\nLawinV2 or Momentum, select Local. If you want to join,\na backend on someone else's PC, select Remote.",
|
||||
"promptBackendTypePageActionLabel": "Next",
|
||||
"promptBackendGameServerAddressText": "When you are using an embedded backend, you can type\nhere the IP of the game server you want to join. When\nyou click Join in the Server Browser, this field will be\nautocompleted. If you are not using an embedded backend,\nyou will need to set the IP manually in your backend configuration.",
|
||||
"promptBackendGameServerAddressActionLabel": "Next",
|
||||
"promptBackendUnrealEngineKeyText": "For some Fortnite versions, the PLAY button doesn't work: when this happens,\nyou need to click this Key to open the Unreal Engine console and type: open IP.\nSo for example if you want to join your own server you can type: open 127.0.0.1.\nIf you don't know, 127.0.0.1 is the IP of your local machine. If you are not using\nthe embedded backend, you'll need to set the Unreal Engine key in its configuration.",
|
||||
"promptBackendUnrealEngineKeyActionLabel": "Next",
|
||||
"promptBackendDetachedText": "If you get an authentication error when trying to log into Fortnite,\nswitch to embedded backend and enable this option to debug the backend.\nIf you can't fix the error, report a bug on Discord.",
|
||||
"promptBackendDetachedActionLabel": "Next",
|
||||
"promptInfoTabText": "The Info tab contains useful links to report bugs and receive support",
|
||||
"promptInfoTabActionLabel": "Next",
|
||||
"promptSettingsTabText": "The Settings tab contains options to customize and reset the launcher",
|
||||
"promptSettingsTabActionLabel": "Done",
|
||||
"automaticGameServerDialogContent": "The launcher detected that you are not running a game server, but that your matchmaker is set to your local machine. If you don't want to join another player's server, you should start a game server. This is necessary to be able to play!",
|
||||
"automaticGameServerDialogIgnore": "Ignore",
|
||||
"automaticGameServerDialogStart": "Start server"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_acrylic/flutter_acrylic.dart';
|
||||
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
|
||||
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
||||
@@ -14,26 +12,18 @@ import 'package:local_notifier/local_notifier.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/build_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/error.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/error.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/info_page.dart';
|
||||
import 'package:reboot_launcher/src/util/log.dart';
|
||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:system_theme/system_theme.dart';
|
||||
import 'package:url_protocol/url_protocol.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
|
||||
const double kDefaultWindowWidth = 1164;
|
||||
const double kDefaultWindowHeight = 864;
|
||||
@@ -45,8 +35,8 @@ bool appWithNoStorage = false;
|
||||
void main() {
|
||||
log("[APP] Called");
|
||||
runZonedGuarded(
|
||||
() => _startApp(),
|
||||
(error, stack) => onError(error, stack, false),
|
||||
() => _startApp(),
|
||||
(error, stack) => onError(error, stack, false),
|
||||
zoneSpecification: ZoneSpecification(
|
||||
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
|
||||
)
|
||||
@@ -54,6 +44,7 @@ void main() {
|
||||
}
|
||||
|
||||
Future<void> _startApp() async {
|
||||
_overrideHttpCertificate();
|
||||
final errors = <Object>[];
|
||||
try {
|
||||
log("[APP] Starting application");
|
||||
@@ -72,11 +63,6 @@ Future<void> _startApp() async {
|
||||
errors.add(notificationsError);
|
||||
}
|
||||
|
||||
final tilesError = InfoPage.initInfoTiles();
|
||||
if(tilesError != null) {
|
||||
errors.add(tilesError);
|
||||
}
|
||||
|
||||
final versionError = await _initVersion();
|
||||
if(versionError != null) {
|
||||
errors.add(versionError);
|
||||
@@ -103,6 +89,18 @@ Future<void> _startApp() async {
|
||||
}
|
||||
}
|
||||
|
||||
class _MyHttpOverrides extends HttpOverrides {
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext? context){
|
||||
return super.createHttpClient(context)
|
||||
..badCertificateCallback = ((X509Certificate cert, String host, int port) => true);
|
||||
}
|
||||
}
|
||||
|
||||
void _overrideHttpCertificate() {
|
||||
HttpOverrides.global = _MyHttpOverrides(); // Not safe, but necessary
|
||||
}
|
||||
|
||||
Future<Object?> _initNotifications() async {
|
||||
try {
|
||||
await localNotifier.setup(
|
||||
@@ -174,11 +172,12 @@ void _initWindow() => doWhenWindowReady(() async {
|
||||
appWindow.alignment = Alignment.center;
|
||||
}
|
||||
|
||||
appWindow.minSize = const Size(kDefaultWindowWidth, kDefaultWindowHeight);
|
||||
if(isWin11) {
|
||||
await Window.setEffect(
|
||||
effect: WindowEffect.acrylic,
|
||||
color: Colors.transparent,
|
||||
dark: SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark
|
||||
dark: isDarkMode
|
||||
);
|
||||
}
|
||||
}catch(error, stackTrace) {
|
||||
@@ -191,11 +190,10 @@ void _initWindow() => doWhenWindowReady(() async {
|
||||
Future<List<Object>> _initStorage() async {
|
||||
final errors = <Object>[];
|
||||
try {
|
||||
await GetStorage("game", settingsDirectory.path).initStorage;
|
||||
await GetStorage("backend", settingsDirectory.path).initStorage;
|
||||
await GetStorage("update", settingsDirectory.path).initStorage;
|
||||
await GetStorage("settings", settingsDirectory.path).initStorage;
|
||||
await GetStorage("hosting", settingsDirectory.path).initStorage;
|
||||
await GetStorage("game_storage", settingsDirectory.path).initStorage;
|
||||
await GetStorage("backend_storage", settingsDirectory.path).initStorage;
|
||||
await GetStorage("settings_storage", settingsDirectory.path).initStorage;
|
||||
await GetStorage("hosting_storage", settingsDirectory.path).initStorage;
|
||||
}catch(error) {
|
||||
appWithNoStorage = true;
|
||||
errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage");
|
||||
@@ -214,19 +212,9 @@ Future<List<Object>> _initStorage() async {
|
||||
}
|
||||
|
||||
try {
|
||||
Get.put(BuildController());
|
||||
}catch(error) {
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
try {
|
||||
Get.put(HostingController());
|
||||
}catch(error) {
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
try {
|
||||
Get.put(UpdateController());
|
||||
final controller = HostingController();
|
||||
Get.put(controller);
|
||||
controller.discardServer();
|
||||
}catch(error) {
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ class BackendController extends GetxController {
|
||||
late final Rx<ServerType> type;
|
||||
late final TextEditingController gameServerAddress;
|
||||
late final FocusNode gameServerAddressFocusNode;
|
||||
late final RxnString gameServerOwner;
|
||||
late final RxBool started;
|
||||
late final RxBool detached;
|
||||
StreamSubscription? worker;
|
||||
@@ -22,7 +21,7 @@ class BackendController extends GetxController {
|
||||
HttpServer? remoteServer;
|
||||
|
||||
BackendController() {
|
||||
storage = appWithNoStorage ? null : GetStorage("backend");
|
||||
storage = appWithNoStorage ? null : GetStorage("backend_storage");
|
||||
started = RxBool(false);
|
||||
type = Rx(ServerType.values.elementAt(storage?.read("type") ?? 0));
|
||||
type.listen((value) {
|
||||
@@ -64,8 +63,10 @@ class BackendController extends GetxController {
|
||||
}
|
||||
});
|
||||
gameServerAddressFocusNode = FocusNode();
|
||||
gameServerOwner = RxnString(storage?.read("game_server_owner"));
|
||||
gameServerOwner.listen((value) => storage?.write("game_server_owner", value));
|
||||
}
|
||||
|
||||
void joinLocalhost() {
|
||||
gameServerAddress.text = kDefaultGameServerHost;
|
||||
}
|
||||
|
||||
void reset() async {
|
||||
@@ -147,12 +148,11 @@ class BackendController extends GetxController {
|
||||
switch(type()){
|
||||
case ServerType.embedded:
|
||||
final process = await startEmbeddedBackend(detached.value);
|
||||
final processPid = process.pid;
|
||||
watchProcess(processPid).then((value) {
|
||||
if(started()) {
|
||||
started.value = false;
|
||||
}
|
||||
});
|
||||
watchProcess(process.pid)
|
||||
.asStream()
|
||||
.asBroadcastStream()
|
||||
.where((_) => !started())
|
||||
.map((_) => ServerResult(ServerResultType.processError));
|
||||
break;
|
||||
case ServerType.remote:
|
||||
yield ServerResult(ServerResultType.pingingRemote);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
class BuildController extends GetxController {
|
||||
List<FortniteBuild>? _builds;
|
||||
Rxn<FortniteBuild> _selectedBuild;
|
||||
|
||||
BuildController() : _selectedBuild = Rxn();
|
||||
|
||||
List<FortniteBuild>? get builds => _builds;
|
||||
|
||||
FortniteBuild? get selectedBuild => _selectedBuild.value;
|
||||
|
||||
set selectedBuild(FortniteBuild? value) {
|
||||
_selectedBuild.value = value;
|
||||
}
|
||||
|
||||
set builds(List<FortniteBuild>? builds) {
|
||||
_builds = builds;
|
||||
_selectedBuild.value = builds?.firstOrNull;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ class GameController extends GetxController {
|
||||
late final Rx<PhysicalKeyboardKey> consoleKey;
|
||||
|
||||
GameController() {
|
||||
_storage = appWithNoStorage ? null : GetStorage("game");
|
||||
_storage = appWithNoStorage ? null : GetStorage("game_storage");
|
||||
Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]");
|
||||
final decodedVersions = decodedVersionsJson
|
||||
.map((entry) => FortniteVersion.fromJson(entry))
|
||||
@@ -33,8 +33,7 @@ class GameController extends GetxController {
|
||||
versions = Rx(decodedVersions);
|
||||
versions.listen((data) => _saveVersions());
|
||||
final decodedSelectedVersionName = _storage?.read("version");
|
||||
final decodedSelectedVersion = decodedVersions.firstWhereOrNull((
|
||||
element) => element.name == decodedSelectedVersionName);
|
||||
final decodedSelectedVersion = decodedVersions.firstWhereOrNull((element) => element.content.toString() == decodedSelectedVersionName);
|
||||
_selectedVersion = Rxn(decodedSelectedVersion);
|
||||
username = TextEditingController(
|
||||
text: _storage?.read("username") ?? kDefaultPlayerName);
|
||||
@@ -88,7 +87,7 @@ class GameController extends GetxController {
|
||||
}
|
||||
|
||||
FortniteVersion? getVersionByName(String name) {
|
||||
return versions.value.firstWhereOrNull((element) => element.name == name);
|
||||
return versions.value.firstWhereOrNull((element) => element.content.toString() == name);
|
||||
}
|
||||
|
||||
void addVersion(FortniteVersion version) {
|
||||
@@ -99,15 +98,9 @@ class GameController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
FortniteVersion removeVersionByName(String versionName) {
|
||||
var version = versions.value.firstWhere((element) => element.name == versionName);
|
||||
removeVersion(version);
|
||||
return version;
|
||||
}
|
||||
|
||||
void removeVersion(FortniteVersion version) {
|
||||
versions.update((val) => val?.remove(version));
|
||||
if (selectedVersion?.name == version.name || hasNoVersions) {
|
||||
if (selectedVersion == version || hasNoVersions) {
|
||||
selectedVersion = null;
|
||||
}
|
||||
}
|
||||
@@ -125,7 +118,7 @@ class GameController extends GetxController {
|
||||
|
||||
set selectedVersion(FortniteVersion? version) {
|
||||
_selectedVersion.value = version;
|
||||
_storage?.write("version", version?.name);
|
||||
_storage?.write("version", version?.content.toString());
|
||||
}
|
||||
|
||||
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) {
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dart_ipify/dart_ipify.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/util/cryptography.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class HostingController extends GetxController {
|
||||
late final GetStorage? _storage;
|
||||
late final String uuid;
|
||||
late final TextEditingController name;
|
||||
late final FocusNode nameFocusNode;
|
||||
late final TextEditingController description;
|
||||
late final FocusNode descriptionFocusNode;
|
||||
late final TextEditingController password;
|
||||
late final FocusNode passwordFocusNode;
|
||||
late final RxBool showPassword;
|
||||
late final RxBool discoverable;
|
||||
late final Rx<GameServerType> type;
|
||||
@@ -19,10 +27,11 @@ class HostingController extends GetxController {
|
||||
late final RxBool started;
|
||||
late final RxBool published;
|
||||
late final Rxn<GameInstance> instance;
|
||||
late final Rxn<Set<Map<String, dynamic>>> servers;
|
||||
late final Rxn<Set<FortniteServer>> servers;
|
||||
late final Semaphore _semaphore;
|
||||
|
||||
HostingController() {
|
||||
_storage = appWithNoStorage ? null : GetStorage("hosting");
|
||||
_storage = appWithNoStorage ? null : GetStorage("hosting_storage");
|
||||
uuid = _storage?.read("uuid") ?? const Uuid().v4();
|
||||
_storage?.write("uuid", uuid);
|
||||
name = TextEditingController(text: _storage?.read("name"));
|
||||
@@ -31,6 +40,9 @@ class HostingController extends GetxController {
|
||||
description.addListener(() => _storage?.write("description", description.text));
|
||||
password = TextEditingController(text: _storage?.read("password") ?? "");
|
||||
password.addListener(() => _storage?.write("password", password.text));
|
||||
nameFocusNode = FocusNode();
|
||||
descriptionFocusNode = FocusNode();
|
||||
passwordFocusNode = FocusNode();
|
||||
discoverable = RxBool(_storage?.read("discoverable") ?? false);
|
||||
discoverable.listen((value) => _storage?.write("discoverable", value));
|
||||
type = Rx(GameServerType.values.elementAt(_storage?.read("type") ?? GameServerType.headless.index));
|
||||
@@ -43,16 +55,80 @@ class HostingController extends GetxController {
|
||||
instance = Rxn();
|
||||
final supabase = Supabase.instance.client;
|
||||
servers = Rxn();
|
||||
supabase.from("hosting")
|
||||
supabase.from("hosting_v2")
|
||||
.stream(primaryKey: ['id'])
|
||||
.map((event) => _parseValidServers(event))
|
||||
.map((event) => event.map((element) => FortniteServer.fromJson(element)).where((element) => element.ip.isNotEmpty).toSet())
|
||||
.listen((event) {
|
||||
servers.value = event;
|
||||
published.value = event.any((element) => element["id"] == uuid);
|
||||
published.value = event.any((element) => element.id == uuid);
|
||||
});
|
||||
_semaphore = Semaphore();
|
||||
}
|
||||
|
||||
Set<Map<String, dynamic>> _parseValidServers(event) => event.where((element) => element["ip"] != null).toSet();
|
||||
Future<void> publishServer(String author, String version) async {
|
||||
try {
|
||||
_semaphore.acquire();
|
||||
log("[SERVER] Publishing server...");
|
||||
if(published.value) {
|
||||
log("[SERVER] Already published");
|
||||
return;
|
||||
}
|
||||
|
||||
final passwordText = password.text;
|
||||
final hasPassword = passwordText.isNotEmpty;
|
||||
var ip = await Ipify.ipv4();
|
||||
if(hasPassword) {
|
||||
ip = aes256Encrypt(ip, passwordText);
|
||||
}
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
final hosts = supabase.from("hosting_v2");
|
||||
final payload = FortniteServer(
|
||||
id: uuid,
|
||||
name: name.text,
|
||||
description: description.text,
|
||||
author: author,
|
||||
ip: ip,
|
||||
version: version,
|
||||
password: hasPassword ? hashPassword(passwordText) : null,
|
||||
timestamp: DateTime.now(),
|
||||
discoverable: discoverable.value
|
||||
).toJson();
|
||||
log("[SERVER] Payload: ${jsonEncode(payload)}");
|
||||
if(published()) {
|
||||
await hosts.update(payload)
|
||||
.eq("id", uuid);
|
||||
}else {
|
||||
await hosts.insert(payload);
|
||||
}
|
||||
|
||||
published.value = true;
|
||||
log("[SERVER] Published");
|
||||
}catch(error) {
|
||||
log("[SERVER] Cannot publish server: $error");
|
||||
published.value = false;
|
||||
}finally {
|
||||
_semaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> discardServer() async {
|
||||
try {
|
||||
_semaphore.acquire();
|
||||
log("[SERVER] Discarding server...");
|
||||
final supabase = Supabase.instance.client;
|
||||
await supabase.from("hosting_v2")
|
||||
.delete()
|
||||
.match({'id': uuid});
|
||||
servers.value?.removeWhere((element) => element.id == uuid);
|
||||
log("[SERVER] Discarded server");
|
||||
}catch(error) {
|
||||
log("[SERVER] Cannot discard server: $error");
|
||||
}finally {
|
||||
published.value = false;
|
||||
_semaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
name.text = "";
|
||||
@@ -65,9 +141,9 @@ class HostingController extends GetxController {
|
||||
autoRestart.value = true;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? findServerById(String uuid) {
|
||||
FortniteServer? findServerById(String uuid) {
|
||||
try {
|
||||
return servers.value?.firstWhere((element) => element["id"] == uuid);
|
||||
return servers.value?.firstWhere((element) => element.id == uuid);
|
||||
} on StateError catch(_) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,71 +1,333 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
class SettingsController extends GetxController {
|
||||
late final GetStorage _storage;
|
||||
late final GetStorage? _storage;
|
||||
late final String originalDll;
|
||||
late final TextEditingController gameServerDll;
|
||||
late final TextEditingController unrealEngineConsoleDll;
|
||||
late final TextEditingController backendDll;
|
||||
late final TextEditingController memoryLeakDll;
|
||||
late final TextEditingController gameServerPort;
|
||||
late final RxBool firstRun;
|
||||
late final RxString language;
|
||||
late final Rx<ThemeMode> themeMode;
|
||||
late final RxnInt timestamp;
|
||||
late final Rx<UpdateStatus> status;
|
||||
late final Rx<UpdateTimer> timer;
|
||||
late final TextEditingController url;
|
||||
late final RxBool customGameServer;
|
||||
late final RxBool firstRun;
|
||||
late final Map<String, Future<bool>> _operations;
|
||||
late double width;
|
||||
late double height;
|
||||
late double? offsetX;
|
||||
late double? offsetY;
|
||||
InfoBarEntry? infoBarEntry;
|
||||
Future<bool>? _updater;
|
||||
|
||||
SettingsController() {
|
||||
_storage = GetStorage("settings");
|
||||
gameServerDll = _createController("game_server", "reboot.dll");
|
||||
unrealEngineConsoleDll = _createController("unreal_engine_console", "console.dll");
|
||||
backendDll = _createController("backend", "cobalt.dll");
|
||||
memoryLeakDll = _createController("memory_leak", "memory.dll");
|
||||
gameServerPort = TextEditingController(text: _storage.read("game_server_port") ?? kDefaultGameServerPort);
|
||||
gameServerPort.addListener(() => _storage.write("game_server_port", gameServerPort.text));
|
||||
width = _storage.read("width") ?? kDefaultWindowWidth;
|
||||
height = _storage.read("height") ?? kDefaultWindowHeight;
|
||||
offsetX = _storage.read("offset_x");
|
||||
offsetY = _storage.read("offset_y");
|
||||
firstRun = RxBool(_storage.read("first_run_new1") ?? true);
|
||||
firstRun.listen((value) => _storage.write("first_run_new1", value));
|
||||
themeMode = Rx(ThemeMode.values.elementAt(_storage.read("theme") ?? 0));
|
||||
themeMode.listen((value) => _storage.write("theme", value.index));
|
||||
language = RxString(_storage.read("language") ?? currentLocale);
|
||||
language.listen((value) => _storage.write("language", value));
|
||||
_storage = appWithNoStorage ? null : GetStorage("settings_storage");
|
||||
gameServerDll = _createController("game_server", InjectableDll.reboot);
|
||||
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
|
||||
backendDll = _createController("backend", InjectableDll.cobalt);
|
||||
memoryLeakDll = _createController("memory_leak", InjectableDll.memory);
|
||||
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
|
||||
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
|
||||
width = _storage?.read("width") ?? kDefaultWindowWidth;
|
||||
height = _storage?.read("height") ?? kDefaultWindowHeight;
|
||||
offsetX = _storage?.read("offset_x");
|
||||
offsetY = _storage?.read("offset_y");
|
||||
themeMode = Rx(ThemeMode.values.elementAt(_storage?.read("theme") ?? 0));
|
||||
themeMode.listen((value) => _storage?.write("theme", value.index));
|
||||
language = RxString(_storage?.read("language") ?? currentLocale);
|
||||
language.listen((value) => _storage?.write("language", value));
|
||||
timestamp = RxnInt(_storage?.read("ts"));
|
||||
timestamp.listen((value) => _storage?.write("ts", value));
|
||||
final timerIndex = _storage?.read("timer");
|
||||
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
|
||||
timer.listen((value) => _storage?.write("timer", value.index));
|
||||
url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl);
|
||||
url.addListener(() => _storage?.write("update_url", url.text));
|
||||
status = Rx(UpdateStatus.waiting);
|
||||
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
|
||||
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
|
||||
firstRun = RxBool(_storage?.read("first_run_tutorial") ?? true);
|
||||
firstRun.listen((value) => _storage?.write("first_run_tutorial", value));
|
||||
_operations = {};
|
||||
}
|
||||
|
||||
TextEditingController _createController(String key, String name) {
|
||||
var controller = TextEditingController(text: _storage.read(key) ?? _controllerDefaultPath(name));
|
||||
controller.addListener(() => _storage.write(key, controller.text));
|
||||
TextEditingController _createController(String key, InjectableDll dll) {
|
||||
final controller = TextEditingController(text: _storage?.read(key) ?? _getDefaultPath(dll));
|
||||
controller.addListener(() => _storage?.write(key, controller.text));
|
||||
return controller;
|
||||
}
|
||||
|
||||
void saveWindowSize(Size size) {
|
||||
_storage.write("width", size.width);
|
||||
_storage.write("height", size.height);
|
||||
_storage?.write("width", size.width);
|
||||
_storage?.write("height", size.height);
|
||||
}
|
||||
|
||||
void saveWindowOffset(Offset position) {
|
||||
offsetX = position.dx;
|
||||
offsetY = position.dy;
|
||||
_storage.write("offset_x", offsetX);
|
||||
_storage.write("offset_y", offsetY);
|
||||
_storage?.write("offset_x", offsetX);
|
||||
_storage?.write("offset_y", offsetY);
|
||||
}
|
||||
|
||||
void reset(){
|
||||
gameServerDll.text = _controllerDefaultPath("reboot.dll");
|
||||
unrealEngineConsoleDll.text = _controllerDefaultPath("console.dll");
|
||||
backendDll.text = _controllerDefaultPath("cobalt.dll");
|
||||
gameServerDll.text = _getDefaultPath(InjectableDll.reboot);
|
||||
unrealEngineConsoleDll.text = _getDefaultPath(InjectableDll.console);
|
||||
backendDll.text = _getDefaultPath(InjectableDll.cobalt);
|
||||
memoryLeakDll.text = _getDefaultPath(InjectableDll.memory);
|
||||
gameServerPort.text = kDefaultGameServerPort;
|
||||
firstRun.value = true;
|
||||
timestamp.value = null;
|
||||
timer.value = UpdateTimer.never;
|
||||
url.text = kRebootDownloadUrl;
|
||||
status.value = UpdateStatus.waiting;
|
||||
customGameServer.value = false;
|
||||
updateReboot();
|
||||
}
|
||||
|
||||
String _controllerDefaultPath(String name) => "${dllsDirectory.path}\\$name";
|
||||
Future<void> notifyLauncherUpdate() async {
|
||||
if(appVersion == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pubspec = await _getPubspecYaml();
|
||||
if(pubspec == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final latestVersion = Version.parse(pubspec["version"]);
|
||||
if(latestVersion <= appVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
late InfoBarEntry infoBar;
|
||||
infoBar = showRebootInfoBar(
|
||||
translations.updateAvailable(latestVersion.toString()),
|
||||
duration: null,
|
||||
severity: InfoBarSeverity.warning,
|
||||
action: Button(
|
||||
child: Text(translations.updateAvailableAction),
|
||||
onPressed: () {
|
||||
infoBar.close();
|
||||
launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/releases"));
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<dynamic> _getPubspecYaml() async {
|
||||
try {
|
||||
final pubspecResponse = await http.get(Uri.parse("https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
|
||||
if(pubspecResponse.statusCode != 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return loadYaml(pubspecResponse.body);
|
||||
}catch(error) {
|
||||
log("[UPDATER] Cannot check for updates: $error");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> updateReboot({bool force = false, bool silent = false}) async {
|
||||
if(_updater != null) {
|
||||
return await _updater!;
|
||||
}
|
||||
|
||||
final result = _updateReboot(force, silent);
|
||||
_updater = result;
|
||||
return await result;
|
||||
}
|
||||
|
||||
Future<bool> _updateReboot(bool force, bool silent) async {
|
||||
try {
|
||||
if(customGameServer.value) {
|
||||
status.value = UpdateStatus.success;
|
||||
return true;
|
||||
}
|
||||
|
||||
final needsUpdate = await hasRebootDllUpdate(
|
||||
timestamp.value,
|
||||
hours: timer.value.hours,
|
||||
force: force
|
||||
);
|
||||
if(!needsUpdate) {
|
||||
status.value = UpdateStatus.success;
|
||||
return true;
|
||||
}
|
||||
|
||||
if(!silent) {
|
||||
infoBarEntry = showRebootInfoBar(
|
||||
translations.downloadingDll("reboot"),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
timestamp.value = await downloadRebootDll(url.text);
|
||||
status.value = UpdateStatus.success;
|
||||
infoBarEntry?.close();
|
||||
if(!silent) {
|
||||
infoBarEntry = showRebootInfoBar(
|
||||
translations.downloadDllSuccess("reboot"),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}catch(message) {
|
||||
infoBarEntry?.close();
|
||||
var error = message.toString();
|
||||
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
status.value = UpdateStatus.error;
|
||||
showRebootInfoBar(
|
||||
translations.downloadDllError("reboot.dll", error.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
action: Button(
|
||||
onPressed: () => updateReboot(
|
||||
force: true,
|
||||
silent: silent
|
||||
),
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}finally {
|
||||
_updater = null;
|
||||
}
|
||||
}
|
||||
|
||||
(File, bool) getInjectableData(InjectableDll dll) {
|
||||
final defaultPath = canonicalize(_getDefaultPath(dll));
|
||||
switch(dll){
|
||||
case InjectableDll.reboot:
|
||||
if(customGameServer.value) {
|
||||
final file = File(gameServerDll.text);
|
||||
if(file.existsSync()) {
|
||||
return (file, true);
|
||||
}
|
||||
}
|
||||
|
||||
return (rebootDllFile, false);
|
||||
case InjectableDll.console:
|
||||
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
|
||||
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
|
||||
case InjectableDll.cobalt:
|
||||
final backendFile = File(backendDll.text);
|
||||
return (backendFile, canonicalize(backendFile.path) != defaultPath);
|
||||
case InjectableDll.memory:
|
||||
final memoryLeakFile = File(memoryLeakDll.text);
|
||||
return (memoryLeakFile, canonicalize(memoryLeakFile.path) != defaultPath);
|
||||
}
|
||||
}
|
||||
|
||||
String _getDefaultPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll";
|
||||
|
||||
Future<bool> downloadCriticalDllInteractive(String filePath, {bool silent = false}) {
|
||||
log("[DLL] Asking for $filePath(silent: $silent)");
|
||||
final old = _operations[filePath];
|
||||
if(old != null) {
|
||||
log("[DLL] Download task already exists");
|
||||
return old;
|
||||
}
|
||||
|
||||
log("[DLL] Creating new download task...");
|
||||
final newRun = _downloadCriticalDllInteractive(filePath, silent);
|
||||
_operations[filePath] = newRun;
|
||||
return newRun;
|
||||
}
|
||||
|
||||
Future<bool> _downloadCriticalDllInteractive(String filePath, bool silent) async {
|
||||
final fileName = basename(filePath).toLowerCase();
|
||||
log("[DLL] File name: $fileName");
|
||||
InfoBarEntry? entry;
|
||||
try {
|
||||
if (fileName == "reboot.dll") {
|
||||
log("[DLL] Downloading reboot.dll...");
|
||||
return await updateReboot(
|
||||
silent: silent
|
||||
);
|
||||
}
|
||||
|
||||
if(File(filePath).existsSync()) {
|
||||
log("[DLL] File already exists");
|
||||
return true;
|
||||
}
|
||||
|
||||
final fileNameWithoutExtension = basenameWithoutExtension(filePath);
|
||||
if(!silent) {
|
||||
entry = showRebootInfoBar(
|
||||
translations.downloadingDll(fileNameWithoutExtension),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
await downloadCriticalDll(fileName, filePath);
|
||||
entry?.close();
|
||||
if(!silent) {
|
||||
entry = await showRebootInfoBar(
|
||||
translations.downloadDllSuccess(fileNameWithoutExtension),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}catch(message) {
|
||||
log("[DLL] Error: $message");
|
||||
entry?.close();
|
||||
var error = message.toString();
|
||||
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
final completer = Completer();
|
||||
await showRebootInfoBar(
|
||||
translations.downloadDllError(fileName, error.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
onDismissed: () => completer.complete(null),
|
||||
action: Button(
|
||||
onPressed: () async {
|
||||
await downloadCriticalDllInteractive(filePath);
|
||||
completer.complete(null);
|
||||
},
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
await completer.future;
|
||||
return false;
|
||||
}finally {
|
||||
_operations.remove(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _UpdateTimerExtension on UpdateTimer {
|
||||
int get hours {
|
||||
switch(this) {
|
||||
case UpdateTimer.never:
|
||||
return -1;
|
||||
case UpdateTimer.hour:
|
||||
return 1;
|
||||
case UpdateTimer.day:
|
||||
return 24;
|
||||
case UpdateTimer.week:
|
||||
return 24 * 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
class UpdateController {
|
||||
late final GetStorage? _storage;
|
||||
late final RxnInt timestamp;
|
||||
late final Rx<UpdateStatus> status;
|
||||
late final Rx<UpdateTimer> timer;
|
||||
late final TextEditingController url;
|
||||
late final RxBool customGameServer;
|
||||
InfoBarEntry? infoBarEntry;
|
||||
Future? _updater;
|
||||
|
||||
UpdateController() {
|
||||
_storage = appWithNoStorage ? null : GetStorage("update");
|
||||
timestamp = RxnInt(_storage?.read("ts"));
|
||||
timestamp.listen((value) => _storage?.write("ts", value));
|
||||
var timerIndex = _storage?.read("timer");
|
||||
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
|
||||
timer.listen((value) => _storage?.write("timer", value.index));
|
||||
url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl);
|
||||
url.addListener(() => _storage?.write("update_url", url.text));
|
||||
status = Rx(UpdateStatus.waiting);
|
||||
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
|
||||
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
|
||||
}
|
||||
|
||||
Future<void> notifyLauncherUpdate() async {
|
||||
if(appVersion == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pubspecResponse = await http.get(Uri.parse("https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
|
||||
if(pubspecResponse.statusCode != 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pubspec = loadYaml(pubspecResponse.body);
|
||||
final latestVersion = Version.parse(pubspec["version"]);
|
||||
if(latestVersion <= appVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
late InfoBarEntry infoBar;
|
||||
infoBar = showInfoBar(
|
||||
translations.updateAvailable(latestVersion.toString()),
|
||||
duration: null,
|
||||
severity: InfoBarSeverity.warning,
|
||||
action: Button(
|
||||
child: Text(translations.updateAvailableAction),
|
||||
onPressed: () {
|
||||
infoBar.close();
|
||||
launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/releases"));
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateReboot({bool force = false, bool silent = false}) async {
|
||||
if(_updater != null) {
|
||||
return await _updater;
|
||||
}
|
||||
|
||||
final result = _updateReboot(force, silent);
|
||||
_updater = result;
|
||||
return await result;
|
||||
}
|
||||
|
||||
Future<void> _updateReboot(bool force, bool silent) async {
|
||||
try {
|
||||
if(customGameServer.value) {
|
||||
status.value = UpdateStatus.success;
|
||||
return;
|
||||
}
|
||||
|
||||
final needsUpdate = await hasRebootDllUpdate(
|
||||
timestamp.value,
|
||||
hours: timer.value.hours,
|
||||
force: force
|
||||
);
|
||||
if(!needsUpdate) {
|
||||
status.value = UpdateStatus.success;
|
||||
return;
|
||||
}
|
||||
|
||||
if(!silent) {
|
||||
infoBarEntry = showInfoBar(
|
||||
translations.downloadingDll("reboot"),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
timestamp.value = await downloadRebootDll(url.text);
|
||||
status.value = UpdateStatus.success;
|
||||
infoBarEntry?.close();
|
||||
if(!silent) {
|
||||
infoBarEntry = showInfoBar(
|
||||
translations.downloadDllSuccess("reboot"),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}
|
||||
}catch(message) {
|
||||
if(!silent) {
|
||||
infoBarEntry?.close();
|
||||
var error = message.toString();
|
||||
error =
|
||||
error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
status.value = UpdateStatus.error;
|
||||
showInfoBar(
|
||||
translations.downloadDllError("reboot.dll", error.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
action: Button(
|
||||
onPressed: () => updateReboot(
|
||||
force: true,
|
||||
silent: silent
|
||||
),
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
}
|
||||
}finally {
|
||||
_updater = null;
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
timestamp.value = null;
|
||||
timer.value = UpdateTimer.never;
|
||||
url.text = kRebootDownloadUrl;
|
||||
status.value = UpdateStatus.waiting;
|
||||
customGameServer.value = false;
|
||||
updateReboot();
|
||||
}
|
||||
}
|
||||
|
||||
extension _UpdateTimerExtension on UpdateTimer {
|
||||
int get hours {
|
||||
switch(this) {
|
||||
case UpdateTimer.never:
|
||||
return -1;
|
||||
case UpdateTimer.hour:
|
||||
return 1;
|
||||
case UpdateTimer.day:
|
||||
return 24;
|
||||
case UpdateTimer.week:
|
||||
return 24 * 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
class DialogButton extends StatefulWidget {
|
||||
final String? text;
|
||||
final Function()? onTap;
|
||||
final ButtonType type;
|
||||
|
||||
const DialogButton(
|
||||
{Key? key,
|
||||
this.text,
|
||||
this.onTap,
|
||||
required this.type})
|
||||
: assert(type != ButtonType.primary || onTap != null,
|
||||
"OnTap handler cannot be null for primary buttons"),
|
||||
assert(type != ButtonType.primary || text != null,
|
||||
"Text cannot be null for primary buttons"),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<DialogButton> createState() => _DialogButtonState();
|
||||
}
|
||||
|
||||
class _DialogButtonState extends State<DialogButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.type == ButtonType.only ? _onlyButton : _button;
|
||||
|
||||
SizedBox get _onlyButton => SizedBox(
|
||||
width: double.infinity,
|
||||
child: _button
|
||||
);
|
||||
|
||||
Widget get _button => widget.type == ButtonType.primary ? _primaryButton : _secondaryButton;
|
||||
|
||||
Widget get _primaryButton {
|
||||
return Button(
|
||||
onPressed: widget.onTap!,
|
||||
child: Text(widget.text!),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _secondaryButton {
|
||||
return Button(
|
||||
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
|
||||
child: Text(widget.text ?? translations.defaultDialogSecondaryAction),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDefaultSecondaryActionTap() => Navigator.of(context).pop(null);
|
||||
}
|
||||
|
||||
enum ButtonType {
|
||||
primary,
|
||||
secondary,
|
||||
only
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
import 'dialog_button.dart';
|
||||
|
||||
bool inDialog = false;
|
||||
|
||||
Future<T?> showAppDialog<T extends Object?>({required WidgetBuilder builder}) async {
|
||||
Future<T?> showRebootDialog<T extends Object?>({required WidgetBuilder builder, bool dismissWithEsc = true}) async {
|
||||
inDialog = true;
|
||||
pagesController.add(null);
|
||||
try {
|
||||
return await fluent.showDialog(
|
||||
context: appKey.currentContext!,
|
||||
context: appNavigatorKey.currentContext!,
|
||||
useRootNavigator: false,
|
||||
dismissWithEsc: dismissWithEsc,
|
||||
builder: builder
|
||||
);
|
||||
}finally {
|
||||
@@ -58,7 +57,7 @@ class FormDialog extends AbstractDialog {
|
||||
return Form(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
var parsed = buttons.map((entry) => _createFormButton(entry, context)).toList();
|
||||
final parsed = buttons.map((entry) => _createFormButton(entry, context)).toList();
|
||||
return GenericDialog(
|
||||
header: content,
|
||||
buttons: parsed
|
||||
@@ -117,8 +116,9 @@ class InfoDialog extends AbstractDialog {
|
||||
class ProgressDialog extends AbstractDialog {
|
||||
final String text;
|
||||
final Function()? onStop;
|
||||
final bool showButton;
|
||||
|
||||
const ProgressDialog({required this.text, this.onStop, Key? key}) : super(key: key);
|
||||
const ProgressDialog({required this.text, this.onStop, this.showButton = true, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -132,11 +132,12 @@ class ProgressDialog extends AbstractDialog {
|
||||
),
|
||||
),
|
||||
buttons: [
|
||||
DialogButton(
|
||||
text: translations.defaultDialogSecondaryAction,
|
||||
type: ButtonType.only,
|
||||
onTap: onStop
|
||||
)
|
||||
if(showButton)
|
||||
DialogButton(
|
||||
text: translations.defaultDialogSecondaryAction,
|
||||
type: ButtonType.only,
|
||||
onTap: onStop
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -239,7 +240,7 @@ class ErrorDialog extends AbstractDialog {
|
||||
type: type,
|
||||
onTap: () async {
|
||||
FlutterClipboard.controlC("$error\n$stackTrace");
|
||||
showInfoBar(translations.copyErrorDialogSuccess);
|
||||
showRebootInfoBar(translations.copyErrorDialogSuccess);
|
||||
onClick();
|
||||
},
|
||||
);
|
||||
@@ -263,3 +264,61 @@ class ErrorDialog extends AbstractDialog {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DialogButton extends StatefulWidget {
|
||||
final String? text;
|
||||
final Function()? onTap;
|
||||
final ButtonType type;
|
||||
final Color? color;
|
||||
|
||||
const DialogButton(
|
||||
{Key? key,
|
||||
this.text,
|
||||
this.onTap,
|
||||
this.color,
|
||||
required this.type})
|
||||
: assert(type != ButtonType.primary || onTap != null,
|
||||
"OnTap handler cannot be null for primary buttons"),
|
||||
assert(type != ButtonType.primary || text != null,
|
||||
"Text cannot be null for primary buttons"),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<DialogButton> createState() => _DialogButtonState();
|
||||
}
|
||||
|
||||
class _DialogButtonState extends State<DialogButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.type == ButtonType.only ? _onlyButton : _button;
|
||||
|
||||
SizedBox get _onlyButton => SizedBox(
|
||||
width: double.infinity,
|
||||
child: _button
|
||||
);
|
||||
|
||||
Widget get _button => widget.type == ButtonType.primary ? _primaryButton : _secondaryButton;
|
||||
|
||||
Widget get _primaryButton => Button(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor)
|
||||
),
|
||||
onPressed: widget.onTap!,
|
||||
child: Text(widget.text!),
|
||||
);
|
||||
|
||||
Widget get _secondaryButton => Button(
|
||||
style: widget.color != null ? ButtonStyle(
|
||||
backgroundColor: ButtonState.all(widget.color!)
|
||||
) : null,
|
||||
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
|
||||
child: Text(widget.text ?? translations.defaultDialogSecondaryAction),
|
||||
);
|
||||
|
||||
void _onDefaultSecondaryActionTap() => Navigator.of(context).pop(null);
|
||||
}
|
||||
|
||||
enum ButtonType {
|
||||
primary,
|
||||
secondary,
|
||||
only
|
||||
}
|
||||
@@ -5,7 +5,7 @@ const infoBarLongDuration = Duration(seconds: 4);
|
||||
const infoBarShortDuration = Duration(seconds: 2);
|
||||
const _height = 64.0;
|
||||
|
||||
InfoBarEntry showInfoBar(dynamic text, {
|
||||
InfoBarEntry showRebootInfoBar(dynamic text, {
|
||||
InfoBarSeverity severity = InfoBarSeverity.info,
|
||||
bool loading = false,
|
||||
Duration? duration = infoBarShortDuration,
|
||||
@@ -21,33 +21,40 @@ InfoBarEntry showInfoBar(dynamic text, {
|
||||
return overlayEntry;
|
||||
}
|
||||
|
||||
Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => SizedBox(
|
||||
width: double.infinity,
|
||||
height: _height,
|
||||
Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: double.infinity,
|
||||
minHeight: _height
|
||||
),
|
||||
child: Mica(
|
||||
child: InfoBar(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if(text is Widget)
|
||||
text,
|
||||
if(text is String)
|
||||
Text(text),
|
||||
if(action != null)
|
||||
action
|
||||
],
|
||||
),
|
||||
isLong: false,
|
||||
isIconVisible: true,
|
||||
content: SizedBox(
|
||||
width: double.infinity,
|
||||
child: loading ? const Padding(
|
||||
padding: EdgeInsets.only(top: 8.0, bottom: 2.0),
|
||||
child: ProgressBar(),
|
||||
) : const SizedBox()
|
||||
),
|
||||
severity: severity
|
||||
),
|
||||
elevation: 1,
|
||||
child: InfoBar(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if(text is Widget)
|
||||
text,
|
||||
if(text is String)
|
||||
Text(text),
|
||||
if(action != null)
|
||||
action
|
||||
],
|
||||
),
|
||||
isLong: false,
|
||||
isIconVisible: true,
|
||||
content: SizedBox(
|
||||
width: double.infinity,
|
||||
child: loading ? const Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8.0,
|
||||
bottom: 2.0,
|
||||
right: 6.0
|
||||
),
|
||||
child: ProgressBar(),
|
||||
) : const SizedBox()
|
||||
),
|
||||
severity: severity
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
170
gui/lib/src/messenger/abstract/overlay.dart
Normal file
170
gui/lib/src/messenger/abstract/overlay.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
|
||||
typedef WidgetBuilder = Widget Function(BuildContext, void Function());
|
||||
|
||||
class OverlayTarget extends StatefulWidget {
|
||||
final Widget child;
|
||||
const OverlayTarget({super.key, required this.child});
|
||||
|
||||
@override
|
||||
State<OverlayTarget> createState() => OverlayTargetState();
|
||||
|
||||
OverlayTargetState of(BuildContext context) => context.findAncestorStateOfType<OverlayTargetState>()!;
|
||||
}
|
||||
|
||||
class OverlayTargetState extends State<OverlayTarget> {
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
|
||||
void showOverlay({
|
||||
required String text,
|
||||
required WidgetBuilder actionBuilder,
|
||||
Offset offset = Offset.zero,
|
||||
bool ignoreTargetPointers = true,
|
||||
AttachMode attachMode = AttachMode.start
|
||||
}) {
|
||||
final renderBox = context.findRenderObject() as RenderBox;
|
||||
final position = renderBox.localToGlobal(Offset.zero);
|
||||
final color = FluentTheme.of(context).acrylicBackgroundColor;
|
||||
late OverlayEntry entry;
|
||||
entry = OverlayEntry(
|
||||
builder: (context) => Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: _AbsorbPointer(
|
||||
exclusion: ignoreTargetPointers ? null : renderBox
|
||||
)
|
||||
),
|
||||
Positioned(
|
||||
left: position.dx - (attachMode != AttachMode.start ? renderBox.size.width : 0) + offset.dx,
|
||||
top: position.dy + (renderBox.size.height / 2) + offset.dy,
|
||||
child: CustomPaint(
|
||||
painter: _CallOutShape(color, attachMode != AttachMode.start),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(text),
|
||||
const SizedBox(height: 12.0),
|
||||
actionBuilder(context, () => entry.remove())
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
appOverlayKey.currentState?.insert(entry);
|
||||
}
|
||||
}
|
||||
|
||||
enum AttachMode {
|
||||
start,
|
||||
middle,
|
||||
end;
|
||||
}
|
||||
|
||||
// Harder than one would think
|
||||
class _CallOutShape extends CustomPainter {
|
||||
final Color color;
|
||||
final bool end;
|
||||
_CallOutShape(this.color, this.end);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final fillPaint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final borderPaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.25
|
||||
..color = Colors.white;
|
||||
|
||||
final path = Path();
|
||||
path.moveTo(10, 0);
|
||||
if(!end) {
|
||||
path.lineTo(12.5, 0);
|
||||
path.lineTo(20, -12.5);
|
||||
path.lineTo(27.5, 0);
|
||||
}else {
|
||||
path.lineTo(size.width - 27.5, 0);
|
||||
path.lineTo(size.width - 20, -12.5);
|
||||
path.lineTo(size.width - 12.5, 0);
|
||||
}
|
||||
|
||||
path.lineTo(size.width - 10, 0);
|
||||
path.arcToPoint(Offset(size.width, 10), radius: Radius.circular(10));
|
||||
path.lineTo(size.width, size.height - 10);
|
||||
path.arcToPoint(Offset(size.width - 10, size.height), radius: Radius.circular(10));
|
||||
path.lineTo(10, size.height);
|
||||
path.arcToPoint(Offset(0, size.height - 10), radius: Radius.circular(10));
|
||||
path.lineTo(0, 10);
|
||||
path.arcToPoint(Offset(10, 0), radius: Radius.circular(10));
|
||||
path.close();
|
||||
|
||||
canvas.drawPath(path, fillPaint);
|
||||
canvas.drawPath(path, borderPaint);
|
||||
canvas.drawShadow(path, color, 1, true);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class _AbsorbPointer extends SingleChildRenderObjectWidget {
|
||||
final RenderBox? exclusion;
|
||||
const _AbsorbPointer({
|
||||
required this.exclusion
|
||||
});
|
||||
|
||||
@override
|
||||
_RenderAbsorbPointer createRenderObject(BuildContext context) => _RenderAbsorbPointer(
|
||||
exclusion: exclusion
|
||||
);
|
||||
}
|
||||
|
||||
class _RenderAbsorbPointer extends RenderProxyBox {
|
||||
final RenderBox? exclusion;
|
||||
_RenderAbsorbPointer({
|
||||
required this.exclusion,
|
||||
RenderBox? child
|
||||
}) : super(child);
|
||||
|
||||
@override
|
||||
bool hitTest(BoxHitTestResult result, { required Offset position }) {
|
||||
final exclusion = this.exclusion;
|
||||
if(exclusion == null) {
|
||||
return size.contains(position);
|
||||
}
|
||||
|
||||
// 32 is the height of the title bar (need this offset as the overlay area doesn't include it)
|
||||
// Not an optimal solution but it works (calculating it is kind of complicated)
|
||||
position = Offset(position.dx, position.dy + HomePage.kTitleBarHeight);
|
||||
final exclusionPosition = exclusion.localToGlobal(Offset.zero);
|
||||
final exclusionSize = Rect.fromLTRB(
|
||||
exclusionPosition.dx,
|
||||
exclusionPosition.dy,
|
||||
exclusionPosition.dx + exclusion.size.width,
|
||||
exclusionPosition.dy + exclusion.size.height
|
||||
);
|
||||
return !exclusionSize.contains(position);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitChildrenForSemantics(RenderObjectVisitor visitor) => super.visitChildrenForSemantics(visitor);
|
||||
|
||||
@override
|
||||
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||
super.describeSemanticsConfiguration(config);
|
||||
config.isBlockingUserActions = true;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
Future<void> showResetDialog(Function() onConfirm) => showAppDialog(
|
||||
Future<void> showResetDialog(Function() onConfirm) => showRebootDialog(
|
||||
builder: (context) => InfoDialog(
|
||||
text: translations.resetDefaultsDialogTitle,
|
||||
buttons: [
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
Future<void> showDllDeletedDialog(Function() onConfirm) => showAppDialog(
|
||||
Future<void> showDllDeletedDialog(Function() onConfirm) => showRebootDialog(
|
||||
builder: (context) => InfoDialog(
|
||||
text: translations.dllDeletedTitle,
|
||||
buttons: [
|
||||
@@ -1,12 +1,9 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
import '../../util/log.dart';
|
||||
|
||||
|
||||
String? lastError;
|
||||
|
||||
void onError(Object exception, StackTrace? stackTrace, bool framework) {
|
||||
@@ -21,12 +18,12 @@ void onError(Object exception, StackTrace? stackTrace, bool framework) {
|
||||
}
|
||||
|
||||
lastError = exception.toString();
|
||||
var route = ModalRoute.of(pageKey.currentContext!);
|
||||
final route = ModalRoute.of(pageKey.currentContext!);
|
||||
if(route != null && !route.isCurrent){
|
||||
Navigator.of(pageKey.currentContext!).pop(false);
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showAppDialog(
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showRebootDialog(
|
||||
builder: (context) =>
|
||||
ErrorDialog(
|
||||
exception: exception,
|
||||
346
gui/lib/src/messenger/implementation/onboard.dart
Normal file
346
gui/lib/src/messenger/implementation/onboard.dart
Normal file
@@ -0,0 +1,346 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/profile.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/backend_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/host_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/play_page.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector.dart';
|
||||
|
||||
void startOnboarding() {
|
||||
final settingsController = Get.find<SettingsController>();
|
||||
settingsController.firstRun.value = false;
|
||||
profileOverlayKey.currentState!.showOverlay(
|
||||
text: translations.startOnboardingText,
|
||||
offset: Offset(27.5, 17.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.startOnboardingActionLabel,
|
||||
onTap: () async {
|
||||
onClose();
|
||||
await showProfileForm(context);
|
||||
_promptPlayPage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptPlayPage() {
|
||||
pageIndex.value = RebootPageType.play.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptPlayPageText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptPlayPageActionLabel,
|
||||
onTap: () async {
|
||||
onClose();
|
||||
_promptPlayVersion();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptPlayVersion() {
|
||||
final gameController = Get.find<GameController>();
|
||||
final hasBuilds = gameController.versions.value.isNotEmpty;
|
||||
gameVersionOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptPlayVersionText,
|
||||
attachMode: AttachMode.middle,
|
||||
offset: Offset(-25, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: hasBuilds ? translations.promptPlayVersionActionLabelHasBuilds : translations.promptPlayVersionActionLabelNoBuilds,
|
||||
onTap: () async {
|
||||
onClose();
|
||||
if(!hasBuilds) {
|
||||
await VersionSelector.openDownloadDialog(closable: false);
|
||||
}
|
||||
_promptServerBrowserPage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptServerBrowserPage() {
|
||||
pageIndex.value = RebootPageType.browser.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptServerBrowserPageText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptServerBrowserPageActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostPage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostPage() {
|
||||
pageIndex.value = RebootPageType.host.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostPageText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostPageActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostInfo();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _promptHostInfo() {
|
||||
final hostingController = Get.find<HostingController>();
|
||||
hostInfoOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostInfoText,
|
||||
offset: Offset(-10, 2.5),
|
||||
actionBuilder: (context, onClose) => Row(
|
||||
children: [
|
||||
_buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInfoActionLabelSkip,
|
||||
themed: false,
|
||||
onTap: () {
|
||||
onClose();
|
||||
hostingController.discoverable.value = false;
|
||||
_promptHostVersion();
|
||||
}
|
||||
),
|
||||
const SizedBox(width: 12.0),
|
||||
_buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInfoActionLabelConfigure,
|
||||
onTap: () {
|
||||
onClose();
|
||||
hostingController.discoverable.value = true;
|
||||
hostInfoTileKey.currentState!.openNestedPage();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _promptHostInformation());
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostInformation() {
|
||||
final hostingController = Get.find<HostingController>();
|
||||
hostingController.nameFocusNode.requestFocus();
|
||||
hostInfoNameOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostInformationText,
|
||||
attachMode: AttachMode.middle,
|
||||
ignoreTargetPointers: false,
|
||||
offset: Offset(100, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInformationActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostInformationDescription();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostInformationDescription() {
|
||||
final hostingController = Get.find<HostingController>();
|
||||
hostingController.descriptionFocusNode.requestFocus();
|
||||
hostInfoDescriptionOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostInformationDescriptionText,
|
||||
attachMode: AttachMode.middle,
|
||||
ignoreTargetPointers: false,
|
||||
offset: Offset(70, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInformationDescriptionActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostInformationPassword();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostInformationPassword() {
|
||||
final hostingController = Get.find<HostingController>();
|
||||
hostingController.passwordFocusNode.requestFocus();
|
||||
hostInfoPasswordOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostInformationPasswordText,
|
||||
ignoreTargetPointers: false,
|
||||
attachMode: AttachMode.middle,
|
||||
offset: Offset(25, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInformationPasswordActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
Navigator.of(hostInfoTileKey.currentContext!).pop();
|
||||
pageStack.removeLast();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _promptHostVersion());
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostVersion() {
|
||||
hostVersionOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostVersionText,
|
||||
attachMode: AttachMode.end,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostVersionActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostShare();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostShare() {
|
||||
final backendController = Get.find<BackendController>();
|
||||
hostShareOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostShareText,
|
||||
offset: Offset(-10, 2.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostShareActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
backendController.type.value = ServerType.embedded;
|
||||
_promptBackendPage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _promptBackendPage() {
|
||||
pageIndex.value = RebootPageType.backend.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendPageText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendPageActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptBackendTypePage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptBackendTypePage() {
|
||||
backendTypeOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendTypePageText,
|
||||
attachMode: AttachMode.end,
|
||||
offset: Offset(-25, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendTypePageActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptBackendGameServerAddress();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptBackendGameServerAddress() {
|
||||
backendGameServerAddressOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendGameServerAddressText,
|
||||
attachMode: AttachMode.end,
|
||||
offset: Offset(-100, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendGameServerAddressActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptBackendUnrealEngineKey();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptBackendUnrealEngineKey() {
|
||||
backendUnrealEngineOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendUnrealEngineKeyText,
|
||||
attachMode: AttachMode.end,
|
||||
offset: Offset(-465, 2.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendUnrealEngineKeyActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptBackendDetached();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptBackendDetached() {
|
||||
backendDetachedOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendDetachedText,
|
||||
attachMode: AttachMode.end,
|
||||
offset: Offset(-410, 2.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendDetachedActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptInfoTab();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptInfoTab() {
|
||||
pageIndex.value = RebootPageType.info.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptInfoTabText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptInfoTabActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptSettingsTab();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptSettingsTab() {
|
||||
pageIndex.value = RebootPageType.settings.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptSettingsTabText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptSettingsTabActionLabel,
|
||||
onTap: onClose
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton({
|
||||
required BuildContext context,
|
||||
required String label,
|
||||
bool themed = true,
|
||||
required void Function() onTap,
|
||||
}) => Button(
|
||||
style: themed ? ButtonStyle(
|
||||
backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor)
|
||||
) : null,
|
||||
child: Text(label),
|
||||
onPressed: onTap
|
||||
);
|
||||
@@ -2,18 +2,17 @@ import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/material.dart' show Icons;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
|
||||
Future<bool> showProfileForm(BuildContext context) async{
|
||||
var showPassword = RxBool(false);
|
||||
var oldUsername = _gameController.username.text;
|
||||
var showPasswordTrailing = RxBool(oldUsername.isNotEmpty);
|
||||
var oldPassword = _gameController.password.text;
|
||||
var result = await showAppDialog<bool?>(
|
||||
final showPassword = RxBool(false);
|
||||
final oldUsername = _gameController.username.text;
|
||||
final showPasswordTrailing = RxBool(oldUsername.isNotEmpty);
|
||||
final oldPassword = _gameController.password.text;
|
||||
final result = await showRebootDialog<bool?>(
|
||||
builder: (context) => Obx(() => FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -1,24 +1,20 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:dart_ipify/dart_ipify.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/cryptography.dart';
|
||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
|
||||
final Semaphore _publishingSemaphore = Semaphore();
|
||||
|
||||
extension ServerControllerDialog on BackendController {
|
||||
Future<bool> toggleInteractive() async {
|
||||
@@ -39,108 +35,108 @@ extension ServerControllerDialog on BackendController {
|
||||
}
|
||||
|
||||
InfoBarEntry _handeEvent(ServerResult event) {
|
||||
switch (event.type) {
|
||||
log("[BACKEND] Handling event: $event");
|
||||
switch (event.type) {
|
||||
case ServerResultType.starting:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.startingServer,
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.startSuccess:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
case ServerResultType.startError:
|
||||
print(event.stackTrace);
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
case ServerResultType.stopping:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.stoppingServer,
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.stopSuccess:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.stoppedServer,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
case ServerResultType.stopError:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.stopServerError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
case ServerResultType.missingHostError:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.missingHostNameError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.missingPortError:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.missingPortError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.illegalPortError:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.illegalPortError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.freeingPort:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.freeingPort,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.freePortSuccess:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.freedPort,
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
case ServerResultType.freePortError:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.freePortError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
case ServerResultType.pingingRemote:
|
||||
return showInfoBar(
|
||||
translations.pingingRemoteServer,
|
||||
return showRebootInfoBar(
|
||||
translations.pingingServer(ServerType.remote.name),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.pingingLocal:
|
||||
return showInfoBar(
|
||||
translations.pingingLocalServer(type.value.name),
|
||||
return showRebootInfoBar(
|
||||
translations.pingingServer(type.value.name),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.pingError:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.pingError(type.value.name),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.processError:
|
||||
return showRebootInfoBar(
|
||||
translations.backendProcessError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void joinLocalHost() {
|
||||
gameServerAddress.text = kDefaultGameServerHost;
|
||||
gameServerOwner.value = null;
|
||||
}
|
||||
|
||||
Future<void> joinServer(String uuid, Map<String, dynamic> entry) async {
|
||||
final id = entry["id"];
|
||||
if(uuid == id) {
|
||||
showInfoBar(
|
||||
Future<void> joinServerInteractive(String uuid, FortniteServer server) async {
|
||||
if(!kDebugMode && uuid == server.id) {
|
||||
showRebootInfoBar(
|
||||
translations.joinSelfServer,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
@@ -148,18 +144,29 @@ extension ServerControllerDialog on BackendController {
|
||||
return;
|
||||
}
|
||||
|
||||
final hashedPassword = entry["password"];
|
||||
final gameController = Get.find<GameController>();
|
||||
final version = gameController.getVersionByName(server.version.toString());
|
||||
if(version == null) {
|
||||
showRebootInfoBar(
|
||||
translations.cannotJoinServerVersion(server.version.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final hashedPassword = server.password;
|
||||
final hasPassword = hashedPassword != null;
|
||||
final embedded = type.value == ServerType.embedded;
|
||||
final author = entry["author"];
|
||||
final encryptedIp = entry["ip"];
|
||||
final author = server.author;
|
||||
final encryptedIp = server.ip;
|
||||
if(!hasPassword) {
|
||||
final valid = await _isServerValid(encryptedIp);
|
||||
if(!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
_onSuccess(embedded, encryptedIp, author);
|
||||
_onSuccess(gameController, embedded, encryptedIp, author, version);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -169,7 +176,7 @@ extension ServerControllerDialog on BackendController {
|
||||
}
|
||||
|
||||
if(!checkPassword(confirmPassword, hashedPassword)) {
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.wrongServerPassword,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
@@ -183,7 +190,7 @@ extension ServerControllerDialog on BackendController {
|
||||
return;
|
||||
}
|
||||
|
||||
_onSuccess(embedded, decryptedIp, author);
|
||||
_onSuccess(gameController, embedded, decryptedIp, author, version);
|
||||
}
|
||||
|
||||
Future<bool> _isServerValid(String address) async {
|
||||
@@ -192,7 +199,7 @@ extension ServerControllerDialog on BackendController {
|
||||
return true;
|
||||
}
|
||||
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.offlineServer,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
@@ -204,7 +211,7 @@ extension ServerControllerDialog on BackendController {
|
||||
final confirmPasswordController = TextEditingController();
|
||||
final showPassword = RxBool(false);
|
||||
final showPasswordTrailing = RxBool(false);
|
||||
return await showAppDialog<String?>(
|
||||
return await showRebootDialog<String?>(
|
||||
builder: (context) => FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -253,80 +260,18 @@ extension ServerControllerDialog on BackendController {
|
||||
);
|
||||
}
|
||||
|
||||
void _onSuccess(bool embedded, String decryptedIp, String author) {
|
||||
void _onSuccess(GameController controller, bool embedded, String decryptedIp, String author, FortniteVersion version) {
|
||||
if(embedded) {
|
||||
gameServerAddress.text = decryptedIp;
|
||||
gameServerOwner.value = author;
|
||||
pageIndex.value = 0;
|
||||
pageIndex.value = RebootPageType.play.index;
|
||||
}else {
|
||||
FlutterClipboard.controlC(decryptedIp);
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
|
||||
controller.selectedVersion = version;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar(
|
||||
embedded ? translations.joinedServer(author) : translations.copiedIp,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.success
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
extension HostingControllerExtension on HostingController {
|
||||
Future<void> publishServer(String author, String version) async {
|
||||
try {
|
||||
_publishingSemaphore.acquire();
|
||||
if(published.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
final passwordText = password.text;
|
||||
final hasPassword = passwordText.isNotEmpty;
|
||||
var ip = await Ipify.ipv4();
|
||||
if(hasPassword) {
|
||||
ip = aes256Encrypt(ip, passwordText);
|
||||
}
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
final hosts = supabase.from("hosting");
|
||||
final payload = {
|
||||
'name': name.text,
|
||||
'description': description.text,
|
||||
'author': author,
|
||||
'ip': ip,
|
||||
'version': version,
|
||||
'password': hasPassword ? hashPassword(passwordText) : null,
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
'discoverable': discoverable.value
|
||||
};
|
||||
if(published()) {
|
||||
await hosts.update(payload).eq("id", uuid);
|
||||
}else {
|
||||
payload["id"] = uuid;
|
||||
await hosts.insert(payload);
|
||||
}
|
||||
|
||||
published.value = true;
|
||||
}catch(error) {
|
||||
published.value = false;
|
||||
}finally {
|
||||
_publishingSemaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> discardServer() async {
|
||||
try {
|
||||
_publishingSemaphore.acquire();
|
||||
if(!published.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
await supabase.from("hosting")
|
||||
.delete()
|
||||
.match({'id': uuid});
|
||||
published.value = false;
|
||||
}catch(_) {
|
||||
published.value = true;
|
||||
}finally {
|
||||
_publishingSemaphore.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
463
gui/lib/src/messenger/implementation/version.dart
Normal file
463
gui/lib/src/messenger/implementation/version.dart
Normal file
@@ -0,0 +1,463 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:universal_disk_space/universal_disk_space.dart';
|
||||
import 'package:windows_taskbar/windows_taskbar.dart';
|
||||
|
||||
class AddVersionDialog extends StatefulWidget {
|
||||
final bool closable;
|
||||
const AddVersionDialog({Key? key, required this.closable}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AddVersionDialog> createState() => _AddVersionDialogState();
|
||||
}
|
||||
|
||||
class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final TextEditingController _pathController = TextEditingController();
|
||||
final GlobalKey<FormState> _formKey = GlobalKey();
|
||||
final GlobalKey<FormFieldState> _formFieldKey = GlobalKey();
|
||||
|
||||
final Rx<_DownloadStatus> _status = Rx(_DownloadStatus.form);
|
||||
final Rx<_BuildSource> _source = Rx(_BuildSource.githubArchive);
|
||||
final Rxn<FortniteBuild> _build = Rxn();
|
||||
final RxnInt _timeLeft = RxnInt();
|
||||
final Rxn<double> _progress = Rxn();
|
||||
|
||||
late DiskSpace _diskSpace;
|
||||
late Future<List<FortniteBuild>> _fetchFuture;
|
||||
late Future _diskFuture;
|
||||
|
||||
Isolate? _isolate;
|
||||
SendPort? _downloadPort;
|
||||
Object? _error;
|
||||
StackTrace? _stackTrace;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_fetchFuture = compute(fetchBuilds, null);
|
||||
_diskSpace = DiskSpace();
|
||||
_diskFuture = _diskSpace.scan()
|
||||
.then((_) => _updateFormDefaults());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pathController.dispose();
|
||||
_cancelDownload();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _cancelDownload() {
|
||||
Process.run('${assetsDirectory.path}\\build\\stop.bat', []);
|
||||
_downloadPort?.send(kStopBuildDownloadSignal);
|
||||
_isolate?.kill(priority: Isolate.immediate);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Form(
|
||||
key: _formKey,
|
||||
child: Obx(() {
|
||||
switch(_status.value){
|
||||
case _DownloadStatus.form:
|
||||
return FutureBuilder(
|
||||
future: Future.wait([_fetchFuture, _diskFuture]).then((_) async => await _fetchFuture),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _onDownloadError(snapshot.error, snapshot.stackTrace));
|
||||
}
|
||||
|
||||
final data = snapshot.data;
|
||||
if (data == null) {
|
||||
return ProgressDialog(
|
||||
text: translations.fetchingBuilds,
|
||||
showButton: widget.closable,
|
||||
onStop: () => Navigator.of(context).pop()
|
||||
);
|
||||
}
|
||||
|
||||
return Obx(() => FormDialog(
|
||||
content: _buildFormBody(data),
|
||||
buttons: _formButtons
|
||||
));
|
||||
}
|
||||
);
|
||||
case _DownloadStatus.downloading:
|
||||
case _DownloadStatus.extracting:
|
||||
return GenericDialog(
|
||||
header: _progressBody,
|
||||
buttons: _stopButton
|
||||
);
|
||||
case _DownloadStatus.error:
|
||||
return ErrorDialog(
|
||||
exception: _error ?? Exception(translations.unknownError),
|
||||
stackTrace: _stackTrace,
|
||||
errorMessageBuilder: (exception) => translations.downloadVersionError(exception.toString())
|
||||
);
|
||||
case _DownloadStatus.done:
|
||||
return InfoDialog(
|
||||
text: translations.downloadedVersion
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
List<DialogButton> get _formButtons => [
|
||||
if(widget.closable)
|
||||
DialogButton(type: ButtonType.secondary),
|
||||
DialogButton(
|
||||
text: _source.value == _BuildSource.local ? translations.saveLocalVersion : translations.download,
|
||||
type: widget.closable ? ButtonType.primary : ButtonType.only,
|
||||
color: FluentTheme.of(context).accentColor,
|
||||
onTap: () => _startDownload(context),
|
||||
)
|
||||
];
|
||||
|
||||
void _startDownload(BuildContext context) async {
|
||||
try {
|
||||
final topResult = _formKey.currentState?.validate();
|
||||
if(topResult != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
final fieldResult = _formFieldKey.currentState?.validate();
|
||||
if(fieldResult != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
final build = _build.value;
|
||||
if(build == null){
|
||||
return;
|
||||
}
|
||||
|
||||
final source = _source.value;
|
||||
if(source == _BuildSource.local) {
|
||||
Navigator.of(context).pop();
|
||||
_addFortniteVersion(build);
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = _DownloadStatus.downloading;
|
||||
final communicationPort = ReceivePort();
|
||||
communicationPort.listen((message) {
|
||||
if(message is FortniteBuildDownloadProgress) {
|
||||
_onProgress(build, message.progress, message.minutesLeft, message.extracting);
|
||||
}else if(message is SendPort) {
|
||||
_downloadPort = message;
|
||||
}else {
|
||||
_onDownloadError(message, null);
|
||||
}
|
||||
});
|
||||
final options = FortniteBuildDownloadOptions(
|
||||
build,
|
||||
Directory(_pathController.text),
|
||||
communicationPort.sendPort
|
||||
);
|
||||
final errorPort = ReceivePort();
|
||||
errorPort.listen((message) => _onDownloadError(message, null));
|
||||
_isolate = await Isolate.spawn(
|
||||
downloadArchiveBuild,
|
||||
options,
|
||||
onError: errorPort.sendPort,
|
||||
errorsAreFatal: true
|
||||
);
|
||||
} catch (exception, stackTrace) {
|
||||
_onDownloadError(exception, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDownloadComplete(FortniteBuild build) async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = _DownloadStatus.done;
|
||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||
_addFortniteVersion(build);
|
||||
}
|
||||
|
||||
void _addFortniteVersion(FortniteBuild build) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion(
|
||||
content: build.version,
|
||||
location: Directory(_pathController.text)
|
||||
)));
|
||||
}
|
||||
|
||||
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
||||
_cancelDownload();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = _DownloadStatus.error;
|
||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||
_error = error;
|
||||
_stackTrace = stackTrace;
|
||||
}
|
||||
|
||||
void _onProgress(FortniteBuild build, double progress, int? timeLeft, bool extracting) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(progress >= 100 && extracting) {
|
||||
_onDownloadComplete(build);
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = extracting ? _DownloadStatus.extracting : _DownloadStatus.downloading;
|
||||
if(progress >= 0) {
|
||||
WindowsTaskbar.setProgress(progress.round(), 100);
|
||||
}
|
||||
|
||||
_timeLeft.value = timeLeft;
|
||||
_progress.value = progress;
|
||||
}
|
||||
|
||||
Widget get _progressBody {
|
||||
final timeLeft = _timeLeft.value;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
_status.value == _DownloadStatus.downloading ? translations.downloading : translations.extracting,
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
translations.buildProgress((_progress.value ?? 0).round()),
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
),
|
||||
|
||||
if(timeLeft != null)
|
||||
Text(
|
||||
translations.timeLeft(timeLeft),
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar(value: (_progress.value ?? 0).toDouble())
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormBody(List<FortniteBuild> builds) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSourceSelector(),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
_buildBuildSelector(builds),
|
||||
|
||||
FileSelector(
|
||||
label: translations.gameFolderTitle,
|
||||
placeholder: _source.value == _BuildSource.local ? translations.gameFolderPlaceholder : translations.buildInstallationDirectoryPlaceholder,
|
||||
windowTitle: _source.value == _BuildSource.local ? translations.gameFolderPlaceWindowTitle : translations.buildInstallationDirectoryWindowTitle,
|
||||
controller: _pathController,
|
||||
validator: _source.value == _BuildSource.local ? _checkGameFolder : _checkDownloadDestination,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
String? _checkGameFolder(text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyGamePath;
|
||||
}
|
||||
|
||||
final directory = Directory(text);
|
||||
if (!directory.existsSync()) {
|
||||
return translations.directoryDoesNotExist;
|
||||
}
|
||||
|
||||
if (FortniteVersionExtension.findFile(directory, "FortniteClient-Win64-Shipping.exe") == null) {
|
||||
return translations.missingShippingExe;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _checkDownloadDestination(text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.invalidDownloadPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Widget _buildBuildSelector(List<FortniteBuild> builds) => InfoLabel(
|
||||
label: translations.build,
|
||||
child: FormField<FortniteBuild?>(
|
||||
key: _formFieldKey,
|
||||
validator: (data) => _checkBuild(data),
|
||||
builder: (formContext) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ComboBox<FortniteBuild>(
|
||||
placeholder: Text(translations.selectBuild),
|
||||
isExpanded: true,
|
||||
items: (_source.value == _BuildSource.local ? builds : builds.where((build) => build.available)).map((element) => _buildBuildItem(element)).toList(),
|
||||
value: _build.value,
|
||||
onChanged: (value) {
|
||||
if(value == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_build.value = value;
|
||||
formContext.didChange(value);
|
||||
formContext.validate();
|
||||
_updateFormDefaults();
|
||||
}
|
||||
),
|
||||
if(formContext.hasError)
|
||||
const SizedBox(height: 4.0),
|
||||
if(formContext.hasError)
|
||||
Text(
|
||||
formContext.errorText ?? "",
|
||||
style: TextStyle(
|
||||
color: Colors.red.defaultBrushFor(FluentTheme.of(context).brightness)
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: formContext.hasError ? 8.0 : 16.0
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
String? _checkBuild(FortniteBuild? data) {
|
||||
if(data == null) {
|
||||
return translations.selectBuild;
|
||||
}
|
||||
|
||||
final versions = _gameController.versions.value;
|
||||
if (versions.any((element) => data.version == element.content)) {
|
||||
return translations.versionAlreadyExists;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ComboBoxItem<FortniteBuild> _buildBuildItem(FortniteBuild element) => ComboBoxItem<FortniteBuild>(
|
||||
value: element,
|
||||
child: Text(element.version.toString())
|
||||
);
|
||||
|
||||
Widget _buildSourceSelector() => InfoLabel(
|
||||
label: translations.source,
|
||||
child: ComboBox<_BuildSource>(
|
||||
placeholder: Text(translations.selectBuild),
|
||||
isExpanded: true,
|
||||
items: _BuildSource.values.map((entry) => _buildSourceItem(entry)).toList(),
|
||||
value: _source.value,
|
||||
onChanged: (value) {
|
||||
if(value == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_source.value = value;
|
||||
_updateFormDefaults();
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
ComboBoxItem<_BuildSource> _buildSourceItem(_BuildSource element) => ComboBoxItem<_BuildSource>(
|
||||
value: element,
|
||||
child: Text(element.translatedName)
|
||||
);
|
||||
|
||||
|
||||
List<DialogButton> get _stopButton => [
|
||||
DialogButton(
|
||||
text: translations.stopLoadingDialogAction,
|
||||
type: ButtonType.only
|
||||
)
|
||||
];
|
||||
|
||||
Future<void> _updateFormDefaults() async {
|
||||
if(_source.value != _BuildSource.local && _build.value?.available != true) {
|
||||
_build.value = null;
|
||||
}
|
||||
|
||||
if(_source.value != _BuildSource.local && _diskSpace.disks.isNotEmpty) {
|
||||
await _fetchFuture;
|
||||
final bestDisk = _diskSpace.disks
|
||||
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
|
||||
final build = _build.value;
|
||||
if(build == null){
|
||||
return;
|
||||
}
|
||||
|
||||
final pathText = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}";
|
||||
_pathController.text = pathText;
|
||||
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
|
||||
}
|
||||
|
||||
_formKey.currentState?.validate();
|
||||
}
|
||||
}
|
||||
|
||||
enum _DownloadStatus {
|
||||
form,
|
||||
downloading,
|
||||
extracting,
|
||||
error,
|
||||
done
|
||||
}
|
||||
|
||||
enum _BuildSource {
|
||||
local,
|
||||
githubArchive;
|
||||
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case _BuildSource.local:
|
||||
return translations.localBuild;
|
||||
case _BuildSource.githubArchive:
|
||||
return translations.githubArchive;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/keyboard.dart';
|
||||
@@ -16,7 +18,10 @@ import 'package:reboot_launcher/src/widget/server_type_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../dialog/implementation/data.dart';
|
||||
final GlobalKey<OverlayTargetState> backendTypeOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> backendGameServerAddressOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> backendUnrealEngineOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> backendDetachedOverlayTargetKey = GlobalKey();
|
||||
|
||||
class BackendPage extends RebootPage {
|
||||
const BackendPage({Key? key}) : super(key: key);
|
||||
@@ -84,10 +89,13 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
),
|
||||
title: Text(translations.matchmakerConfigurationAddressName),
|
||||
subtitle: Text(translations.matchmakerConfigurationAddressDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.matchmakerConfigurationAddressName,
|
||||
controller: _backendController.gameServerAddress,
|
||||
focusNode: _backendController.gameServerAddressFocusNode
|
||||
content: OverlayTarget(
|
||||
key: backendGameServerAddressOverlayTargetKey,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.matchmakerConfigurationAddressName,
|
||||
controller: _backendController.gameServerAddress,
|
||||
focusNode: _backendController.gameServerAddressFocusNode
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -152,9 +160,12 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
ToggleSwitch(
|
||||
checked: _backendController.detached(),
|
||||
onChanged: (value) => _backendController.detached.value = value
|
||||
OverlayTarget(
|
||||
key: backendDetachedOverlayTargetKey,
|
||||
child: ToggleSwitch(
|
||||
checked: _backendController.detached(),
|
||||
onChanged: (value) => _backendController.detached.value = value
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -173,14 +184,18 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
title: Text(translations.settingsClientConsoleKeyName),
|
||||
subtitle: Text(translations.settingsClientConsoleKeyDescription),
|
||||
contentWidth: null,
|
||||
content: Button(
|
||||
onPressed: () {
|
||||
_infoBarEntry = showInfoBar(
|
||||
translations.clickKey,
|
||||
loading: true
|
||||
);
|
||||
},
|
||||
child: Text(_gameController.consoleKey.value.unrealEnginePrettyName ?? ""),
|
||||
content: OverlayTarget(
|
||||
key: backendUnrealEngineOverlayTargetKey,
|
||||
child: Button(
|
||||
onPressed: () {
|
||||
_infoBarEntry = showRebootInfoBar(
|
||||
translations.clickKey,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
},
|
||||
child: Text(_gameController.consoleKey.value.unrealEnginePrettyName ?? ""),
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -221,7 +236,9 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
),
|
||||
title: Text(translations.backendTypeName),
|
||||
subtitle: Text(translations.backendTypeDescription),
|
||||
content: const ServerTypeSelector()
|
||||
content: ServerTypeSelector(
|
||||
overlayKey: backendTypeOverlayTargetKey
|
||||
)
|
||||
);
|
||||
|
||||
@override
|
||||
|
||||
359
gui/lib/src/page/implementation/browser_page.dart
Normal file
359
gui/lib/src/page/implementation/browser_page.dart
Normal file
@@ -0,0 +1,359 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart' as fluentUiIcons;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
|
||||
class BrowsePage extends RebootPage {
|
||||
const BrowsePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
String get name => translations.browserName;
|
||||
|
||||
@override
|
||||
RebootPageType get type => RebootPageType.browser;
|
||||
|
||||
@override
|
||||
String get iconAsset => "assets/images/server_browser.png";
|
||||
|
||||
@override
|
||||
bool hasButton(String? pageName) => false;
|
||||
|
||||
@override
|
||||
RebootPageState<BrowsePage> createState() => _BrowsePageState();
|
||||
}
|
||||
|
||||
class _BrowsePageState extends RebootPageState<BrowsePage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final TextEditingController _filterController = TextEditingController();
|
||||
final StreamController<String> _filterControllerStream = StreamController.broadcast();
|
||||
|
||||
final Rx<_Filter> _filter = Rx(_Filter.all);
|
||||
final Rx<_Sort> _sort = Rx(_Sort.timeDescending);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Obx(() {
|
||||
final data = _hostingController.servers.value
|
||||
?.where((entry) => (kDebugMode || entry.id != _hostingController.uuid) && entry.discoverable)
|
||||
.toSet();
|
||||
if(data == null || data.isEmpty == true) {
|
||||
return _noServers;
|
||||
}
|
||||
|
||||
return _buildPageBody(data);
|
||||
});
|
||||
}
|
||||
|
||||
Widget get _noServers => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translations.noServersAvailableTitle,
|
||||
style: FluentTheme.of(context).typography.titleLarge,
|
||||
),
|
||||
Text(
|
||||
translations.noServersAvailableSubtitle,
|
||||
style: FluentTheme.of(context).typography.body
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildPageBody(Set<FortniteServer> data) => StreamBuilder(
|
||||
stream: _filterControllerStream.stream,
|
||||
builder: (context, filterSnapshot) {
|
||||
final items = data.where((entry) => _isValidItem(entry, filterSnapshot.data)).toSet();
|
||||
return Column(
|
||||
children: [
|
||||
_searchBar,
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_buildFilter(context),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
_buildSort(context),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Expanded(
|
||||
child: _buildPopulatedListBody(items)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Widget _buildSort(BuildContext context) => Row(
|
||||
children: [
|
||||
Icon(
|
||||
fluentUiIcons.FluentIcons.arrow_sort_24_regular,
|
||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Text(
|
||||
"Sort by: ",
|
||||
style: TextStyle(
|
||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Obx(() => SizedBox(
|
||||
width: 230,
|
||||
child: DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(
|
||||
_sort.value.translatedName,
|
||||
textAlign: TextAlign.start
|
||||
),
|
||||
title: const Spacer(),
|
||||
items: _Sort.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.translatedName),
|
||||
onPressed: () => _sort.value = entry
|
||||
)).toList()
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
|
||||
Row _buildFilter(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
fluentUiIcons.FluentIcons.filter_24_regular,
|
||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Text(
|
||||
"Filter by: ",
|
||||
style: TextStyle(
|
||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Obx(() => SizedBox(
|
||||
width: 125,
|
||||
child: DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(
|
||||
_filter.value.translatedName,
|
||||
textAlign: TextAlign.start
|
||||
),
|
||||
title: const Spacer(),
|
||||
items: _Filter.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.translatedName),
|
||||
onPressed: () => _filter.value = entry
|
||||
)).toList()
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPopulatedListBody(Set<FortniteServer> items) => Obx(() {
|
||||
final filter = _filter.value;
|
||||
final sorted = items.where((element) {
|
||||
switch(filter) {
|
||||
case _Filter.all:
|
||||
return true;
|
||||
case _Filter.accessible:
|
||||
return element.password == null;
|
||||
case _Filter.playable:
|
||||
return _gameController.getVersionByName(element.version) != null;
|
||||
}
|
||||
}).toList();
|
||||
final sort = _sort.value;
|
||||
sorted.sort((first, second) {
|
||||
switch(sort) {
|
||||
case _Sort.timeAscending:
|
||||
return first.timestamp.compareTo(second.timestamp);
|
||||
case _Sort.timeDescending:
|
||||
return second.timestamp.compareTo(first.timestamp);
|
||||
case _Sort.nameAscending:
|
||||
return first.name.compareTo(second.name);
|
||||
case _Sort.nameDescending:
|
||||
return second.name.compareTo(first.name);
|
||||
}
|
||||
});
|
||||
if(sorted.isEmpty) {
|
||||
return _noServersByQuery;
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: sorted.length,
|
||||
itemBuilder: (context, index) {
|
||||
final entry = sorted.elementAt(index);
|
||||
final hasPassword = entry.password != null;
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
hasPassword ? FluentIcons.lock : FluentIcons.globe
|
||||
),
|
||||
title: Text("${_formatName(entry)} • ${entry.author}"),
|
||||
subtitle: Text("${_formatDescription(entry)} • ${_formatVersion(entry)}"),
|
||||
content: Button(
|
||||
onPressed: () => _backendController.joinServerInteractive(_hostingController.uuid, entry),
|
||||
child: Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Widget get _noServersByQuery => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translations.noServersAvailableByQueryTitle,
|
||||
style: FluentTheme.of(context).typography.titleLarge,
|
||||
),
|
||||
Text(
|
||||
translations.noServersAvailableByQuerySubtitle,
|
||||
style: FluentTheme.of(context).typography.body
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
bool _isValidItem(FortniteServer entry, String? filter) =>
|
||||
filter == null || filter.isEmpty || _filterServer(entry, filter);
|
||||
|
||||
bool _filterServer(FortniteServer element, String filter) {
|
||||
filter = filter.toLowerCase();
|
||||
|
||||
final uri = Uri.tryParse(filter);
|
||||
if(uri != null && uri.host.isNotEmpty && element.id.toLowerCase().contains(uri.host.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return element.id.toLowerCase().contains(filter.toLowerCase())
|
||||
|| element.name.toLowerCase().contains(filter)
|
||||
|| element.author.toLowerCase().contains(filter)
|
||||
|| element.description.toLowerCase().contains(filter);
|
||||
}
|
||||
|
||||
Widget get _searchBar => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 350
|
||||
),
|
||||
child: TextBox(
|
||||
placeholder: translations.findServer,
|
||||
controller: _filterController,
|
||||
autofocus: true,
|
||||
onChanged: (value) => _filterControllerStream.add(value),
|
||||
suffix: _searchBarIcon,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _searchBarIcon => Button(
|
||||
onPressed: _filterController.text.isEmpty ? null : () {
|
||||
_filterController.clear();
|
||||
_filterControllerStream.add("");
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: ButtonState.all(Colors.transparent),
|
||||
shape: ButtonState.all(Border())
|
||||
),
|
||||
child: _searchBarIconData
|
||||
);
|
||||
|
||||
Widget get _searchBarIconData {
|
||||
final color = FluentTheme.of(context).resources.textFillColorPrimary;
|
||||
if (_filterController.text.isNotEmpty) {
|
||||
return Icon(
|
||||
FluentIcons.clear,
|
||||
size: 8.0,
|
||||
color: color
|
||||
);
|
||||
}
|
||||
|
||||
return Transform.flip(
|
||||
flipX: true,
|
||||
child: Icon(
|
||||
FluentIcons.search,
|
||||
size: 12.0,
|
||||
color: color
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatName(FortniteServer server) {
|
||||
final result = server.name;
|
||||
return result.isEmpty ? translations.defaultServerName : result;
|
||||
}
|
||||
|
||||
String _formatDescription(FortniteServer server) {
|
||||
final result = server.description;
|
||||
return result.isEmpty ? translations.defaultServerDescription : result;
|
||||
}
|
||||
|
||||
String _formatVersion(FortniteServer server) => "Fortnite ${server.version.toString()}";
|
||||
|
||||
@override
|
||||
Widget? get button => null;
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [];
|
||||
}
|
||||
|
||||
enum _Filter {
|
||||
all,
|
||||
accessible,
|
||||
playable;
|
||||
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case _Filter.all:
|
||||
return translations.all;
|
||||
case _Filter.accessible:
|
||||
return translations.accessible;
|
||||
case _Filter.playable:
|
||||
return translations.playable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum _Sort {
|
||||
timeAscending,
|
||||
timeDescending,
|
||||
nameAscending,
|
||||
nameDescending;
|
||||
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case _Sort.timeAscending:
|
||||
return translations.timeAscending;
|
||||
case _Sort.timeDescending:
|
||||
return translations.timeDescending;
|
||||
case _Sort.nameAscending:
|
||||
return translations.nameAscending;
|
||||
case _Sort.nameDescending:
|
||||
return translations.nameDescending;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,15 +12,14 @@ import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/dll.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/dll.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_suggestion.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/dll.dart';
|
||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
@@ -29,9 +28,12 @@ import 'package:reboot_launcher/src/widget/profile_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/title_bar.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import 'info_page.dart';
|
||||
final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey();
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
static const double kDefaultPadding = 12.0;
|
||||
static const double kTitleBarHeight = 32;
|
||||
|
||||
const HomePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -39,16 +41,14 @@ class HomePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
|
||||
static const double _kDefaultPadding = 12.0;
|
||||
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
final GlobalKey _searchKey = GlobalKey();
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final RxBool _focused = RxBool(true);
|
||||
final PageController _pageController = PageController(keepPage: true, initialPage: pageIndex.value);
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
@@ -56,7 +56,9 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
windowManager.setPreventClose(true);
|
||||
windowManager.addListener(this);
|
||||
_syncPageViewWithNavigator();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkUpdates();
|
||||
_initAppLink();
|
||||
@@ -64,6 +66,18 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
});
|
||||
}
|
||||
|
||||
void _syncPageViewWithNavigator() {
|
||||
var lastPage = pageIndex.value;
|
||||
pageIndex.listen((index) {
|
||||
if(index == lastPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastPage = index;
|
||||
_pageController.jumpToPage(index);
|
||||
});
|
||||
}
|
||||
|
||||
void _initAppLink() async {
|
||||
final appLinks = AppLinks();
|
||||
final initialUrl = await appLinks.getInitialLink();
|
||||
@@ -78,9 +92,9 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
final uuid = uri.host;
|
||||
final server = _hostingController.findServerById(uuid);
|
||||
if(server != null) {
|
||||
_backendController.joinServer(_hostingController.uuid, server);
|
||||
_backendController.joinServerInteractive(_hostingController.uuid, server);
|
||||
}else {
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.noServerFound,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
@@ -100,10 +114,9 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
return;
|
||||
}
|
||||
|
||||
var oldOwner = _backendController.gameServerOwner.value;
|
||||
_backendController.joinLocalHost();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
|
||||
oldOwner == null ? translations.serverNoLongerAvailableUnnamed : translations.serverNoLongerAvailable(oldOwner),
|
||||
_backendController.joinLocalhost();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar(
|
||||
translations.serverNoLongerAvailableUnnamed,
|
||||
severity: InfoBarSeverity.warning,
|
||||
duration: infoBarLongDuration
|
||||
));
|
||||
@@ -114,27 +127,34 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
}
|
||||
|
||||
void _checkUpdates() {
|
||||
_updateController.notifyLauncherUpdate();
|
||||
_settingsController.notifyLauncherUpdate();
|
||||
|
||||
if(!dllsDirectory.existsSync()) {
|
||||
dllsDirectory.createSync(recursive: true);
|
||||
}
|
||||
|
||||
for(final injectable in InjectableDll.values) {
|
||||
downloadCriticalDllInteractive(
|
||||
injectable.path,
|
||||
silent: true
|
||||
);
|
||||
final (file, custom) = _settingsController.getInjectableData(injectable);
|
||||
if(!custom) {
|
||||
_settingsController.downloadCriticalDllInteractive(
|
||||
file.path,
|
||||
silent: true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
watchDlls().listen((filePath) => showDllDeletedDialog(() {
|
||||
downloadCriticalDllInteractive(filePath);
|
||||
_settingsController.downloadCriticalDllInteractive(filePath);
|
||||
}));
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowClose() {
|
||||
exit(0); // Force closing
|
||||
void onWindowClose() async {
|
||||
try {
|
||||
await _hostingController.discardServer();
|
||||
}finally {
|
||||
exit(0); // Force closing
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -153,7 +173,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
|
||||
@override
|
||||
void onWindowBlur() {
|
||||
_focused.value = false;
|
||||
_focused.value = !_focused.value;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -218,137 +238,364 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
_focused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowEvent(String eventName) {
|
||||
if(eventName != "move") {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => log("[WINDOW] Event: $eventName ${_focused.value}"));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
_settingsController.language.value;
|
||||
loadTranslations(context);
|
||||
// InfoPage.initInfoTiles();
|
||||
return Obx(() {
|
||||
return NavigationPaneTheme(
|
||||
data: NavigationPaneThemeData(
|
||||
backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
|
||||
),
|
||||
child: NavigationView(
|
||||
paneBodyBuilder: (pane, body) => _PaneBody(
|
||||
padding: _kDefaultPadding,
|
||||
controller: pagesController,
|
||||
body: body
|
||||
),
|
||||
appBar: NavigationAppBar(
|
||||
height: 32,
|
||||
title: _draggableArea,
|
||||
actions: WindowTitleBar(focused: _focused()),
|
||||
leading: _backButton,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
pane: NavigationPane(
|
||||
selected: pageIndex.value,
|
||||
onChanged: (index) {
|
||||
final lastPageIndex = pageIndex.value;
|
||||
if(lastPageIndex != index) {
|
||||
pageIndex.value = index;
|
||||
}else if(pageStack.isNotEmpty) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
pagesController.add(null);
|
||||
}
|
||||
},
|
||||
menuButton: const SizedBox(),
|
||||
displayMode: PaneDisplayMode.open,
|
||||
items: _items,
|
||||
customPane: _CustomPane(_settingsController),
|
||||
header: const ProfileWidget(),
|
||||
autoSuggestBox: _autoSuggestBox,
|
||||
indicator: const StickyNavigationIndicator(
|
||||
duration: Duration(milliseconds: 500),
|
||||
curve: Curves.easeOut,
|
||||
indicatorSize: 3.25
|
||||
return Container(
|
||||
color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: HomePage.kTitleBarHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
_backButton,
|
||||
Expanded(child: _draggableArea),
|
||||
WindowTitleBar(focused: _focused())
|
||||
],
|
||||
)
|
||||
),
|
||||
Expanded(
|
||||
child: Navigator(
|
||||
key: appNavigatorKey,
|
||||
onPopPage: (page, data) => false,
|
||||
pages: [
|
||||
MaterialPage(
|
||||
child: Overlay(
|
||||
key: appOverlayKey,
|
||||
initialEntries: [
|
||||
OverlayEntry(
|
||||
maintainState: true,
|
||||
builder: (context) => Row(
|
||||
children: [
|
||||
_buildLateralView(),
|
||||
_buildBody()
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
contentShape: const RoundedRectangleBorder(),
|
||||
onOpenSearch: () => _searchFocusNode.requestFocus(),
|
||||
transitionBuilder: (child, animation) => child
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: HomePage.kDefaultPadding,
|
||||
right: HomePage.kDefaultPadding * 2,
|
||||
top: HomePage.kDefaultPadding,
|
||||
bottom: HomePage.kDefaultPadding * 2
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 1000
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildBodyHeader(),
|
||||
const SizedBox(height: 24.0),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
children: [
|
||||
_buildBodyContent(),
|
||||
InfoBarArea(
|
||||
key: infoBarAreaKey
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBodyContent() => PageView.builder(
|
||||
controller: _pageController,
|
||||
itemBuilder: (context, index) => Navigator(
|
||||
onPopPage: (page, data) => true,
|
||||
observers: [
|
||||
_NestedPageObserver(
|
||||
onChanged: (routeName) {
|
||||
if(routeName != null) {
|
||||
pageIndex.refresh();
|
||||
addSubPageToStack(routeName);
|
||||
pagesController.add(null);
|
||||
}
|
||||
}
|
||||
)
|
||||
],
|
||||
pages: [
|
||||
MaterialPage(
|
||||
child: KeyedSubtree(
|
||||
key: getPageKeyByIndex(index),
|
||||
child: pages[index]
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
itemCount: pages.length
|
||||
);
|
||||
|
||||
Widget _buildBodyHeader() {
|
||||
final themeMode = _settingsController.themeMode.value;
|
||||
final inactiveColor = themeMode == ThemeMode.dark
|
||||
|| (themeMode == ThemeMode.system && isDarkMode) ? Colors.grey[60] : Colors.grey[100];
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: StreamBuilder(
|
||||
stream: pagesController.stream,
|
||||
builder: (context, _) {
|
||||
final elements = <TextSpan>[];
|
||||
elements.add(_buildBodyHeaderRootPage(inactiveColor));
|
||||
for(var i = pageStack.length - 1; i >= 0; i--) {
|
||||
var innerPage = pageStack.elementAt(i);
|
||||
innerPage = innerPage.substring(innerPage.indexOf("_") + 1);
|
||||
elements.add(_buildBodyHeaderPageSeparator(inactiveColor));
|
||||
elements.add(_buildBodyHeaderNestedPage(innerPage, i, inactiveColor));
|
||||
}
|
||||
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: elements
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w600
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TextSpan _buildBodyHeaderRootPage(Color inactiveColor) => TextSpan(
|
||||
text: pages[pageIndex.value].name,
|
||||
recognizer: pageStack.isNotEmpty ? (TapGestureRecognizer()..onTap = () {
|
||||
if(inDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(var i = 0; i < pageStack.length; i++) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
}
|
||||
|
||||
pagesController.add(null);
|
||||
}) : null,
|
||||
style: TextStyle(
|
||||
color: pageStack.isNotEmpty ? inactiveColor : null
|
||||
)
|
||||
);
|
||||
|
||||
TextSpan _buildBodyHeaderPageSeparator(Color inactiveColor) => TextSpan(
|
||||
text: " > ",
|
||||
style: TextStyle(
|
||||
color: inactiveColor
|
||||
)
|
||||
);
|
||||
|
||||
TextSpan _buildBodyHeaderNestedPage(String nestedPageName, int nestedPageIndex, Color inactiveColor) => TextSpan(
|
||||
text: nestedPageName,
|
||||
recognizer: nestedPageIndex == pageStack.length - 1 ? null : (TapGestureRecognizer()..onTap = () {
|
||||
if(inDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(var j = 0; j < nestedPageIndex - 1; j++) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
}
|
||||
pagesController.add(null);
|
||||
}),
|
||||
style: TextStyle(
|
||||
color: nestedPageIndex == pageStack.length - 1 ? null : inactiveColor
|
||||
)
|
||||
);
|
||||
|
||||
Widget _buildLateralView() => SizedBox(
|
||||
width: 310,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ProfileWidget(
|
||||
overlayKey: profileOverlayKey
|
||||
),
|
||||
_autoSuggestBox,
|
||||
const SizedBox(height: 12.0),
|
||||
_buildNavigationTrail()
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildNavigationTrail() => Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0
|
||||
),
|
||||
child: Scrollbar(
|
||||
child: ListView.separated(
|
||||
primary: true,
|
||||
itemCount: pages.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
height: 4.0
|
||||
),
|
||||
itemBuilder: (context, index) => _buildNavigationItem(pages[index]),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
Widget _buildNavigationItem(RebootPage page) {
|
||||
final index = page.type.index;
|
||||
return OverlayTarget(
|
||||
key: getOverlayTargetKeyByPage(index),
|
||||
child: HoverButton(
|
||||
onPressed: () {
|
||||
final lastPageIndex = pageIndex.value;
|
||||
if(lastPageIndex != index) {
|
||||
pageIndex.value = index;
|
||||
}else if(pageStack.isNotEmpty) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
pagesController.add(null);
|
||||
}
|
||||
},
|
||||
builder: (context, states) => Obx(() => Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context),
|
||||
pageIndex.value == index ? {ButtonStates.hovering} : states,
|
||||
transparentWhenNone: true,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6.0))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox.square(
|
||||
dimension: 24,
|
||||
child: Image.asset(page.iconAsset)
|
||||
),
|
||||
const SizedBox(width: 12.0),
|
||||
Text(page.name)
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _backButton => StreamBuilder(
|
||||
stream: pagesController.stream,
|
||||
builder: (context, _) => Button(
|
||||
style: ButtonStyle(
|
||||
padding: ButtonState.all(const EdgeInsets.only(top: 6.0)),
|
||||
backgroundColor: ButtonState.all(Colors.transparent),
|
||||
shape: ButtonState.all(Border())
|
||||
),
|
||||
onPressed: appStack.isEmpty && !inDialog ? null : () {
|
||||
if(inDialog) {
|
||||
Navigator.of(appKey.currentContext!).pop();
|
||||
}else {
|
||||
final lastPage = appStack.removeLast();
|
||||
pageStack.remove(lastPage);
|
||||
if (lastPage is int) {
|
||||
hitBack = true;
|
||||
pageIndex.value = lastPage;
|
||||
} else {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
}
|
||||
style: ButtonStyle(
|
||||
padding: ButtonState.all(const EdgeInsets.symmetric(
|
||||
vertical: 12.0,
|
||||
horizontal: 16.0
|
||||
)),
|
||||
backgroundColor: ButtonState.all(Colors.transparent),
|
||||
shape: ButtonState.all(Border())
|
||||
),
|
||||
onPressed: appStack.isEmpty && !inDialog ? null : () {
|
||||
if(inDialog) {
|
||||
Navigator.of(appNavigatorKey.currentContext!).pop();
|
||||
}else {
|
||||
final lastPage = appStack.removeLast();
|
||||
pageStack.remove(lastPage);
|
||||
if (lastPage is int) {
|
||||
hitBack = true;
|
||||
pageIndex.value = lastPage;
|
||||
} else {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
}
|
||||
pagesController.add(null);
|
||||
},
|
||||
child: const Icon(FluentIcons.back, size: 12.0),
|
||||
)
|
||||
}
|
||||
pagesController.add(null);
|
||||
},
|
||||
child: const Icon(FluentIcons.back, size: 12.0),
|
||||
)
|
||||
);
|
||||
|
||||
GestureDetector get _draggableArea => GestureDetector(
|
||||
onDoubleTap: appWindow.maximizeOrRestore,
|
||||
onHorizontalDragStart: (_) => appWindow.startDragging(),
|
||||
onVerticalDragStart: (_) => appWindow.startDragging()
|
||||
onHorizontalDragStart: (_) => windowManager.startDragging(),
|
||||
onVerticalDragStart: (_) => windowManager.startDragging()
|
||||
);
|
||||
|
||||
Widget get _autoSuggestBox => Obx(() {
|
||||
final firstRun = _settingsController.firstRun.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0
|
||||
Widget get _autoSuggestBox => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0
|
||||
),
|
||||
child: AutoSuggestBox<PageSuggestion>(
|
||||
key: _searchKey,
|
||||
controller: _searchController,
|
||||
placeholder: translations.find,
|
||||
focusNode: _searchFocusNode,
|
||||
selectionHeightStyle: BoxHeightStyle.max,
|
||||
itemBuilder: (context, item) => ListTile(
|
||||
onPressed: () {
|
||||
pageIndex.value = item.value.pageIndex;
|
||||
_searchController.clear();
|
||||
_searchFocusNode.unfocus();
|
||||
},
|
||||
leading: item.child,
|
||||
title: Text(
|
||||
item.value.name,
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1
|
||||
)
|
||||
),
|
||||
child: AutoSuggestBox<PageSuggestion>(
|
||||
key: _searchKey,
|
||||
controller: _searchController,
|
||||
enabled: !firstRun,
|
||||
placeholder: translations.find,
|
||||
focusNode: _searchFocusNode,
|
||||
selectionHeightStyle: BoxHeightStyle.max,
|
||||
itemBuilder: (context, item) => ListTile(
|
||||
onPressed: () {
|
||||
pageIndex.value = item.value.pageIndex;
|
||||
_searchController.clear();
|
||||
_searchFocusNode.unfocus();
|
||||
},
|
||||
leading: item.child,
|
||||
title: Text(
|
||||
item.value.name,
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1
|
||||
)
|
||||
),
|
||||
items: _suggestedItems,
|
||||
autofocus: true,
|
||||
trailingIcon: IgnorePointer(
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
icon: Transform.flip(
|
||||
flipX: true,
|
||||
child: const Icon(FluentIcons.search)
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
items: _suggestedItems,
|
||||
autofocus: true,
|
||||
trailingIcon: IgnorePointer(
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
icon: Transform.flip(
|
||||
flipX: true,
|
||||
child: const Icon(FluentIcons.search)
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
List<AutoSuggestBoxItem<PageSuggestion>> get _suggestedItems => pages.mapMany((page) {
|
||||
final pageIcon = SizedBox.square(
|
||||
@@ -367,282 +614,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
));
|
||||
return results;
|
||||
}).toList();
|
||||
|
||||
List<NavigationPaneItem> get _items => pages.map((page) => _createItem(page)).toList();
|
||||
|
||||
NavigationPaneItem _createItem(RebootPage page) => PaneItem(
|
||||
title: Text(page.name),
|
||||
icon: SizedBox.square(
|
||||
dimension: 24,
|
||||
child: Image.asset(page.iconAsset)
|
||||
),
|
||||
body: page
|
||||
);
|
||||
}
|
||||
|
||||
class _PaneBody extends StatefulWidget {
|
||||
const _PaneBody({
|
||||
required this.padding,
|
||||
required this.controller,
|
||||
required this.body
|
||||
});
|
||||
|
||||
final double padding;
|
||||
final StreamController<void> controller;
|
||||
final Widget? body;
|
||||
|
||||
@override
|
||||
State<_PaneBody> createState() => _PaneBodyState();
|
||||
}
|
||||
|
||||
class _PaneBodyState extends State<_PaneBody> with AutomaticKeepAliveClientMixin {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final PageController _pageController = PageController(keepPage: true, initialPage: pageIndex.value);
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
var lastPage = pageIndex.value;
|
||||
pageIndex.listen((index) {
|
||||
if(index == lastPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastPage = index;
|
||||
_pageController.jumpToPage(index);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final themeMode = _settingsController.themeMode.value;
|
||||
final inactiveColor = themeMode == ThemeMode.dark
|
||||
|| (themeMode == ThemeMode.system && isDarkMode) ? Colors.grey[60] : Colors.grey[100];
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: widget.padding,
|
||||
right: widget.padding * 2,
|
||||
top: widget.padding,
|
||||
bottom: widget.padding * 2
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 1000
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: StreamBuilder(
|
||||
stream: widget.controller.stream,
|
||||
builder: (context, _) {
|
||||
final elements = <TextSpan>[];
|
||||
elements.add(TextSpan(
|
||||
text: pages[pageIndex.value].name,
|
||||
recognizer: pageStack.isNotEmpty ? (TapGestureRecognizer()..onTap = () {
|
||||
if(inDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(var i = 0; i < pageStack.length; i++) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
}
|
||||
|
||||
widget.controller.add(null);
|
||||
}) : null,
|
||||
style: TextStyle(
|
||||
color: pageStack.isNotEmpty ? inactiveColor : null
|
||||
)
|
||||
));
|
||||
for(var i = pageStack.length - 1; i >= 0; i--) {
|
||||
var innerPage = pageStack.elementAt(i);
|
||||
innerPage = innerPage.substring(innerPage.indexOf("_") + 1);
|
||||
elements.add(TextSpan(
|
||||
text: " > ",
|
||||
style: TextStyle(
|
||||
color: inactiveColor
|
||||
)
|
||||
));
|
||||
elements.add(TextSpan(
|
||||
text: innerPage,
|
||||
recognizer: i == pageStack.length - 1 ? null : (TapGestureRecognizer()..onTap = () {
|
||||
if(inDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(var j = 0; j < i - 1; j++) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
}
|
||||
widget.controller.add(null);
|
||||
}),
|
||||
style: TextStyle(
|
||||
color: i == pageStack.length - 1 ? null : inactiveColor
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: elements
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w600
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
children: [
|
||||
PageView.builder(
|
||||
controller: _pageController,
|
||||
itemBuilder: (context, index) => Navigator(
|
||||
onPopPage: (page, data) => true,
|
||||
observers: [
|
||||
_NestedPageObserver(
|
||||
onChanged: (routeName) {
|
||||
if(routeName != null) {
|
||||
pageIndex.refresh();
|
||||
addSubPageToStack(routeName);
|
||||
widget.controller.add(null);
|
||||
}
|
||||
}
|
||||
)
|
||||
],
|
||||
pages: [
|
||||
MaterialPage(
|
||||
child: KeyedSubtree(
|
||||
key: getPageKeyByIndex(index),
|
||||
child: widget.body ?? const SizedBox.shrink()
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
itemCount: pages.length
|
||||
),
|
||||
InfoBarArea(
|
||||
key: infoBarAreaKey
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomPane extends NavigationPaneWidget {
|
||||
final SettingsController settingsController;
|
||||
_CustomPane(this.settingsController);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, NavigationPaneWidgetData data) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
data.appBar,
|
||||
Expanded(
|
||||
child: Navigator(
|
||||
key: appKey,
|
||||
onPopPage: (page, data) => false,
|
||||
pages: [
|
||||
MaterialPage(
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 310,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
data.pane.header ?? const SizedBox.shrink(),
|
||||
data.pane.autoSuggestBox ?? const SizedBox.shrink(),
|
||||
const SizedBox(height: 12.0),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0
|
||||
),
|
||||
child: Scrollbar(
|
||||
controller: data.scrollController,
|
||||
child: ListView.separated(
|
||||
controller: data.scrollController,
|
||||
itemCount: data.pane.items.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
height: 4.0
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final item = data.pane.items[index] as PaneItem;
|
||||
return Obx(() {
|
||||
final firstRun = settingsController.firstRun.value;
|
||||
return HoverButton(
|
||||
onPressed: firstRun ? null : () => data.pane.onChanged?.call(index),
|
||||
builder: (context, states) => Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context),
|
||||
item == data.pane.selectedItem ? {ButtonStates.hovering} : states,
|
||||
transparentWhenNone: true,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6.0))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
data.pane.indicator ?? const SizedBox.shrink(),
|
||||
item.icon,
|
||||
const SizedBox(width: 12.0),
|
||||
item.title ?? const SizedBox.shrink()
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: data.content
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _NestedPageObserver extends NavigatorObserver {
|
||||
|
||||
@@ -10,11 +10,10 @@ import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
@@ -23,7 +22,13 @@ import 'package:reboot_launcher/src/widget/game_start_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
||||
|
||||
import '../../util/checks.dart';
|
||||
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoNameOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoDescriptionOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoPasswordOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostShareOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<SettingTileState> hostInfoTileKey = GlobalKey();
|
||||
|
||||
class HostPage extends RebootPage {
|
||||
const HostPage({Key? key}) : super(key: key);
|
||||
@@ -47,7 +52,6 @@ class HostPage extends RebootPage {
|
||||
class _HostingPageState extends RebootPageState<HostPage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty);
|
||||
@@ -67,39 +71,32 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
|
||||
@override
|
||||
Widget get button => LaunchButton(
|
||||
host: true,
|
||||
startLabel: translations.startHosting,
|
||||
stopLabel: translations.stopHosting
|
||||
host: true,
|
||||
startLabel: translations.startHosting,
|
||||
stopLabel: translations.stopHosting
|
||||
);
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [
|
||||
_information,
|
||||
versionSelectSettingTile,
|
||||
buildVersionSelector(
|
||||
key: hostVersionOverlayTargetKey
|
||||
),
|
||||
_options,
|
||||
_internalFiles,
|
||||
_share,
|
||||
_resetDefaults
|
||||
];
|
||||
|
||||
SettingTile get _resetDefaults => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.hostResetName),
|
||||
subtitle: Text(translations.hostResetDescription),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_hostingController.reset),
|
||||
child: Text(translations.hostResetContent),
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _information => SettingTile(
|
||||
key: hostInfoTileKey,
|
||||
icon: Icon(
|
||||
FluentIcons.info_24_regular
|
||||
),
|
||||
title: Text(translations.hostGameServerName),
|
||||
subtitle: Text(translations.hostGameServerDescription),
|
||||
overlayKey: hostInfoOverlayTargetKey,
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
@@ -107,10 +104,14 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
),
|
||||
title: Text(translations.hostGameServerNameName),
|
||||
subtitle: Text(translations.hostGameServerNameDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.hostGameServerNameName,
|
||||
controller: _hostingController.name,
|
||||
onChanged: (_) => _updateServer()
|
||||
content: OverlayTarget(
|
||||
key: hostInfoNameOverlayTargetKey,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.hostGameServerNameName,
|
||||
controller: _hostingController.name,
|
||||
focusNode: _hostingController.nameFocusNode,
|
||||
onChanged: (_) => _updateServer()
|
||||
),
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
@@ -119,10 +120,14 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
),
|
||||
title: Text(translations.hostGameServerDescriptionName),
|
||||
subtitle: Text(translations.hostGameServerDescriptionDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.hostGameServerDescriptionName,
|
||||
controller: _hostingController.description,
|
||||
onChanged: (_) => _updateServer()
|
||||
content: OverlayTarget(
|
||||
key: hostInfoDescriptionOverlayTargetKey,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.hostGameServerDescriptionName,
|
||||
controller: _hostingController.description,
|
||||
focusNode: _hostingController.descriptionFocusNode,
|
||||
onChanged: (_) => _updateServer()
|
||||
),
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
@@ -131,28 +136,32 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
),
|
||||
title: Text(translations.hostGameServerPasswordName),
|
||||
subtitle: Text(translations.hostGameServerDescriptionDescription),
|
||||
content: Obx(() => TextFormBox(
|
||||
placeholder: translations.hostGameServerPasswordName,
|
||||
controller: _hostingController.password,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
obscureText: !_hostingController.showPassword.value,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
onChanged: (text) {
|
||||
_showPasswordTrailing.value = text.isNotEmpty;
|
||||
_updateServer();
|
||||
},
|
||||
suffix: Button(
|
||||
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
|
||||
style: ButtonStyle(
|
||||
shape: ButtonState.all(const CircleBorder()),
|
||||
backgroundColor: ButtonState.all(Colors.transparent)
|
||||
),
|
||||
child: Icon(
|
||||
_hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled,
|
||||
color: _showPasswordTrailing.value ? null : Colors.transparent
|
||||
),
|
||||
)
|
||||
content: Obx(() => OverlayTarget(
|
||||
key: hostInfoPasswordOverlayTargetKey,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.hostGameServerPasswordName,
|
||||
controller: _hostingController.password,
|
||||
focusNode: _hostingController.passwordFocusNode,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
obscureText: !_hostingController.showPassword.value,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
onChanged: (text) {
|
||||
_showPasswordTrailing.value = text.isNotEmpty;
|
||||
_updateServer();
|
||||
},
|
||||
suffix: Button(
|
||||
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
|
||||
style: ButtonStyle(
|
||||
shape: ButtonState.all(const CircleBorder()),
|
||||
backgroundColor: ButtonState.all(Colors.transparent)
|
||||
),
|
||||
child: Icon(
|
||||
_hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled,
|
||||
color: _showPasswordTrailing.value ? null : Colors.transparent
|
||||
),
|
||||
)
|
||||
),
|
||||
))
|
||||
),
|
||||
SettingTile(
|
||||
@@ -183,105 +192,6 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
]
|
||||
);
|
||||
|
||||
SettingTile get _internalFiles => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.archive_settings_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerName),
|
||||
subtitle: Text(translations.settingsServerSubtitle),
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.timer_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerTypeName),
|
||||
subtitle: Text(translations.settingsServerTypeDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_updateController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
|
||||
items: {
|
||||
false: translations.settingsServerTypeEmbeddedName,
|
||||
true: translations.settingsServerTypeCustomName
|
||||
}.entries.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.value),
|
||||
onPressed: () {
|
||||
final oldValue = _updateController.customGameServer.value;
|
||||
if(oldValue == entry.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
_updateController.customGameServer.value = entry.key;
|
||||
_updateController.infoBarEntry?.close();
|
||||
if(!entry.key) {
|
||||
_updateController.updateReboot(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
Obx(() {
|
||||
if(!_updateController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return createFileSetting(
|
||||
title: translations.settingsServerFileName,
|
||||
description: translations.settingsServerFileDescription,
|
||||
controller: _settingsController.gameServerDll
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
if(_updateController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerMirrorName),
|
||||
subtitle: Text(translations.settingsServerMirrorDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _updateController.url,
|
||||
validator: checkUpdateUrl
|
||||
)
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
if(_updateController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.timer_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerTimerName),
|
||||
subtitle: Text(translations.settingsServerTimerSubtitle),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_updateController.timer.value.text),
|
||||
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.text),
|
||||
onPressed: () {
|
||||
_updateController.timer.value = entry;
|
||||
_updateController.infoBarEntry?.close();
|
||||
_updateController.updateReboot(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _options => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.options_24_regular
|
||||
@@ -347,12 +257,121 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _internalFiles => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.archive_settings_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerName),
|
||||
subtitle: Text(translations.settingsServerSubtitle),
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.timer_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerTypeName),
|
||||
subtitle: Text(translations.settingsServerTypeDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_settingsController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
|
||||
items: {
|
||||
false: translations.settingsServerTypeEmbeddedName,
|
||||
true: translations.settingsServerTypeCustomName
|
||||
}.entries.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.value),
|
||||
onPressed: () {
|
||||
final oldValue = _settingsController.customGameServer.value;
|
||||
if(oldValue == entry.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
_settingsController.customGameServer.value = entry.key;
|
||||
_settingsController.infoBarEntry?.close();
|
||||
if(!entry.key) {
|
||||
_settingsController.updateReboot(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
Obx(() {
|
||||
if(!_settingsController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return createFileSetting(
|
||||
title: translations.settingsServerFileName,
|
||||
description: translations.settingsServerFileDescription,
|
||||
controller: _settingsController.gameServerDll
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
if(_settingsController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerMirrorName),
|
||||
subtitle: Text(translations.settingsServerMirrorDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _settingsController.url,
|
||||
validator: _checkUpdateUrl
|
||||
)
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
if(_settingsController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.timer_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerTimerName),
|
||||
subtitle: Text(translations.settingsServerTimerSubtitle),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_settingsController.timer.value.text),
|
||||
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.text),
|
||||
onPressed: () {
|
||||
_settingsController.timer.value = entry;
|
||||
_settingsController.infoBarEntry?.close();
|
||||
_settingsController.updateReboot(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
String? _checkUpdateUrl(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyURL;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
SettingTile get _share => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.link_24_regular
|
||||
),
|
||||
title: Text(translations.hostShareName),
|
||||
subtitle: Text(translations.hostShareDescription),
|
||||
overlayKey: hostShareOverlayTargetKey,
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
@@ -382,7 +401,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
final ip = await Ipify.ipv4();
|
||||
entry.close();
|
||||
FlutterClipboard.controlC(ip);
|
||||
_showCopiedIp();
|
||||
_showCopiedIp();
|
||||
}catch(error) {
|
||||
entry?.close();
|
||||
_showCannotCopyIp(error);
|
||||
@@ -394,6 +413,18 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _resetDefaults => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.hostResetName),
|
||||
subtitle: Text(translations.hostResetDescription),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_hostingController.reset),
|
||||
child: Text(translations.hostResetContent),
|
||||
)
|
||||
);
|
||||
|
||||
Future<void> _updateServer() async {
|
||||
if(!_hostingController.published()) {
|
||||
return;
|
||||
@@ -409,29 +440,29 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _showCopiedLink() => showInfoBar(
|
||||
void _showCopiedLink() => showRebootInfoBar(
|
||||
translations.hostShareLinkMessageSuccess,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
|
||||
InfoBarEntry _showCopyingIp() => showInfoBar(
|
||||
InfoBarEntry _showCopyingIp() => showRebootInfoBar(
|
||||
translations.hostShareIpMessageLoading,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
|
||||
void _showCopiedIp() => showInfoBar(
|
||||
void _showCopiedIp() => showRebootInfoBar(
|
||||
translations.hostShareIpMessageSuccess,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
|
||||
void _showCannotCopyIp(Object error) => showInfoBar(
|
||||
void _showCannotCopyIp(Object error) => showRebootInfoBar(
|
||||
translations.hostShareIpMessageError(error.toString()),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
|
||||
void _showCannotUpdateGameServer(Object error) => showInfoBar(
|
||||
void _showCannotUpdateGameServer(Object error) => showRebootInfoBar(
|
||||
translations.cannotUpdateGameServer(error.toString()),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarLongDuration
|
||||
@@ -1,80 +1,14 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_launcher/src/widget/info_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class InfoPage extends RebootPage {
|
||||
static late List<InfoTile> _infoTiles;
|
||||
static late List<_QuizEntry> _quizEntries;
|
||||
|
||||
static Object? initInfoTiles() {
|
||||
try {
|
||||
final faqDirectory = Directory("${assetsDirectory.path}\\info\\$currentLocale\\faq");
|
||||
final infoTiles = SplayTreeMap<int, InfoTile>();
|
||||
for(final entry in faqDirectory.listSync()) {
|
||||
if(entry is File) {
|
||||
final name = Uri.decodeQueryComponent(path.basename(entry.path));
|
||||
final splitter = name.indexOf(".");
|
||||
if(splitter == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final index = int.tryParse(name.substring(0, splitter));
|
||||
if(index == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final questionName = Uri.decodeQueryComponent(name.substring(splitter + 2));
|
||||
infoTiles[index] = InfoTile(
|
||||
title: Text(questionName),
|
||||
content: Text(entry.readAsStringSync())
|
||||
);
|
||||
}
|
||||
}
|
||||
_infoTiles = infoTiles.values.toList(growable: false);
|
||||
|
||||
final questionsDirectory = Directory("${assetsDirectory.path}\\info\\$currentLocale\\questions");
|
||||
final questions = SplayTreeMap<int, _QuizEntry>();
|
||||
for(final entry in questionsDirectory.listSync()) {
|
||||
if(entry is File) {
|
||||
final name = Uri.decodeQueryComponent(path.basename(entry.path));
|
||||
final splitter = name.indexOf(".");
|
||||
if(splitter == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final index = int.tryParse(name.substring(0, splitter));
|
||||
if(index == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final questionName = Uri.decodeQueryComponent(name.substring(splitter + 2));
|
||||
questions[index] = _QuizEntry(
|
||||
question: questionName,
|
||||
options: entry.readAsStringSync().split("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
_quizEntries = questions.values.toList(growable: false);
|
||||
|
||||
return null;
|
||||
}catch(error) {
|
||||
_infoTiles = [];
|
||||
_quizEntries = [];
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
const InfoPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -87,207 +21,59 @@ class InfoPage extends RebootPage {
|
||||
String get iconAsset => "assets/images/info.png";
|
||||
|
||||
@override
|
||||
bool hasButton(String? pageName) => Get.find<SettingsController>().firstRun.value && pageName != null;
|
||||
bool hasButton(String? routeName) => false;
|
||||
|
||||
@override
|
||||
RebootPageType get type => RebootPageType.info;
|
||||
}
|
||||
|
||||
class _InfoPageState extends RebootPageState<InfoPage> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
late final Rxn<Widget> _quizPage;
|
||||
static const String _kReportBugUrl = "https://github.com/Auties00/reboot_launcher/issues/new";
|
||||
static const String _kDiscordInviteUrl = "https://discord.gg/reboot";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_quizPage = Rxn(_settingsController.firstRun.value ? _QuizRoute(
|
||||
entries: InfoPage._quizEntries,
|
||||
onSuccess: () => _quizPage.value = null
|
||||
) : null);
|
||||
super.initState();
|
||||
}
|
||||
List<SettingTile> get settings => [
|
||||
_discord,
|
||||
_tutorial,
|
||||
_reportBug
|
||||
];
|
||||
|
||||
SettingTile get _reportBug => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.bug_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsBugReportName),
|
||||
subtitle: Text(translations.settingsUtilsBugReportSubtitle),
|
||||
content: Button(
|
||||
onPressed: () => launchUrlString(_kReportBugUrl),
|
||||
child: Text(translations.settingsUtilsBugReportContent),
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _tutorial => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.chat_help_24_regular
|
||||
),
|
||||
title: Text(translations.infoVideoName),
|
||||
subtitle: Text(translations.infoVideoDescription),
|
||||
content: Button(
|
||||
onPressed: () => startOnboarding(),
|
||||
child: Text(translations.infoVideoContent)
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _discord => SettingTile(
|
||||
icon: Icon(
|
||||
Icons.discord_outlined
|
||||
),
|
||||
title: Text(translations.infoDiscordName),
|
||||
subtitle: Text(translations.infoDiscordDescription),
|
||||
content: Button(
|
||||
onPressed: () => launchUrlString(_kDiscordInviteUrl),
|
||||
child: Text(translations.infoDiscordContent)
|
||||
)
|
||||
);
|
||||
|
||||
@override
|
||||
List<Widget> get settings => InfoPage._infoTiles;
|
||||
|
||||
@override
|
||||
Widget? get button {
|
||||
if(_quizPage.value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Obx(() {
|
||||
final page = _quizPage.value;
|
||||
if(page == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: Button(
|
||||
onPressed: () => Navigator.of(context).push(PageRouteBuilder(
|
||||
transitionDuration: Duration.zero,
|
||||
reverseTransitionDuration: Duration.zero,
|
||||
settings: RouteSettings(
|
||||
name: translations.quiz
|
||||
),
|
||||
pageBuilder: (context, incoming, outgoing) => page
|
||||
)),
|
||||
child: Text(
|
||||
translations.startQuiz
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _QuizRoute extends StatefulWidget {
|
||||
final List<_QuizEntry> entries;
|
||||
final void Function() onSuccess;
|
||||
const _QuizRoute({
|
||||
required this.entries,
|
||||
required this.onSuccess
|
||||
});
|
||||
|
||||
@override
|
||||
State<_QuizRoute> createState() => _QuizRouteState();
|
||||
}
|
||||
|
||||
class _QuizRouteState extends State<_QuizRoute> with AutomaticKeepAliveClientMixin {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
late final List<RxInt> _selectedIndexes = List.generate(widget.entries.length, (_) => RxInt(-1));
|
||||
int _triesLeft = 3;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: widget.entries.indexed.expand((entry) {
|
||||
final selectedIndex = _selectedIndexes[entry.$1];
|
||||
return [
|
||||
Text(
|
||||
"${entry.$1 + 1}. ${entry.$2.question}",
|
||||
style: TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.w600
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12.0),
|
||||
...entry.$2.options.indexed.map<Widget>((value) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: Obx(() => RadioButton(
|
||||
checked: value.$1 == selectedIndex.value,
|
||||
content: Text(value.$2, textAlign: TextAlign.center),
|
||||
onChanged: (_) => selectedIndex.value = value.$1
|
||||
)),
|
||||
)),
|
||||
const SizedBox(height: 12.0)
|
||||
];
|
||||
}).toList()
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: Obx(() {
|
||||
var clickable = true;
|
||||
for(final index in _selectedIndexes) {
|
||||
if(index.value == -1) {
|
||||
clickable = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Button(
|
||||
onPressed: clickable ? () async {
|
||||
if(_triesLeft <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var right = 0;
|
||||
final total = widget.entries.length;
|
||||
for(var i = 0; i < total; i++) {
|
||||
final selectedIndex = _selectedIndexes[i].value;
|
||||
final correctIndex = widget.entries[i].correctIndex;
|
||||
if(selectedIndex == correctIndex) {
|
||||
right++;
|
||||
}
|
||||
}
|
||||
|
||||
if(right == total) {
|
||||
widget.onSuccess();
|
||||
showInfoBar(
|
||||
translations.quizSuccess,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
_settingsController.firstRun.value = false;
|
||||
Navigator.of(context).pop();
|
||||
pageIndex.value = RebootPageType.play.index;
|
||||
return;
|
||||
}
|
||||
|
||||
switch(--_triesLeft) {
|
||||
case 0:
|
||||
showInfoBar(
|
||||
translations.quizFailed(
|
||||
right,
|
||||
total,
|
||||
translations.quizZeroTriesLeft
|
||||
),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
exit(0);
|
||||
case 1:
|
||||
showInfoBar(
|
||||
translations.quizFailed(
|
||||
right,
|
||||
total,
|
||||
translations.quizOneTryLeft
|
||||
),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
showInfoBar(
|
||||
translations.quizFailed(
|
||||
right,
|
||||
total,
|
||||
translations.quizTwoTriesLeft
|
||||
),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
break;
|
||||
}
|
||||
} : null,
|
||||
child: Text(translations.checkQuiz),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuizEntry {
|
||||
final String question;
|
||||
final List<String> options;
|
||||
late final int correctIndex;
|
||||
|
||||
_QuizEntry({required this.question, required this.options}) {
|
||||
final correct = options.first;
|
||||
options.shuffle();
|
||||
correctIndex = options.indexOf(correct);
|
||||
}
|
||||
Widget? get button => null;
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/game_start_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
||||
|
||||
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
|
||||
|
||||
class PlayPage extends RebootPage {
|
||||
const PlayPage({Key? key}) : super(key: key);
|
||||
@@ -36,7 +37,48 @@ class PlayPage extends RebootPage {
|
||||
class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildFirstLaunchInfo(),
|
||||
Expanded(
|
||||
child: super.build(context),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFirstLaunchInfo() => Obx(() {
|
||||
if(!_settingsController.firstRun.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 8.0
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text(translations.welcomeTitle),
|
||||
severity: InfoBarSeverity.warning,
|
||||
isLong: true,
|
||||
content: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(translations.welcomeDescription)
|
||||
),
|
||||
action: Button(
|
||||
child: Text(translations.welcomeAction),
|
||||
onPressed: () => startOnboarding(),
|
||||
),
|
||||
onClose: () => _settingsController.firstRun.value = false
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget? get button => LaunchButton(
|
||||
@@ -47,25 +89,13 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
|
||||
@override
|
||||
List<SettingTile> get settings => [
|
||||
versionSelectSettingTile,
|
||||
buildVersionSelector(
|
||||
key: gameVersionOverlayTargetKey
|
||||
),
|
||||
_options,
|
||||
_internalFiles,
|
||||
_multiplayer
|
||||
];
|
||||
|
||||
SettingTile get _multiplayer => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.people_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerName),
|
||||
subtitle: Text(translations.playGameServerDescription),
|
||||
children: [
|
||||
_hostSettingTile,
|
||||
_browseServerTile,
|
||||
_matchmakerTile,
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _internalFiles => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.archive_settings_24_regular
|
||||
@@ -95,8 +125,8 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
icon: Icon(
|
||||
FluentIcons.options_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerOptionsName),
|
||||
subtitle: Text(translations.settingsServerOptionsSubtitle),
|
||||
title: Text(translations.settingsClientOptionsName),
|
||||
subtitle: Text(translations.settingsClientOptionsDescription),
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
@@ -111,34 +141,4 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
SettingTile get _matchmakerTile => SettingTile(
|
||||
onPressed: () {
|
||||
pageIndex.value = RebootPageType.backend.index;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _backendController.gameServerAddressFocusNode.requestFocus());
|
||||
},
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerCustomName),
|
||||
subtitle: Text(translations.playGameServerCustomDescription),
|
||||
);
|
||||
|
||||
SettingTile get _browseServerTile => SettingTile(
|
||||
onPressed: () => pageIndex.value = RebootPageType.browser.index,
|
||||
icon: Icon(
|
||||
FluentIcons.search_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerBrowserName),
|
||||
subtitle: Text(translations.playGameServerBrowserDescription)
|
||||
);
|
||||
|
||||
SettingTile get _hostSettingTile => SettingTile(
|
||||
onPressed: () => pageIndex.value = RebootPageType.host.index,
|
||||
icon: Icon(
|
||||
FluentIcons.desktop_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerHostName),
|
||||
subtitle: Text(translations.playGameServerHostDescription),
|
||||
);
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
|
||||
class BrowsePage extends RebootPage {
|
||||
const BrowsePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
String get name => translations.browserName;
|
||||
|
||||
@override
|
||||
RebootPageType get type => RebootPageType.browser;
|
||||
|
||||
@override
|
||||
String get iconAsset => "assets/images/server_browser.png";
|
||||
|
||||
@override
|
||||
bool hasButton(String? pageName) => false;
|
||||
|
||||
@override
|
||||
RebootPageState<BrowsePage> createState() => _BrowsePageState();
|
||||
}
|
||||
|
||||
class _BrowsePageState extends RebootPageState<BrowsePage> {
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final TextEditingController _filterController = TextEditingController();
|
||||
final StreamController<String> _filterControllerStream = StreamController.broadcast();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Obx(() {
|
||||
var data = _hostingController.servers.value
|
||||
?.where((entry) => (kDebugMode || entry["id"] != _hostingController.uuid) && entry["discoverable"])
|
||||
.toSet();
|
||||
if(data == null || data.isEmpty == true) {
|
||||
return _noServers;
|
||||
}
|
||||
|
||||
return _buildPageBody(data);
|
||||
});
|
||||
}
|
||||
|
||||
Widget get _noServers => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translations.noServersAvailableTitle,
|
||||
style: FluentTheme.of(context).typography.titleLarge,
|
||||
),
|
||||
Text(
|
||||
translations.noServersAvailableSubtitle,
|
||||
style: FluentTheme.of(context).typography.body
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildPageBody(Set<Map<String, dynamic>> data) => StreamBuilder(
|
||||
stream: _filterControllerStream.stream,
|
||||
builder: (context, filterSnapshot) {
|
||||
final items = data.where((entry) => _isValidItem(entry, filterSnapshot.data)).toSet();
|
||||
return Column(
|
||||
children: [
|
||||
_searchBar,
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: items.isEmpty ? _noServersByQuery : _buildPopulatedListBody(items)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Widget _buildPopulatedListBody(Set<Map<String, dynamic>> items) => ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
var entry = items.elementAt(index ~/ 2);
|
||||
var hasPassword = entry["password"] != null;
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
hasPassword ? FluentIcons.lock : FluentIcons.globe
|
||||
),
|
||||
title: Text("${_formatName(entry)} • ${entry["author"]}"),
|
||||
subtitle: Text("${_formatDescription(entry)} • ${_formatVersion(entry)}"),
|
||||
content: Button(
|
||||
onPressed: () => _backendController.joinServer(_hostingController.uuid, entry),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Widget get _noServersByQuery => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translations.noServersAvailableByQueryTitle,
|
||||
style: FluentTheme.of(context).typography.titleLarge,
|
||||
),
|
||||
Text(
|
||||
translations.noServersAvailableByQuerySubtitle,
|
||||
style: FluentTheme.of(context).typography.body
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
bool _isValidItem(Map<String, dynamic> entry, String? filter) =>
|
||||
filter == null || filter.isEmpty || _filterServer(entry, filter);
|
||||
|
||||
bool _filterServer(Map<String, dynamic> element, String filter) {
|
||||
String? id = element["id"];
|
||||
if(id?.toLowerCase().contains(filter.toLowerCase()) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var uri = Uri.tryParse(filter);
|
||||
if(uri != null
|
||||
&& uri.host.isNotEmpty
|
||||
&& id?.toLowerCase().contains(uri.host.toLowerCase()) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String? name = element["name"];
|
||||
if(name?.toLowerCase().contains(filter) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String? author = element["author"];
|
||||
if(author?.toLowerCase().contains(filter) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String? description = element["description"];
|
||||
if(description?.toLowerCase().contains(filter) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Widget get _searchBar => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 350
|
||||
),
|
||||
child: TextBox(
|
||||
placeholder: translations.findServer,
|
||||
controller: _filterController,
|
||||
autofocus: true,
|
||||
onChanged: (value) => _filterControllerStream.add(value),
|
||||
suffix: _searchBarIcon,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _searchBarIcon => Button(
|
||||
onPressed: _filterController.text.isEmpty ? null : () {
|
||||
_filterController.clear();
|
||||
_filterControllerStream.add("");
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: ButtonState.all(Colors.transparent),
|
||||
shape: ButtonState.all(Border())
|
||||
),
|
||||
child: _searchBarIconData
|
||||
);
|
||||
|
||||
Widget get _searchBarIconData {
|
||||
var color = FluentTheme.of(context).resources.textFillColorPrimary;
|
||||
if (_filterController.text.isNotEmpty) {
|
||||
return Icon(
|
||||
FluentIcons.clear,
|
||||
size: 8.0,
|
||||
color: color
|
||||
);
|
||||
}
|
||||
|
||||
return Transform.flip(
|
||||
flipX: true,
|
||||
child: Icon(
|
||||
FluentIcons.search,
|
||||
size: 12.0,
|
||||
color: color
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatName(Map<String, dynamic> entry) {
|
||||
String result = entry['name'];
|
||||
return result.isEmpty ? translations.defaultServerName : result;
|
||||
}
|
||||
|
||||
String _formatDescription(Map<String, dynamic> entry) {
|
||||
String result = entry['description'];
|
||||
return result.isEmpty ? translations.defaultServerDescription : result;
|
||||
}
|
||||
|
||||
String _formatVersion(Map<String, dynamic> entry) {
|
||||
var version = entry['version'];
|
||||
var versionSplit = version.indexOf("-");
|
||||
var minimalVersion = version = versionSplit != -1 ? version.substring(0, versionSplit) : version;
|
||||
String result = minimalVersion.endsWith(".0") ? minimalVersion.substring(0, minimalVersion.length - 2) : minimalVersion;
|
||||
if(result.toLowerCase().startsWith("fortnite ")) {
|
||||
result = result.substring(0, 10);
|
||||
}
|
||||
|
||||
return "Fortnite $result";
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? get button => null;
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [];
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
@@ -40,52 +40,67 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.local_language_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsLanguageName),
|
||||
subtitle: Text(translations.settingsUtilsLanguageDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_getLocaleName(_settingsController.language.value)),
|
||||
items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem(
|
||||
text: Text(_getLocaleName(locale.languageCode)),
|
||||
onPressed: () => _settingsController.language.value = locale.languageCode
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.dark_theme_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsThemeName),
|
||||
subtitle: Text(translations.settingsUtilsThemeDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_settingsController.themeMode.value.title),
|
||||
items: ThemeMode.values.map((themeMode) => MenuFlyoutItem(
|
||||
text: Text(themeMode.title),
|
||||
onPressed: () => _settingsController.themeMode.value = themeMode
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsResetDefaultsName),
|
||||
subtitle: Text(translations.settingsUtilsResetDefaultsSubtitle),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_settingsController.reset),
|
||||
child: Text(translations.settingsUtilsResetDefaultsContent),
|
||||
)
|
||||
),
|
||||
_language,
|
||||
_theme,
|
||||
_resetDefaults,
|
||||
_installationDirectory
|
||||
];
|
||||
|
||||
SettingTile get _language => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.local_language_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsLanguageName),
|
||||
subtitle: Text(translations.settingsUtilsLanguageDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_getLocaleName(_settingsController.language.value)),
|
||||
items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem(
|
||||
text: Text(_getLocaleName(locale.languageCode)),
|
||||
onPressed: () => _settingsController.language.value = locale.languageCode
|
||||
)).toList()
|
||||
))
|
||||
);
|
||||
|
||||
String _getLocaleName(String locale) {
|
||||
var result = LocaleNames.of(context)!.nameOf(locale);
|
||||
if(result != null) {
|
||||
return "${result.substring(0, 1).toUpperCase()}${result.substring(1).toLowerCase()}";
|
||||
}
|
||||
|
||||
return locale;
|
||||
}
|
||||
|
||||
SettingTile get _theme => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.dark_theme_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsThemeName),
|
||||
subtitle: Text(translations.settingsUtilsThemeDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_settingsController.themeMode.value.title),
|
||||
items: ThemeMode.values.map((themeMode) => MenuFlyoutItem(
|
||||
text: Text(themeMode.title),
|
||||
onPressed: () => _settingsController.themeMode.value = themeMode
|
||||
)).toList()
|
||||
))
|
||||
);
|
||||
|
||||
SettingTile get _resetDefaults => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsResetDefaultsName),
|
||||
subtitle: Text(translations.settingsUtilsResetDefaultsSubtitle),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_settingsController.reset),
|
||||
child: Text(translations.settingsUtilsResetDefaultsContent),
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _installationDirectory => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.folder_24_regular
|
||||
@@ -97,15 +112,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
child: Text(translations.settingsUtilsInstallationDirectoryContent),
|
||||
)
|
||||
);
|
||||
|
||||
String _getLocaleName(String locale) {
|
||||
var result = LocaleNames.of(context)!.nameOf(locale);
|
||||
if(result != null) {
|
||||
return "${result.substring(0, 1).toUpperCase()}${result.substring(1).toLowerCase()}";
|
||||
}
|
||||
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
|
||||
extension _ThemeModeExtension on ThemeMode {
|
||||
|
||||
@@ -3,14 +3,14 @@ import 'dart:collection';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/backend_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/browser_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/host_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/info_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/play_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/server_browser_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/server_host_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/settings_page.dart';
|
||||
import 'package:reboot_launcher/src/widget/info_bar_area.dart';
|
||||
|
||||
@@ -26,15 +26,15 @@ final List<RebootPage> pages = [
|
||||
const SettingsPage()
|
||||
];
|
||||
|
||||
final RxInt pageIndex = _initialPageIndex;
|
||||
RxInt get _initialPageIndex {
|
||||
final settingsController = Get.find<SettingsController>();
|
||||
return RxInt(settingsController.firstRun.value ? RebootPageType.info.index : RebootPageType.play.index);
|
||||
}
|
||||
final List<GlobalKey<OverlayTargetState>> _flyoutPageControllers = List.generate(pages.length, (_) => GlobalKey());
|
||||
|
||||
final RxInt pageIndex = RxInt(RebootPageType.play.index);
|
||||
|
||||
final HashMap<int, GlobalKey> _pageKeys = HashMap();
|
||||
|
||||
final GlobalKey appKey = GlobalKey();
|
||||
final GlobalKey<NavigatorState> appNavigatorKey = GlobalKey();
|
||||
|
||||
final GlobalKey<OverlayState> appOverlayKey = GlobalKey();
|
||||
|
||||
final GlobalKey<InfoBarAreaState> infoBarAreaKey = GlobalKey();
|
||||
|
||||
@@ -80,3 +80,7 @@ void addSubPageToStack(String pageName) {
|
||||
_pagesStack[index]!.add(identifier);
|
||||
pagesController.add(null);
|
||||
}
|
||||
|
||||
GlobalKey<OverlayTargetState> getOverlayTargetKeyByPage(int pageIndex) => _flyoutPageControllers[pageIndex];
|
||||
|
||||
GlobalKey<OverlayTargetState> get pageOverlayTargetKey => _flyoutPageControllers[pageIndex.value];
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
String? checkVersion(String? text, List<FortniteVersion> versions) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyVersionName;
|
||||
}
|
||||
|
||||
if (versions.any((element) => element.name == text)) {
|
||||
return translations.versionAlreadyExists;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkChangeVersion(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyVersionName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkGameFolder(text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyGamePath;
|
||||
}
|
||||
|
||||
var directory = Directory(text);
|
||||
if (!directory.existsSync()) {
|
||||
return translations.directoryDoesNotExist;
|
||||
}
|
||||
|
||||
if (FortniteVersionExtension.findExecutable(directory, "FortniteClient-Win64-Shipping.exe") == null) {
|
||||
return translations.missingShippingExe;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkDownloadDestination(text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.invalidDownloadPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkDll(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.invalidDllPath;
|
||||
}
|
||||
|
||||
if (!File(text).existsSync()) {
|
||||
return translations.dllDoesNotExist;
|
||||
}
|
||||
|
||||
if (!text.endsWith(".dll")) {
|
||||
return translations.invalidDllExtension;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkMatchmaking(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyHostname;
|
||||
}
|
||||
|
||||
var ipParts = text.split(":");
|
||||
if(ipParts.length > 2){
|
||||
return translations.hostnameFormat;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkUpdateUrl(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyURL;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
final Map<String, Future<void>> _operations = {};
|
||||
|
||||
Future<void> downloadCriticalDllInteractive(String filePath, {bool silent = false}) {
|
||||
final old = _operations[filePath];
|
||||
if(old != null) {
|
||||
return old;
|
||||
}
|
||||
|
||||
final newRun = _downloadCriticalDllInteractive(filePath, silent);
|
||||
_operations[filePath] = newRun;
|
||||
return newRun;
|
||||
}
|
||||
|
||||
Future<void> _downloadCriticalDllInteractive(String filePath, bool silent) async {
|
||||
final fileName = path.basename(filePath).toLowerCase();
|
||||
InfoBarEntry? entry;
|
||||
try {
|
||||
if (fileName == "reboot.dll") {
|
||||
await _updateController.updateReboot(
|
||||
silent: silent
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if(File(filePath).existsSync()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final fileNameWithoutExtension = path.basenameWithoutExtension(filePath);
|
||||
if(!silent) {
|
||||
entry = showInfoBar(
|
||||
translations.downloadingDll(fileNameWithoutExtension),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
await downloadCriticalDll(fileName, filePath);
|
||||
entry?.close();
|
||||
if(!silent) {
|
||||
entry = await showInfoBar(
|
||||
translations.downloadDllSuccess(fileNameWithoutExtension),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}
|
||||
}catch(message) {
|
||||
if(!silent) {
|
||||
entry?.close();
|
||||
var error = message.toString();
|
||||
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
final completer = Completer();
|
||||
await showInfoBar(
|
||||
translations.downloadDllError(fileName, error.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
onDismissed: () => completer.complete(null),
|
||||
action: Button(
|
||||
onPressed: () async {
|
||||
await downloadCriticalDllInteractive(filePath);
|
||||
completer.complete(null);
|
||||
},
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
await completer.future;
|
||||
}
|
||||
}finally {
|
||||
_operations.remove(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
extension InjectableDllExtension on InjectableDll {
|
||||
String get path {
|
||||
final SettingsController settingsController = Get.find<SettingsController>();
|
||||
switch(this){
|
||||
case InjectableDll.reboot:
|
||||
if(_updateController.customGameServer.value) {
|
||||
final file = File(settingsController.gameServerDll.text);
|
||||
if(file.existsSync()) {
|
||||
return file.path;
|
||||
}
|
||||
}
|
||||
|
||||
return rebootDllFile.path;
|
||||
case InjectableDll.console:
|
||||
return settingsController.unrealEngineConsoleDll.text;
|
||||
case InjectableDll.cobalt:
|
||||
return settingsController.backendDll.text;
|
||||
case InjectableDll.memory:
|
||||
return settingsController.memoryLeakDll.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,45 +3,52 @@ import 'dart:io';
|
||||
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
const Duration _timeout = Duration(seconds: 2);
|
||||
|
||||
Future<bool> _pingGameServer(String hostname, int port) async {
|
||||
RawDatagramSocket? socket;
|
||||
try {
|
||||
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
||||
final dataToSend = utf8.encode(DateTime.now().toIso8601String());
|
||||
socket.send(dataToSend, InternetAddress(hostname), port);
|
||||
await for (var event in socket) {
|
||||
switch(event) {
|
||||
case RawSocketEvent.read:
|
||||
case RawSocketEvent.write:
|
||||
return true;
|
||||
case RawSocketEvent.readClosed:
|
||||
case RawSocketEvent.closed:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}finally {
|
||||
socket?.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> get _timeoutFuture => Future.delayed(_timeout).then((value) => false);
|
||||
const Duration _timeout = Duration(seconds: 5);
|
||||
|
||||
Future<bool> pingGameServer(String address, {Duration? timeout}) async {
|
||||
var start = DateTime.now();
|
||||
var firstTime = true;
|
||||
while (firstTime || (timeout != null && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds)) {
|
||||
var split = address.split(":");
|
||||
var hostname = split[0];
|
||||
if(isLocalHost(hostname)) {
|
||||
hostname = "127.0.0.1";
|
||||
}
|
||||
Future<bool> ping(String hostname, int port) async {
|
||||
log("[MATCHMAKER] Pinging $hostname:$port");
|
||||
RawDatagramSocket? socket;
|
||||
try {
|
||||
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
||||
await for (final event in socket) {
|
||||
log("[MATCHMAKER] Event: $event");
|
||||
switch(event) {
|
||||
case RawSocketEvent.read:
|
||||
log("[MATCHMAKER] Success");
|
||||
return true;
|
||||
case RawSocketEvent.write:
|
||||
log("[MATCHMAKER] Sending data");
|
||||
final dataToSend = base64Decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA==");
|
||||
socket.send(dataToSend, InternetAddress(hostname), port);
|
||||
case RawSocketEvent.readClosed:
|
||||
case RawSocketEvent.closed:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort);
|
||||
var result = await Future.any([_timeoutFuture, _pingGameServer(hostname, port)]);
|
||||
return false;
|
||||
}catch(error) {
|
||||
log("[MATCHMAKER] Error: $error");
|
||||
return false;
|
||||
}finally {
|
||||
socket?.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final start = DateTime.now();
|
||||
var firstTime = true;
|
||||
final split = address.split(":");
|
||||
var hostname = split[0];
|
||||
if(isLocalHost(hostname)) {
|
||||
hostname = "127.0.0.1";
|
||||
}
|
||||
|
||||
final port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort);
|
||||
while (firstTime || (timeout != null && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds)) {
|
||||
final result = await ping(hostname, port)
|
||||
.timeout(_timeout, onTimeout: () => false);
|
||||
if(result) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
||||
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_name_input.dart';
|
||||
|
||||
class AddLocalVersion extends StatefulWidget {
|
||||
const AddLocalVersion({Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<AddLocalVersion> createState() => _AddLocalVersionState();
|
||||
}
|
||||
|
||||
class _AddLocalVersionState extends State<AddLocalVersion> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _gamePathController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_gamePathController.addListener(() async {
|
||||
var file = Directory(_gamePathController.text);
|
||||
if(await file.exists()) {
|
||||
if(_nameController.text.isEmpty) {
|
||||
var text = path.basename(_gamePathController.text);
|
||||
_nameController.text = text;
|
||||
_nameController.selection = TextSelection.collapsed(offset: text.length);
|
||||
}
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text(translations.localBuildsWarning),
|
||||
severity: InfoBarSeverity.info
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
VersionNameInput(
|
||||
controller: _nameController
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
FileSelector(
|
||||
label: translations.gameFolderTitle,
|
||||
placeholder: translations.gameFolderPlaceholder,
|
||||
windowTitle: translations.gameFolderPlaceWindowTitle,
|
||||
controller: _gamePathController,
|
||||
validator: checkGameFolder,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
)
|
||||
],
|
||||
),
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
text: translations.saveLocalVersion,
|
||||
type: ButtonType.primary,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion(
|
||||
name: _nameController.text,
|
||||
location: Directory(_gamePathController.text)
|
||||
)));
|
||||
},
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/build_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_name_input.dart';
|
||||
import 'package:universal_disk_space/universal_disk_space.dart';
|
||||
import 'package:windows_taskbar/windows_taskbar.dart';
|
||||
|
||||
class AddServerVersion extends StatefulWidget {
|
||||
const AddServerVersion({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AddServerVersion> createState() => _AddServerVersionState();
|
||||
}
|
||||
|
||||
class _AddServerVersionState extends State<AddServerVersion> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final BuildController _buildController = Get.find<BuildController>();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _pathController = TextEditingController();
|
||||
final Rx<DownloadStatus> _status = Rx(DownloadStatus.form);
|
||||
final GlobalKey<FormState> _formKey = GlobalKey();
|
||||
final RxnInt _timeLeft = RxnInt();
|
||||
final Rxn<double> _progress = Rxn();
|
||||
|
||||
late DiskSpace _diskSpace;
|
||||
late Future _fetchFuture;
|
||||
late Future _diskFuture;
|
||||
|
||||
Isolate? _isolate;
|
||||
SendPort? _downloadPort;
|
||||
Object? _error;
|
||||
StackTrace? _stackTrace;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_fetchFuture = _buildController.builds != null
|
||||
? Future.value(true)
|
||||
: compute(fetchBuilds, null)
|
||||
.then((value) => _buildController.builds = value);
|
||||
_diskSpace = DiskSpace();
|
||||
_diskFuture = _diskSpace.scan()
|
||||
.then((_) => _updateFormDefaults());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pathController.dispose();
|
||||
_nameController.dispose();
|
||||
_cancelDownload();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _cancelDownload() {
|
||||
Process.run('${assetsDirectory.path}\\build\\stop.bat', []);
|
||||
_downloadPort?.send(kStopBuildDownloadSignal);
|
||||
_isolate?.kill(priority: Isolate.immediate);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Form(
|
||||
key: _formKey,
|
||||
child: Obx(() {
|
||||
switch(_status.value){
|
||||
case DownloadStatus.form:
|
||||
return FutureBuilder(
|
||||
future: Future.wait([_fetchFuture, _diskFuture]),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _onDownloadError(snapshot.error, snapshot.stackTrace));
|
||||
}
|
||||
|
||||
if (!snapshot.hasData) {
|
||||
return ProgressDialog(
|
||||
text: translations.fetchingBuilds,
|
||||
onStop: () => Navigator.of(context).pop()
|
||||
);
|
||||
}
|
||||
|
||||
return FormDialog(
|
||||
content: _formBody,
|
||||
buttons: _formButtons
|
||||
);
|
||||
}
|
||||
);
|
||||
case DownloadStatus.downloading:
|
||||
case DownloadStatus.extracting:
|
||||
return GenericDialog(
|
||||
header: _progressBody,
|
||||
buttons: _stopButton
|
||||
);
|
||||
case DownloadStatus.error:
|
||||
return ErrorDialog(
|
||||
exception: _error ?? Exception(translations.unknownError),
|
||||
stackTrace: _stackTrace,
|
||||
errorMessageBuilder: (exception) => translations.downloadVersionError(exception.toString())
|
||||
);
|
||||
case DownloadStatus.done:
|
||||
return InfoDialog(
|
||||
text: translations.downloadedVersion
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
List<DialogButton> get _formButtons => [
|
||||
DialogButton(type: ButtonType.secondary),
|
||||
DialogButton(
|
||||
text: translations.download,
|
||||
type: ButtonType.primary,
|
||||
onTap: () => _startDownload(context),
|
||||
)
|
||||
];
|
||||
|
||||
void _startDownload(BuildContext context) async {
|
||||
try {
|
||||
final build = _buildController.selectedBuild;
|
||||
if(build == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = DownloadStatus.downloading;
|
||||
final communicationPort = ReceivePort();
|
||||
communicationPort.listen((message) {
|
||||
if(message is FortniteBuildDownloadProgress) {
|
||||
_onProgress(message.progress, message.minutesLeft, message.extracting);
|
||||
}else if(message is SendPort) {
|
||||
_downloadPort = message;
|
||||
}else {
|
||||
_onDownloadError(message, null);
|
||||
}
|
||||
});
|
||||
final options = FortniteBuildDownloadOptions(
|
||||
build,
|
||||
Directory(_pathController.text),
|
||||
communicationPort.sendPort
|
||||
);
|
||||
final errorPort = ReceivePort();
|
||||
errorPort.listen((message) => _onDownloadError(message, null));
|
||||
_isolate = await Isolate.spawn(
|
||||
downloadArchiveBuild,
|
||||
options,
|
||||
onError: errorPort.sendPort,
|
||||
errorsAreFatal: true
|
||||
);
|
||||
} catch (exception, stackTrace) {
|
||||
_onDownloadError(exception, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDownloadComplete() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = DownloadStatus.done;
|
||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion(
|
||||
name: _nameController.text,
|
||||
location: Directory(_pathController.text)
|
||||
)));
|
||||
}
|
||||
|
||||
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
||||
_cancelDownload();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = DownloadStatus.error;
|
||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||
_error = error;
|
||||
_stackTrace = stackTrace;
|
||||
}
|
||||
|
||||
void _onProgress(double progress, int? timeLeft, bool extracting) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(progress >= 100 && extracting) {
|
||||
_onDownloadComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = extracting ? DownloadStatus.extracting : DownloadStatus.downloading;
|
||||
if(progress >= 0) {
|
||||
WindowsTaskbar.setProgress(progress.round(), 100);
|
||||
}
|
||||
|
||||
_timeLeft.value = timeLeft;
|
||||
_progress.value = progress;
|
||||
}
|
||||
|
||||
Widget get _progressBody {
|
||||
final timeLeft = _timeLeft.value;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
_status.value == DownloadStatus.downloading ? translations.downloading : translations.extracting,
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
translations.buildProgress((_progress.value ?? 0).round()),
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
),
|
||||
|
||||
if(timeLeft != null)
|
||||
Text(
|
||||
translations.timeLeft(timeLeft),
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar(value: (_progress.value ?? 0).toDouble())
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _formBody => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSelector(),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
VersionNameInput(
|
||||
controller: _nameController
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
FileSelector(
|
||||
label: translations.buildInstallationDirectory,
|
||||
placeholder: translations.buildInstallationDirectoryPlaceholder,
|
||||
windowTitle: translations.buildInstallationDirectoryWindowTitle,
|
||||
controller: _pathController,
|
||||
validator: checkDownloadDestination,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildSelector() => InfoLabel(
|
||||
label: translations.build,
|
||||
child: Obx(() => ComboBox<FortniteBuild>(
|
||||
placeholder: Text(translations.selectBuild),
|
||||
isExpanded: true,
|
||||
items: _builds,
|
||||
value: _buildController.selectedBuild,
|
||||
onChanged: (value) {
|
||||
if(value == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_buildController.selectedBuild = value;
|
||||
_updateFormDefaults();
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
List<ComboBoxItem<FortniteBuild>> get _builds => _buildController.builds!
|
||||
.map((element) => _buildItem(element))
|
||||
.toList();
|
||||
|
||||
ComboBoxItem<FortniteBuild> _buildItem(FortniteBuild element) => ComboBoxItem<FortniteBuild>(
|
||||
value: element,
|
||||
child: Text(element.version.toString())
|
||||
);
|
||||
|
||||
List<DialogButton> get _stopButton => [
|
||||
DialogButton(
|
||||
text: "Stop",
|
||||
type: ButtonType.only
|
||||
)
|
||||
];
|
||||
|
||||
Future<void> _updateFormDefaults() async {
|
||||
if(_diskSpace.disks.isEmpty){
|
||||
return;
|
||||
}
|
||||
|
||||
await _fetchFuture;
|
||||
final bestDisk = _diskSpace.disks
|
||||
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
|
||||
final build = _buildController.selectedBuild;
|
||||
if(build == null){
|
||||
return;
|
||||
}
|
||||
|
||||
final pathText = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}";
|
||||
_pathController.text = pathText;
|
||||
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
|
||||
final buildName = build.version.toString();
|
||||
_nameController.text = buildName;
|
||||
_nameController.selection = TextSelection.collapsed(offset: buildName.length);
|
||||
_formKey.currentState?.validate();
|
||||
}
|
||||
}
|
||||
|
||||
enum DownloadStatus { form, downloading, extracting, error, done }
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
@@ -15,8 +16,26 @@ SettingTile createFileSetting({required String title, required String descriptio
|
||||
placeholder: translations.selectPathPlaceholder,
|
||||
windowTitle: translations.selectPathWindowTitle,
|
||||
controller: controller,
|
||||
validator: checkDll,
|
||||
validator: _checkDll,
|
||||
extension: "dll",
|
||||
folder: false
|
||||
folder: false,
|
||||
validatorMode: AutovalidateMode.always
|
||||
)
|
||||
);
|
||||
|
||||
String? _checkDll(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.invalidDllPath;
|
||||
}
|
||||
|
||||
final file = File(text);
|
||||
if (!file.existsSync()) {
|
||||
return translations.dllDoesNotExist;
|
||||
}
|
||||
|
||||
if (!text.endsWith(".dll")) {
|
||||
return translations.invalidDllExtension;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import 'dart:io';
|
||||
import 'package:async/async.dart';
|
||||
import 'package:dart_ipify/dart_ipify.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:local_notifier/local_notifier.dart';
|
||||
import 'package:path/path.dart';
|
||||
@@ -13,17 +12,16 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/dll.dart';
|
||||
import 'package:reboot_launcher/src/util/log.dart';
|
||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class LaunchButton extends StatefulWidget {
|
||||
final bool host;
|
||||
@@ -43,6 +41,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
InfoBarEntry? _gameClientInfoBar;
|
||||
InfoBarEntry? _gameServerInfoBar;
|
||||
CancelableOperation? _operation;
|
||||
@@ -95,6 +94,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
|
||||
for (final injectable in InjectableDll.values) {
|
||||
if(await _getDllFileOrStop(injectable, host) == null) {
|
||||
_onStop(
|
||||
reason: _StopReason.missingCustomDllError,
|
||||
error: injectable.name,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -124,12 +127,16 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($serverType)");
|
||||
final linkedHostingInstance = await _startMatchMakingServer(version, host, serverType, false);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
|
||||
await _startGameProcesses(version, host, serverType, linkedHostingInstance);
|
||||
if(!host) {
|
||||
_showLaunchingGameClientWidget();
|
||||
final result = await _startGameProcesses(version, host, serverType, linkedHostingInstance);
|
||||
final started = host ? _hostingController.started() : _gameController.started();
|
||||
if(!started) {
|
||||
result?.kill();
|
||||
return;
|
||||
}
|
||||
|
||||
if(linkedHostingInstance != null || host){
|
||||
if(!host) {
|
||||
_showLaunchingGameClientWidget(version, serverType, linkedHostingInstance != null);
|
||||
}else {
|
||||
_showLaunchingGameServerWidget();
|
||||
}
|
||||
} catch (exception, stackTrace) {
|
||||
@@ -148,7 +155,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(_backendController.type.value == ServerType.embedded && !isLocalHost(_backendController.gameServerAddress.text)) {
|
||||
if(!forceLinkedHosting && _backendController.type.value == ServerType.embedded && !isLocalHost(_backendController.gameServerAddress.text)) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server");
|
||||
return null;
|
||||
}
|
||||
@@ -158,7 +165,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return null;
|
||||
}
|
||||
|
||||
final response = forceLinkedHosting || await _askForAutomaticGameServer();
|
||||
final response = forceLinkedHosting || await _askForAutomaticGameServer(host);
|
||||
if(!response) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] The user disabled the automatic server");
|
||||
return null;
|
||||
@@ -172,19 +179,24 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return instance;
|
||||
}
|
||||
|
||||
Future<bool> _askForAutomaticGameServer() async {
|
||||
final result = await showAppDialog<bool>(
|
||||
Future<bool> _askForAutomaticGameServer(bool host) async {
|
||||
if (host ? !_hostingController.started() : !_gameController.started()) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
|
||||
_onStop(reason: _StopReason.normal);
|
||||
return false;
|
||||
}
|
||||
|
||||
final result = await showRebootDialog<bool>(
|
||||
builder: (context) => InfoDialog(
|
||||
text: "The launcher detected that you are not running a game server, but that your matchmaker is set to your local machine. "
|
||||
"If you don't want to join another player's server, you should start a game server. This is necessary to be able to play: for more information check the Info tab in the launcher.",
|
||||
text: translations.automaticGameServerDialogContent,
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary,
|
||||
text: "Ignore"
|
||||
text: translations.automaticGameServerDialogIgnore
|
||||
),
|
||||
DialogButton(
|
||||
type: ButtonType.primary,
|
||||
text: "Start server",
|
||||
text: translations.automaticGameServerDialogStart,
|
||||
onTap: () => Navigator.of(context).pop(true),
|
||||
),
|
||||
],
|
||||
@@ -214,7 +226,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
|
||||
final instance = GameInstance(
|
||||
versionName: version.name,
|
||||
versionName: version.content.toString(),
|
||||
gamePid: gameProcess,
|
||||
launcherPid: launcherProcess,
|
||||
eacPid: eacProcess,
|
||||
@@ -247,7 +259,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
executable: executable,
|
||||
args: gameArgs,
|
||||
useTempBatch: false,
|
||||
name: "${version.name}-${host ? 'HOST' : 'GAME'}"
|
||||
name: "${version.content}-${host ? 'HOST' : 'GAME'}",
|
||||
environment: {
|
||||
"OPENSSL_ia32cap": "~0x20000000"
|
||||
}
|
||||
);
|
||||
void onGameOutput(String line, bool error) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line");
|
||||
@@ -267,12 +282,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
gameProcess.stdError.listen((line) => onGameOutput(line, true));
|
||||
gameProcess.exitCode.then((_) async {
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
if(instance == null) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called exit code, but the game process is no longer running");
|
||||
return;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called exit code(launched: ${instance.launched}): stop signal");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called exit code(launched: ${instance?.launched}): stop signal");
|
||||
_onStop(
|
||||
reason: _StopReason.exitCode,
|
||||
host: host
|
||||
@@ -289,7 +299,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
final process = await startProcess(
|
||||
executable: file,
|
||||
useTempBatch: false,
|
||||
name: "${version.name}-${basenameWithoutExtension(file.path)}"
|
||||
name: "${version.content}-${basenameWithoutExtension(file.path)}",
|
||||
environment: {
|
||||
"OPENSSL_ia32cap": "~0x20000000"
|
||||
}
|
||||
);
|
||||
final pid = process.pid;
|
||||
suspend(pid);
|
||||
@@ -304,7 +317,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
try {
|
||||
final windowManager = VirtualDesktopManager.getInstance();
|
||||
_virtualDesktop = windowManager.createDesktop();
|
||||
windowManager.setDesktopName(_virtualDesktop!, "${version.name} Server (Reboot Launcher)");
|
||||
windowManager.setDesktopName(_virtualDesktop!, "${version.content} Server (Reboot Launcher)");
|
||||
var success = false;
|
||||
try {
|
||||
success = await windowManager.moveWindowToDesktop(
|
||||
@@ -391,7 +404,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
void _onGameClientInjected() {
|
||||
_gameClientInfoBar?.close();
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.gameClientStarted,
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarLongDuration
|
||||
@@ -399,10 +412,15 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
Future<void> _onGameServerInjected() async {
|
||||
_gameServerInfoBar?.close();
|
||||
final theme = FluentTheme.of(appKey.currentContext!);
|
||||
if(_gameServerInfoBar != null) {
|
||||
_gameServerInfoBar?.close();
|
||||
}else {
|
||||
_gameClientInfoBar?.close();
|
||||
}
|
||||
|
||||
final theme = FluentTheme.of(appNavigatorKey.currentContext!);
|
||||
try {
|
||||
_gameServerInfoBar = showInfoBar(
|
||||
_gameServerInfoBar = showRebootInfoBar(
|
||||
translations.waitingForGameServer,
|
||||
loading: true,
|
||||
duration: null
|
||||
@@ -414,17 +432,17 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
);
|
||||
_gameServerInfoBar?.close();
|
||||
if (!localPingResult) {
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.gameServerStartWarning,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
return;
|
||||
}
|
||||
_backendController.joinLocalHost();
|
||||
_backendController.joinLocalhost();
|
||||
final accessible = await _checkGameServer(theme, gameServerPort);
|
||||
if (!accessible) {
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.gameServerStartLocalWarning,
|
||||
severity: InfoBarSeverity.warning,
|
||||
duration: infoBarLongDuration
|
||||
@@ -436,7 +454,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
_gameController.username.text,
|
||||
_hostingController.instance.value!.versionName,
|
||||
);
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.gameServerStarted,
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarLongDuration
|
||||
@@ -448,7 +466,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
Future<bool> _checkGameServer(FluentThemeData theme, String gameServerPort) async {
|
||||
try {
|
||||
_gameServerInfoBar = showInfoBar(
|
||||
_gameServerInfoBar = showRebootInfoBar(
|
||||
translations.checkingGameServer,
|
||||
loading: true,
|
||||
duration: null
|
||||
@@ -464,12 +482,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
"$publicIp:$gameServerPort",
|
||||
timeout: const Duration(days: 365)
|
||||
);
|
||||
_gameServerInfoBar = showInfoBar(
|
||||
_gameServerInfoBar = showRebootInfoBar(
|
||||
translations.checkGameServerFixMessage(gameServerPort),
|
||||
action: Button(
|
||||
onPressed: () async {
|
||||
pageIndex.value = RebootPageType.info.index;
|
||||
},
|
||||
onPressed: () => launchUrlString("https://github.com/Auties00/reboot_launcher/blob/master/documentation/$currentLocale/PortForwarding.md"),
|
||||
child: Text(translations.checkGameServerFixAction),
|
||||
),
|
||||
severity: InfoBarSeverity.warning,
|
||||
@@ -486,7 +502,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
if(host == null) {
|
||||
await _operation?.cancel();
|
||||
_operation = null;
|
||||
await _backendController.worker?.cancel();
|
||||
}
|
||||
|
||||
host = host ?? widget.host;
|
||||
@@ -542,14 +557,14 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
case _StopReason.normal:
|
||||
break;
|
||||
case _StopReason.missingVersionError:
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.missingVersionError,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
break;
|
||||
case _StopReason.missingExecutableError:
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.missingExecutableError,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
@@ -557,7 +572,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
break;
|
||||
case _StopReason.exitCode:
|
||||
if(instance != null && !instance.launched) {
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.corruptedVersionError,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
@@ -565,28 +580,39 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
break;
|
||||
case _StopReason.corruptedVersionError:
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.corruptedVersionError,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
break;
|
||||
case _StopReason.corruptedDllError:
|
||||
showInfoBar(
|
||||
translations.corruptedDllError(error!),
|
||||
showRebootInfoBar(
|
||||
translations.corruptedDllError(error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
break;
|
||||
case _StopReason.missingCustomDllError:
|
||||
showRebootInfoBar(
|
||||
translations.missingCustomDllError(error!),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
break;
|
||||
case _StopReason.tokenError:
|
||||
showInfoBar(
|
||||
translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? "none"),
|
||||
showRebootInfoBar(
|
||||
translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? translations.none),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
action: Button(
|
||||
onPressed: () => launchUrl(launcherLogFile.uri),
|
||||
child: Text(translations.openLog),
|
||||
)
|
||||
);
|
||||
break;
|
||||
case _StopReason.unknownError:
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.unknownFortniteError(error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
@@ -610,7 +636,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
if(dllPath == null) {
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist");
|
||||
_onStop(
|
||||
reason: _StopReason.corruptedDllError,
|
||||
reason: _StopReason.missingCustomDllError,
|
||||
error: injectable.name,
|
||||
host: hosting
|
||||
);
|
||||
return;
|
||||
@@ -631,33 +658,62 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> _getDllFileOrStop(InjectableDll injectable, bool host) async {
|
||||
Future<File?> _getDllFileOrStop(InjectableDll injectable, bool host, [bool isRetry = false]) async {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
|
||||
final path = injectable.path;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path: $path");
|
||||
final file = File(path);
|
||||
final (file, customDll) = _settingsController.getInjectableData(injectable);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
|
||||
if(await file.exists()) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path exists");
|
||||
return file;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path doesn't exist");
|
||||
if(customDll || isRetry) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Custom dll -> no recovery");
|
||||
return null;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
|
||||
await downloadCriticalDllInteractive(path);
|
||||
await _settingsController.downloadCriticalDllInteractive(file.path);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check...");
|
||||
return _getDllFileOrStop(injectable, host);
|
||||
return _getDllFileOrStop(injectable, host, true);
|
||||
}
|
||||
|
||||
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showInfoBar(
|
||||
translations.launchingHeadlessServer,
|
||||
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar(
|
||||
translations.launchingGameServer,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
|
||||
InfoBarEntry _showLaunchingGameClientWidget() => _gameClientInfoBar = showInfoBar(
|
||||
translations.launchingGameClient,
|
||||
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, GameServerType hostType, bool linkedHosting) {
|
||||
return _gameClientInfoBar = showRebootInfoBar(
|
||||
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
|
||||
loading: true,
|
||||
duration: null
|
||||
duration: null,
|
||||
action: Obx(() {
|
||||
if(_hostingController.started.value || linkedHosting) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 2.0
|
||||
),
|
||||
child: Button(
|
||||
onPressed: () async {
|
||||
_backendController.joinLocalhost();
|
||||
if(!_hostingController.started.value) {
|
||||
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, hostType, true);
|
||||
_gameClientInfoBar?.close();
|
||||
_showLaunchingGameClientWidget(version, hostType, true);
|
||||
}
|
||||
},
|
||||
child: Text(translations.startGameServer),
|
||||
),
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum _StopReason {
|
||||
@@ -665,6 +721,7 @@ enum _StopReason {
|
||||
missingVersionError,
|
||||
missingExecutableError,
|
||||
corruptedVersionError,
|
||||
missingCustomDllError,
|
||||
corruptedDllError,
|
||||
backendError,
|
||||
matchmakerError,
|
||||
|
||||
@@ -2,11 +2,12 @@ import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/profile.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/profile.dart';
|
||||
|
||||
class ProfileWidget extends StatefulWidget {
|
||||
const ProfileWidget({Key? key}) : super(key: key);
|
||||
final GlobalKey<OverlayTargetState> overlayKey;
|
||||
const ProfileWidget({required this.overlayKey});
|
||||
|
||||
@override
|
||||
State<ProfileWidget> createState() => _ProfileWidgetState();
|
||||
@@ -14,14 +15,13 @@ class ProfileWidget extends StatefulWidget {
|
||||
|
||||
class _ProfileWidgetState extends State<ProfileWidget> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() {
|
||||
final firstRun = _settingsController.firstRun.value;
|
||||
return HoverButton(
|
||||
Widget build(BuildContext context) => OverlayTarget(
|
||||
key: widget.overlayKey,
|
||||
child: HoverButton(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
onPressed: firstRun ? null : () async {
|
||||
onPressed: () async {
|
||||
if(await showProfileForm(context)) {
|
||||
setState(() {});
|
||||
}
|
||||
@@ -78,8 +78,8 @@ class _ProfileWidgetState extends State<ProfileWidget> {
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
),
|
||||
);
|
||||
|
||||
String get _username {
|
||||
var username = _gameController.username.text;
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
class ServerButton extends StatefulWidget {
|
||||
|
||||
@@ -2,12 +2,13 @@ import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
class ServerTypeSelector extends StatefulWidget {
|
||||
const ServerTypeSelector({Key? key})
|
||||
: super(key: key);
|
||||
final Key overlayKey;
|
||||
const ServerTypeSelector({required this.overlayKey});
|
||||
|
||||
@override
|
||||
State<ServerTypeSelector> createState() => _ServerTypeSelectorState();
|
||||
@@ -18,12 +19,16 @@ class _ServerTypeSelectorState extends State<ServerTypeSelector> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
leading: Text(_controller.type.value.label),
|
||||
items: ServerType.values
|
||||
.map((type) => _createItem(type))
|
||||
.toList()
|
||||
return Obx(() => OverlayTarget(
|
||||
key: widget.overlayKey,
|
||||
child: DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_controller.type.value.label),
|
||||
items: ServerType.values
|
||||
.map((type) => _createItem(type))
|
||||
.toList()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:skeletons/skeletons.dart';
|
||||
|
||||
class SettingTile extends StatelessWidget {
|
||||
class SettingTile extends StatefulWidget {
|
||||
static const double kDefaultHeight = 80.0;
|
||||
static const double kDefaultContentWidth = 200.0;
|
||||
static const double kDefaultHeaderHeight = 72;
|
||||
|
||||
final void Function()? onPressed;
|
||||
final Icon icon;
|
||||
@@ -13,28 +14,38 @@ class SettingTile extends StatelessWidget {
|
||||
final Text? subtitle;
|
||||
final Widget? content;
|
||||
final double? contentWidth;
|
||||
final Key? overlayKey;
|
||||
final List<Widget>? children;
|
||||
|
||||
const SettingTile({
|
||||
super.key,
|
||||
this.onPressed,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
this.content,
|
||||
this.contentWidth = kDefaultContentWidth,
|
||||
this.overlayKey,
|
||||
this.children
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 4.0
|
||||
),
|
||||
child: HoverButton(
|
||||
onPressed: _buildOnPressed(context),
|
||||
builder: (context, states) => Container(
|
||||
height: 80,
|
||||
State<SettingTile> createState() => SettingTileState();
|
||||
}
|
||||
|
||||
class SettingTileState extends State<SettingTile> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 4.0
|
||||
),
|
||||
child: HoverButton(
|
||||
onPressed: _buildOnPressed(),
|
||||
builder: (context, states) => ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: SettingTile.kDefaultHeight
|
||||
),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: ButtonThemeData.uncheckedInputColor(
|
||||
@@ -42,76 +53,92 @@ class SettingTile extends StatelessWidget {
|
||||
states,
|
||||
transparentWhenNone: true,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(4.0))
|
||||
borderRadius: BorderRadius.all(Radius.circular(6.0))
|
||||
),
|
||||
child: Card(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(4.0)
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 6.0
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: 16.0),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
title == null ? _skeletonTitle : title!,
|
||||
subtitle == null ? _skeletonSubtitle : subtitle!,
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
_trailing
|
||||
],
|
||||
),
|
||||
)
|
||||
)
|
||||
child: _buildBody()
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Card _buildBody() {
|
||||
return Card(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(6.0)
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 12.0
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if(widget.overlayKey != null)
|
||||
OverlayTarget(
|
||||
key: widget.overlayKey,
|
||||
child: widget.icon,
|
||||
)
|
||||
else
|
||||
widget.icon,
|
||||
const SizedBox(width: 16.0),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
widget.title == null ? _skeletonTitle : widget.title!,
|
||||
widget.subtitle == null ? _skeletonSubtitle : widget.subtitle!,
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
_trailing
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void Function()? _buildOnPressed(BuildContext context) {
|
||||
if(onPressed != null) {
|
||||
return onPressed;
|
||||
void Function()? _buildOnPressed() {
|
||||
if(widget.onPressed != null) {
|
||||
return widget.onPressed;
|
||||
}
|
||||
|
||||
final children = this.children;
|
||||
final children = this.widget.children;
|
||||
if (children == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return () async {
|
||||
await Navigator.of(context).push(PageRouteBuilder(
|
||||
transitionDuration: Duration.zero,
|
||||
reverseTransitionDuration: Duration.zero,
|
||||
settings: RouteSettings(
|
||||
name: title?.data
|
||||
),
|
||||
pageBuilder: (context, incoming, outgoing) => ListView.builder(
|
||||
itemCount: children.length,
|
||||
itemBuilder: (context, index) => children[index]
|
||||
)
|
||||
));
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => pageIndex.value = pageIndex.value);
|
||||
};
|
||||
return () => openNestedPage();
|
||||
}
|
||||
|
||||
Future<void> openNestedPage() async {
|
||||
final children = this.widget.children;
|
||||
if (children == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Navigator.of(context).push(PageRouteBuilder(
|
||||
transitionDuration: Duration.zero,
|
||||
reverseTransitionDuration: Duration.zero,
|
||||
settings: RouteSettings(
|
||||
name: widget.title?.data
|
||||
),
|
||||
pageBuilder: (context, incoming, outgoing) => ListView.builder(
|
||||
itemCount: children.length,
|
||||
itemBuilder: (context, index) => children[index]
|
||||
)
|
||||
));
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => pageIndex.value = pageIndex.value);
|
||||
}
|
||||
|
||||
Widget get _trailing {
|
||||
final hasContent = content != null;
|
||||
final hasChildren = children?.isNotEmpty == true;
|
||||
final hasListener = onPressed != null;
|
||||
final hasContent = widget.content != null;
|
||||
final hasChildren = widget.children?.isNotEmpty == true;
|
||||
final hasListener = widget.onPressed != null;
|
||||
if(hasContent && hasChildren) {
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: contentWidth,
|
||||
child: content
|
||||
width: widget.contentWidth,
|
||||
child: widget.content
|
||||
),
|
||||
const SizedBox(width: 16.0),
|
||||
Icon(
|
||||
@@ -123,8 +150,8 @@ class SettingTile extends StatelessWidget {
|
||||
|
||||
if (hasContent) {
|
||||
return SizedBox(
|
||||
width: contentWidth,
|
||||
child: content
|
||||
width: widget.contentWidth,
|
||||
child: widget.content
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
class VersionNameInput extends StatelessWidget {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final TextEditingController controller;
|
||||
|
||||
VersionNameInput({Key? key, required this.controller}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => InfoLabel(
|
||||
label: translations.versionName,
|
||||
child: TextFormBox(
|
||||
controller: controller,
|
||||
placeholder: translations.versionNameLabel,
|
||||
autofocus: true,
|
||||
validator: (version) => checkVersion(version, _gameController.versions.value),
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,31 +1,25 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/version.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/add_local_version.dart';
|
||||
import 'package:reboot_launcher/src/widget/add_server_version.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class VersionSelector extends StatefulWidget {
|
||||
const VersionSelector({Key? key}) : super(key: key);
|
||||
|
||||
static Future<void> openDownloadDialog() => showAppDialog<bool>(
|
||||
builder: (context) => const AddServerVersion(),
|
||||
);
|
||||
|
||||
static Future<void> openAddDialog() => showAppDialog<bool>(
|
||||
builder: (context) => const AddLocalVersion(),
|
||||
static Future<void> openDownloadDialog({bool closable = true}) => showRebootDialog<bool>(
|
||||
builder: (context) => AddVersionDialog(
|
||||
closable: closable,
|
||||
),
|
||||
dismissWithEsc: closable
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -48,7 +42,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(
|
||||
_gameController.selectedVersion?.name ?? translations.selectVersion,
|
||||
_gameController.selectedVersion?.content.toString() ?? translations.selectVersion,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -58,30 +52,6 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
);
|
||||
});
|
||||
|
||||
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) {
|
||||
final items = _gameController.versions.value
|
||||
.map((version) => _createVersionItem(context, version))
|
||||
.toList();
|
||||
items.add(MenuFlyoutItem(
|
||||
text: Text(translations.addLocalBuildContent),
|
||||
onPressed: VersionSelector.openAddDialog
|
||||
));
|
||||
items.add(MenuFlyoutItem(
|
||||
text: Text(translations.downloadBuildContent),
|
||||
onPressed: VersionSelector.openDownloadDialog
|
||||
));
|
||||
return items;
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) => MenuFlyoutItem(
|
||||
text: _createOptionsMenu(
|
||||
version: version,
|
||||
close: true,
|
||||
child: Text(version.name),
|
||||
),
|
||||
onPressed: () => _gameController.selectedVersion = version
|
||||
);
|
||||
|
||||
Widget _createOptionsMenu({required FortniteVersion? version, required bool close, required Widget child}) => Listener(
|
||||
onPointerDown: (event) async {
|
||||
if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) {
|
||||
@@ -104,13 +74,64 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
child: child
|
||||
);
|
||||
|
||||
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) {
|
||||
final items = _gameController.versions.value
|
||||
.map((version) => _createVersionItem(context, version))
|
||||
.toList();
|
||||
items.add(MenuFlyoutItem(
|
||||
trailing: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
FluentIcons.add_24_regular,
|
||||
size: 12
|
||||
),
|
||||
),
|
||||
text: Text(translations.addVersion),
|
||||
onPressed: VersionSelector.openDownloadDialog
|
||||
));
|
||||
return items;
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) => MenuFlyoutItem(
|
||||
text: Listener(
|
||||
onPointerDown: (event) async {
|
||||
if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _openVersionOptions(version);
|
||||
},
|
||||
child: Text(version.content.toString())
|
||||
),
|
||||
trailing: IconButton(
|
||||
onPressed: () => _openVersionOptions(version),
|
||||
icon: Icon(
|
||||
FluentIcons.more_vertical_24_regular
|
||||
)
|
||||
),
|
||||
onPressed: () => _gameController.selectedVersion = version
|
||||
);
|
||||
|
||||
Future<void> _openVersionOptions(FortniteVersion version) async {
|
||||
final result = await _flyoutController.showFlyout<_ContextualOption?>(
|
||||
builder: (context) => MenuFlyout(
|
||||
items: _ContextualOption.values
|
||||
.map((entry) => _createOption(context, entry))
|
||||
.toList()
|
||||
),
|
||||
barrierDismissible: true,
|
||||
barrierColor: Colors.transparent
|
||||
);
|
||||
_handleResult(result, version, true);
|
||||
}
|
||||
|
||||
void _handleResult(_ContextualOption? result, FortniteVersion version, bool close) async {
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
switch (result) {
|
||||
case _ContextualOption.openExplorer:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
if(close) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
@@ -118,23 +139,8 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
launchUrl(version.location.uri)
|
||||
.onError((error, stackTrace) => _onExplorerError());
|
||||
break;
|
||||
case _ContextualOption.modify:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
if(close) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
await _openRenameDialog(context, version);
|
||||
break;
|
||||
case _ContextualOption.delete:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await _openDeleteDialog(context, version) ?? false;
|
||||
final result = await _openDeleteDialog(context, version) ?? false;
|
||||
if(!mounted || !result){
|
||||
return;
|
||||
}
|
||||
@@ -149,25 +155,25 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
case null:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createOption(BuildContext context, _ContextualOption entry) {
|
||||
return MenuFlyoutItem(
|
||||
text: Text(entry.name),
|
||||
text: Text(entry.translatedName),
|
||||
onPressed: () => Navigator.of(context).pop(entry)
|
||||
);
|
||||
}
|
||||
|
||||
bool _onExplorerError() {
|
||||
showInfoBar(translations.missingVersion);
|
||||
showRebootInfoBar(translations.missingVersion);
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool?> _openDeleteDialog(BuildContext context, FortniteVersion version) {
|
||||
return showAppDialog<bool>(
|
||||
return showRebootDialog<bool>(
|
||||
builder: (context) => ContentDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -189,87 +195,32 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
Button(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(translations.deleteVersionCancel),
|
||||
DialogButton(
|
||||
type: ButtonType.secondary,
|
||||
onTap: () => Navigator.of(context).pop(false),
|
||||
text: translations.deleteVersionCancel
|
||||
),
|
||||
Button(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(translations.deleteVersionConfirm),
|
||||
DialogButton(
|
||||
type: ButtonType.primary,
|
||||
onTap: () => Navigator.of(context).pop(true),
|
||||
text: translations.deleteVersionConfirm
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> _openRenameDialog(BuildContext context, FortniteVersion version) {
|
||||
var nameController = TextEditingController(text: version.name);
|
||||
var pathController = TextEditingController(text: version.location.path);
|
||||
return showAppDialog<String?>(
|
||||
builder: (context) => FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InfoLabel(
|
||||
label: translations.versionName,
|
||||
child: TextFormBox(
|
||||
controller: nameController,
|
||||
placeholder: translations.newVersionNameLabel,
|
||||
autofocus: true,
|
||||
validator: (text) => checkChangeVersion(text)
|
||||
)
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
FileSelector(
|
||||
placeholder: translations.newVersionNameLabel,
|
||||
windowTitle: translations.gameFolderPlaceWindowTitle,
|
||||
label: translations.gameFolderLabel,
|
||||
controller: pathController,
|
||||
validator: checkGameFolder,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
),
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
text: translations.newVersionNameConfirm,
|
||||
type: ButtonType.primary,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_gameController.updateVersion(version, (version) {
|
||||
version.name = nameController.text;
|
||||
version.location = Directory(pathController.text);
|
||||
});
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum _ContextualOption {
|
||||
openExplorer,
|
||||
modify,
|
||||
delete
|
||||
}
|
||||
delete;
|
||||
|
||||
extension _ContextualOptionExtension on _ContextualOption {
|
||||
String get name {
|
||||
return this == _ContextualOption.openExplorer ? translations.openInExplorer
|
||||
: this == _ContextualOption.modify ? translations.modify
|
||||
: translations.delete;
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case _ContextualOption.openExplorer:
|
||||
return translations.openInExplorer;
|
||||
case _ContextualOption.delete:
|
||||
return translations.delete;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector.dart';
|
||||
|
||||
SettingTile get versionSelectSettingTile => SettingTile(
|
||||
SettingTile buildVersionSelector({
|
||||
required GlobalKey<OverlayTargetState> key
|
||||
}) => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.play_24_regular
|
||||
),
|
||||
@@ -15,6 +18,9 @@ SettingTile get versionSelectSettingTile => SettingTile(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: SettingTile.kDefaultContentWidth,
|
||||
),
|
||||
child: const VersionSelector()
|
||||
child: OverlayTarget(
|
||||
key: key,
|
||||
child: const VersionSelector(),
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
name: reboot_launcher
|
||||
description: Graphical User Interface for Project Reboot
|
||||
version: "9.1.4"
|
||||
version: "9.2.0"
|
||||
|
||||
publish_to: 'none'
|
||||
|
||||
@@ -31,7 +31,7 @@ dependencies:
|
||||
bitsdojo_window: ^0.1.5
|
||||
window_manager: ^0.3.8
|
||||
|
||||
# Extract zip archives (for example the reboot.zip coming from github)
|
||||
# Extract zip archives (for example the reboot.zip)
|
||||
archive: ^3.3.1
|
||||
|
||||
# Cryptographic functions
|
||||
@@ -99,5 +99,3 @@ flutter:
|
||||
- assets/backend/public/
|
||||
- assets/backend/responses/
|
||||
- assets/build/
|
||||
- assets/info/en/faq/
|
||||
- assets/info/en/questions/
|
||||
@@ -19,6 +19,7 @@ WizardStyle=modern
|
||||
PrivilegesRequired=admin
|
||||
ArchitecturesAllowed=x64
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
ChangesEnvironment=yes
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
@@ -48,3 +49,8 @@ begin
|
||||
Dependency_AddVC2015To2022
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
[Registry]
|
||||
Root: HKCU; Subkey: "Environment"; ValueType:string; ValueName: "OPENSSL_ia32cap"; \
|
||||
ValueData: "~0x20000000"; Flags: preservestringtype
|
||||
|
||||
|
||||
Reference in New Issue
Block a user