mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-07-01 03:41:44 +02:00
Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
297c0848d5 | |||
51ef9f9e8f | |||
9717315b79 | |||
e176ef6fc0 | |||
52694d8f8e | |||
b7928ebe68 | |||
05eddce11e | |||
057da5b3ea | |||
11ef014c74 | |||
fa82f0ba95 | |||
36db8db1e7 | |||
d906de7803 | |||
cf546123db | |||
d6a4d1741b |
47
README.md
47
README.md
@ -5,7 +5,9 @@ Improve Xbox Cloud Gaming (xCloud) experience on [xbox.com/play](https://www.xbo
|
||||
> The Android app is in development at [redphx/better-xcloud-android](https://github.com/redphx/better-xcloud-android)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> I don't accept pull requests at the moment (except PR for custom touch controls)
|
||||
> I only accept pull requests for:
|
||||
> - Custom touch controls
|
||||
> - Bug fixes
|
||||
|
||||
**Supported platforms:**
|
||||
- Windows
|
||||
@ -21,50 +23,15 @@ If you like this project please give it a 🌟. Thank you 🙏.
|
||||
[](https://github.com/redphx/better-xcloud/releases)
|
||||
[](https://github.com/redphx/better-xcloud/stargazers)
|
||||
|
||||
## How to install
|
||||
Visit the [home page](https://better-xcloud.github.io) to know how to install Better xCloud on your device.
|
||||
|
||||
## Full documentations
|
||||
- For the full details please visit: https://better-xcloud.github.io
|
||||
- For the full details please visit: [**better-xcloud.github.io**](https://better-xcloud.github.io)
|
||||
- [Demo video](https://youtu.be/hyp69Jrb2sQ)
|
||||
|
||||
⚠️ Please DO NOT report **Better xCloud**'s bugs on [/r/xcloud subreddit](https://reddit.com/r/xcloud/). Report bugs in [Issues](https://github.com/redphx/better-xcloud/issues) or [Telegram channel](https://t.me/betterxcloud) instead.
|
||||
|
||||
## Table of Contents
|
||||
- [**How to install**](#how-to-install)
|
||||
- [**Features**](#features)
|
||||
- [**Donation**](#donation)
|
||||
- [**Acknowledgements**](#acknowledgements)
|
||||
- [**Disclaimers**](#disclaimers)
|
||||
|
||||
## How to install
|
||||
Visit [this page](https://better-xcloud.github.io/browsers) to know how to install Better xCloud on your device.
|
||||
|
||||
## Features
|
||||
|
||||
<img width="400" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/4bec2d62-31df-499c-9aad-2485626b6925">
|
||||
<br>
|
||||
<img width="400" alt="Remote Play dialog" src="https://github.com/redphx/better-xcloud/assets/96280/daf7f698-a228-4f9c-8f23-9669e061a64c">
|
||||
<br>
|
||||
<img width="600" alt="Stream HUD" src="https://github.com/redphx/better-xcloud/assets/96280/51bdb96c-79ab-402f-902a-a9e6229973b2">
|
||||
<br>
|
||||
<img width="600" alt="Stream settings" src="https://github.com/redphx/better-xcloud/assets/96280/ed513cb3-6e6c-4e8e-9e06-c62e71e41c90">
|
||||
<br>
|
||||
<img width="600" alt="Remapper" src="https://github.com/redphx/better-xcloud/assets/96280/f2e2bc51-f673-4b24-b127-c7169b86462b">
|
||||
|
||||
|
||||
|
||||
|
||||
**Demo video:** [https://youtu.be/oDr5Eddp55E ](https://youtu.be/AYb-EUcz72U)
|
||||
- **🔥 Totally free and open-source**
|
||||
- **🔥 Allow playing with [Mouse & Keyboard](https://better-xcloud.github.io/mouse-and-keyboard)**
|
||||
- **🔥 Enable [Remote Play](https://better-xcloud.github.io/remote-play) support**
|
||||
> 1080p resolution and can stream Xbox 360 games.
|
||||
- **🔥 [Improve visual quality](https://better-xcloud.github.io/ingame-features/#improve-streams-clarity) of the stream**
|
||||
> Similar to (but not as good as) the "Clarity Boost" of xCloud on Edge browser. [Demo video](https://youtu.be/ZhW2choAHUs).
|
||||
- **🔥 Show [Stream stats](https://better-xcloud.github.io/stream-stats)**
|
||||
- **🔥 [Screenshot capture](https://better-xcloud.github.io/screenshot-capture)**
|
||||
- **🔥 [Touch controller](https://better-xcloud.github.io/features/#touch-controller)**
|
||||
> Enable touch controller support for all games.
|
||||
- [And more...](https://better-xcloud.github.io/features/)
|
||||
|
||||
## Donation
|
||||
If you think this project is useful and want to support future developments, please consider making a donate via [my Ko-fi page](https://ko-fi.com/redphx).
|
||||
Or you can give this project a star, that's also helpful.
|
||||
|
111
build.ts
111
build.ts
@ -6,10 +6,10 @@ import txtScriptHeader from "./src/assets/header_script.txt" with { type: "text"
|
||||
import txtMetaHeader from "./src/assets/header_meta.txt" with { type: "text" };
|
||||
|
||||
enum BuildTarget {
|
||||
ALL = 'all',
|
||||
ANDROID_APP = 'android-app',
|
||||
MOBILE = 'mobile',
|
||||
WEBOS = 'webos',
|
||||
ALL = 'all',
|
||||
ANDROID_APP = 'android-app',
|
||||
MOBILE = 'mobile',
|
||||
WEBOS = 'webos',
|
||||
}
|
||||
|
||||
const postProcess = (str: string): string => {
|
||||
@ -21,83 +21,86 @@ const postProcess = (str: string): string => {
|
||||
// Replace "globalThis." with "var";
|
||||
str = str.replaceAll('globalThis.', 'var ');
|
||||
|
||||
// Add ADDITIONAL CODE block
|
||||
str = str.replace('var DEFAULT_FLAGS', '\n/* ADDITIONAL CODE */\n\nvar DEFAULT_FLAGS');
|
||||
// Add ADDITIONAL CODE block
|
||||
str = str.replace('var DEFAULT_FLAGS', '\n/* ADDITIONAL CODE */\n\nvar DEFAULT_FLAGS');
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
const build = async (target: BuildTarget, version: string, config: any={}) => {
|
||||
console.log('-- Target:', target);
|
||||
const startTime = performance.now();
|
||||
console.log('-- Target:', target);
|
||||
const startTime = performance.now();
|
||||
|
||||
let outputScriptName = 'better-xcloud';
|
||||
if (target !== BuildTarget.ALL) {
|
||||
outputScriptName += `.${target}`;
|
||||
}
|
||||
let outputMetaName = outputScriptName;
|
||||
outputScriptName += '.user.js';
|
||||
outputMetaName += '.meta.js';
|
||||
let outputScriptName = 'better-xcloud';
|
||||
if (target !== BuildTarget.ALL) {
|
||||
outputScriptName += `.${target}`;
|
||||
}
|
||||
let outputMetaName = outputScriptName;
|
||||
outputScriptName += '.user.js';
|
||||
outputMetaName += '.meta.js';
|
||||
|
||||
const outDir = './dist';
|
||||
const outDir = './dist';
|
||||
|
||||
let output = await Bun.build({
|
||||
entrypoints: ['src/index.ts'],
|
||||
outdir: outDir,
|
||||
naming: outputScriptName,
|
||||
define: {
|
||||
'Bun.env.BUILD_TARGET': JSON.stringify(target),
|
||||
'Bun.env.SCRIPT_VERSION': JSON.stringify(version),
|
||||
},
|
||||
});
|
||||
let output = await Bun.build({
|
||||
entrypoints: ['src/index.ts'],
|
||||
outdir: outDir,
|
||||
naming: outputScriptName,
|
||||
minify: {
|
||||
syntax: true,
|
||||
},
|
||||
define: {
|
||||
'Bun.env.BUILD_TARGET': JSON.stringify(target),
|
||||
'Bun.env.SCRIPT_VERSION': JSON.stringify(version),
|
||||
},
|
||||
});
|
||||
|
||||
if (!output.success) {
|
||||
console.log(output);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!output.success) {
|
||||
console.log(output);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const {path} = output.outputs[0];
|
||||
// Get generated file
|
||||
let result = postProcess(await readFile(path, 'utf-8'));
|
||||
const {path} = output.outputs[0];
|
||||
// Get generated file
|
||||
let result = postProcess(await readFile(path, 'utf-8'));
|
||||
|
||||
// Replace [[VERSION]] with real value
|
||||
const scriptHeader = txtScriptHeader.replace('[[VERSION]]', version);
|
||||
// Replace [[VERSION]] with real value
|
||||
const scriptHeader = txtScriptHeader.replace('[[VERSION]]', version);
|
||||
|
||||
// Save to script
|
||||
await Bun.write(path, scriptHeader + result);
|
||||
console.log(`---- [${target}] done in ${performance.now() - startTime} ms`);
|
||||
// Save to script
|
||||
await Bun.write(path, scriptHeader + result);
|
||||
console.log(`---- [${target}] done in ${performance.now() - startTime} ms`);
|
||||
|
||||
// Create meta file
|
||||
await Bun.write(outDir + '/' + outputMetaName, txtMetaHeader.replace('[[VERSION]]', version));
|
||||
// Create meta file
|
||||
await Bun.write(outDir + '/' + outputMetaName, txtMetaHeader.replace('[[VERSION]]', version));
|
||||
}
|
||||
|
||||
const buildTargets = [
|
||||
BuildTarget.ALL,
|
||||
// BuildTarget.ANDROID_APP,
|
||||
// BuildTarget.MOBILE,
|
||||
// BuildTarget.WEBOS,
|
||||
BuildTarget.ALL,
|
||||
// BuildTarget.ANDROID_APP,
|
||||
// BuildTarget.MOBILE,
|
||||
// BuildTarget.WEBOS,
|
||||
];
|
||||
|
||||
const { values, positionals } = parseArgs({
|
||||
args: Bun.argv,
|
||||
options: {
|
||||
version: {
|
||||
type: 'string',
|
||||
args: Bun.argv,
|
||||
options: {
|
||||
version: {
|
||||
type: 'string',
|
||||
|
||||
},
|
||||
},
|
||||
strict: true,
|
||||
allowPositionals: true,
|
||||
},
|
||||
},
|
||||
strict: true,
|
||||
allowPositionals: true,
|
||||
});
|
||||
|
||||
if (!values['version']) {
|
||||
console.log('Missing --version param');
|
||||
sys.exit(-1);
|
||||
console.log('Missing --version param');
|
||||
sys.exit(-1);
|
||||
}
|
||||
|
||||
console.log('Building: ', values['version']);
|
||||
|
||||
const config = {};
|
||||
for (const target of buildTargets) {
|
||||
await build(target, values['version'], config);
|
||||
await build(target, values['version']!!, config);
|
||||
}
|
||||
|
2
dist/better-xcloud.meta.js
vendored
2
dist/better-xcloud.meta.js
vendored
@ -1,5 +1,5 @@
|
||||
// ==UserScript==
|
||||
// @name Better xCloud
|
||||
// @namespace https://github.com/redphx
|
||||
// @version 5.0.0
|
||||
// @version 5.0.1
|
||||
// ==/UserScript==
|
||||
|
4855
dist/better-xcloud.user.js
vendored
4855
dist/better-xcloud.user.js
vendored
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.5",
|
||||
"@types/node": "^20.14.7",
|
||||
"@types/node": "^20.14.9",
|
||||
"@types/stylus": "^0.48.42",
|
||||
"stylus": "^0.63.0"
|
||||
},
|
||||
|
@ -158,19 +158,26 @@ export class StreamBadges {
|
||||
}
|
||||
|
||||
#secondsToHm(seconds: number) {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor(seconds % 3600 / 60) + 1;
|
||||
let h = Math.floor(seconds / 3600);
|
||||
let m = Math.floor(seconds % 3600 / 60) + 1;
|
||||
|
||||
const hDisplay = h > 0 ? `${h}h`: '';
|
||||
const mDisplay = m > 0 ? `${m}m`: '';
|
||||
return hDisplay + mDisplay;
|
||||
if (m === 60) {
|
||||
h += 1;
|
||||
m = 0;
|
||||
}
|
||||
|
||||
const output = [];
|
||||
h > 0 && output.push(`${h}h`);
|
||||
m > 0 && output.push(`${m}m`);
|
||||
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/20732091
|
||||
#humanFileSize(size: number) {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
|
||||
let i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
|
||||
const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
|
||||
return (size / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
|
||||
}
|
||||
|
||||
|
@ -128,19 +128,19 @@ function setupStreamSettingsDialog() {
|
||||
{
|
||||
pref: PrefKey.CONTROLLER_ENABLE_VIBRATION,
|
||||
unsupported: !VibrationManager.supportControllerVibration(),
|
||||
onChange: VibrationManager.updateGlobalVars,
|
||||
onChange: () => VibrationManager.updateGlobalVars(),
|
||||
},
|
||||
|
||||
{
|
||||
pref: PrefKey.CONTROLLER_DEVICE_VIBRATION,
|
||||
unsupported: !VibrationManager.supportDeviceVibration(),
|
||||
onChange: VibrationManager.updateGlobalVars,
|
||||
onChange: () => VibrationManager.updateGlobalVars(),
|
||||
},
|
||||
|
||||
(VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) && {
|
||||
pref: PrefKey.CONTROLLER_VIBRATION_INTENSITY,
|
||||
unsupported: !VibrationManager.supportDeviceVibration(),
|
||||
onChange: VibrationManager.updateGlobalVars,
|
||||
onChange: () => VibrationManager.updateGlobalVars(),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -53,7 +53,7 @@ export class VibrationManager {
|
||||
return !!window.navigator.vibrate;
|
||||
}
|
||||
|
||||
static updateGlobalVars() {
|
||||
static updateGlobalVars(stopVibration: boolean = true) {
|
||||
window.BX_ENABLE_CONTROLLER_VIBRATION = VibrationManager.supportControllerVibration() ? getPref(PrefKey.CONTROLLER_ENABLE_VIBRATION) : false;
|
||||
window.BX_VIBRATION_INTENSITY = getPref(PrefKey.CONTROLLER_VIBRATION_INTENSITY) / 100;
|
||||
|
||||
@ -63,7 +63,7 @@ export class VibrationManager {
|
||||
}
|
||||
|
||||
// Stop vibration
|
||||
window.navigator.vibrate(0);
|
||||
stopVibration && window.navigator.vibrate(0);
|
||||
|
||||
const value = getPref(PrefKey.CONTROLLER_DEVICE_VIBRATION);
|
||||
let enabled;
|
||||
@ -134,10 +134,10 @@ export class VibrationManager {
|
||||
}
|
||||
|
||||
static initialSetup() {
|
||||
window.addEventListener('gamepadconnected', VibrationManager.updateGlobalVars);
|
||||
window.addEventListener('gamepaddisconnected', VibrationManager.updateGlobalVars);
|
||||
window.addEventListener('gamepadconnected', e => VibrationManager.updateGlobalVars());
|
||||
window.addEventListener('gamepaddisconnected', e => VibrationManager.updateGlobalVars());
|
||||
|
||||
VibrationManager.updateGlobalVars();
|
||||
VibrationManager.updateGlobalVars(false);
|
||||
|
||||
window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => {
|
||||
const dataChannel = (e as any).dataChannel;
|
||||
|
@ -4,7 +4,8 @@ import { renderStylus } from "@macros/build" with {type: "macro"};
|
||||
|
||||
|
||||
export function addCss() {
|
||||
let css = renderStylus();
|
||||
const STYLUS_CSS = renderStylus();
|
||||
let css = STYLUS_CSS;
|
||||
|
||||
if (getPref(PrefKey.BLOCK_SOCIAL_FEATURES)) {
|
||||
css += `
|
||||
|
@ -598,6 +598,10 @@ export class Preferences {
|
||||
max: 10,
|
||||
params: {
|
||||
hideSlider: true,
|
||||
customTextValue: (value: any) => {
|
||||
value = parseInt(value);
|
||||
return value === 0 ? t('off') : value.toString();
|
||||
},
|
||||
},
|
||||
},
|
||||
[PrefKey.VIDEO_RATIO]: {
|
||||
|
@ -132,10 +132,12 @@ export class SettingElement {
|
||||
options.hideSlider = !!options.hideSlider;
|
||||
|
||||
let $text: HTMLSpanElement;
|
||||
let $decBtn: HTMLButtonElement;
|
||||
let $incBtn: HTMLButtonElement;
|
||||
let $btnDec: HTMLButtonElement;
|
||||
let $btnInc: HTMLButtonElement;
|
||||
let $range: HTMLInputElement;
|
||||
|
||||
let controlValue = value;
|
||||
|
||||
const MIN = setting.min!;
|
||||
const MAX = setting.max!;
|
||||
const STEPS = Math.max(setting.steps || 1, 1);
|
||||
@ -155,14 +157,19 @@ export class SettingElement {
|
||||
return textContent;
|
||||
};
|
||||
|
||||
const updateButtonsVisibility = () => {
|
||||
$btnDec.classList.toggle('bx-hidden', controlValue === MIN);
|
||||
$btnInc.classList.toggle('bx-hidden', controlValue === MAX);
|
||||
}
|
||||
|
||||
const $wrapper = CE('div', {'class': 'bx-number-stepper', id: `bx_setting_${key}`},
|
||||
$decBtn = CE('button', {
|
||||
$btnDec = CE('button', {
|
||||
'data-type': 'dec',
|
||||
type: 'button',
|
||||
tabindex: -1,
|
||||
}, '-') as HTMLButtonElement,
|
||||
$text = CE('span', {}, renderTextValue(value)) as HTMLSpanElement,
|
||||
$incBtn = CE('button', {
|
||||
$btnInc = CE('button', {
|
||||
'data-type': 'inc',
|
||||
type: 'button',
|
||||
tabindex: -1,
|
||||
@ -182,6 +189,9 @@ export class SettingElement {
|
||||
|
||||
$range.addEventListener('input', e => {
|
||||
value = parseInt((e.target as HTMLInputElement).value);
|
||||
controlValue = value;
|
||||
updateButtonsVisibility();
|
||||
|
||||
$text.textContent = renderTextValue(value);
|
||||
!(e as any).ignoreOnChange && onChange && onChange(e, value);
|
||||
});
|
||||
@ -212,14 +222,16 @@ export class SettingElement {
|
||||
}
|
||||
|
||||
if (options.disabled) {
|
||||
$incBtn.disabled = true;
|
||||
$incBtn.classList.add('bx-hidden');
|
||||
$btnInc.disabled = true;
|
||||
$btnInc.classList.add('bx-hidden');
|
||||
|
||||
$decBtn.disabled = true;
|
||||
$decBtn.classList.add('bx-hidden');
|
||||
$btnDec.disabled = true;
|
||||
$btnDec.classList.add('bx-hidden');
|
||||
return $wrapper;
|
||||
}
|
||||
|
||||
updateButtonsVisibility();
|
||||
|
||||
let interval: number;
|
||||
let isHolding = false;
|
||||
|
||||
@ -231,19 +243,19 @@ export class SettingElement {
|
||||
return;
|
||||
}
|
||||
|
||||
let value: number;
|
||||
if ($range) {
|
||||
value = parseInt($range.value);
|
||||
} else {
|
||||
value = parseInt($text.textContent!);
|
||||
}
|
||||
const btnType = (e.target as HTMLElement).getAttribute('data-type');
|
||||
const $btn = e.target as HTMLElement;
|
||||
let value = parseInt(controlValue);
|
||||
|
||||
const btnType = $btn.dataset.type;
|
||||
if (btnType === 'dec') {
|
||||
value = Math.max(MIN, value - STEPS);
|
||||
} else {
|
||||
value = Math.min(MAX, value + STEPS);
|
||||
}
|
||||
|
||||
controlValue = value;
|
||||
updateButtonsVisibility();
|
||||
|
||||
$text.textContent = renderTextValue(value);
|
||||
$range && ($range.value = value.toString());
|
||||
|
||||
@ -277,19 +289,21 @@ export class SettingElement {
|
||||
|
||||
// Custom method
|
||||
($wrapper as any).setValue = (value: any) => {
|
||||
controlValue = parseInt(value);
|
||||
|
||||
$text.textContent = renderTextValue(value);
|
||||
$range && ($range.value = value);
|
||||
};
|
||||
|
||||
$decBtn.addEventListener('click', onClick);
|
||||
$decBtn.addEventListener('pointerdown', onMouseDown);
|
||||
$decBtn.addEventListener('pointerup', onMouseUp);
|
||||
$decBtn.addEventListener('contextmenu', onContextMenu);
|
||||
$btnDec.addEventListener('click', onClick);
|
||||
$btnDec.addEventListener('pointerdown', onMouseDown);
|
||||
$btnDec.addEventListener('pointerup', onMouseUp);
|
||||
$btnDec.addEventListener('contextmenu', onContextMenu);
|
||||
|
||||
$incBtn.addEventListener('click', onClick);
|
||||
$incBtn.addEventListener('pointerdown', onMouseDown);
|
||||
$incBtn.addEventListener('pointerup', onMouseUp);
|
||||
$incBtn.addEventListener('contextmenu', onContextMenu);
|
||||
$btnInc.addEventListener('click', onClick);
|
||||
$btnInc.addEventListener('pointerdown', onMouseDown);
|
||||
$btnInc.addEventListener('pointerup', onMouseUp);
|
||||
$btnInc.addEventListener('contextmenu', onContextMenu);
|
||||
|
||||
return $wrapper;
|
||||
}
|
||||
|
Reference in New Issue
Block a user