Compare commits

..

14 Commits

Author SHA1 Message Date
297c0848d5 Bump version to 5.0.1 2024-06-26 18:14:57 +07:00
51ef9f9e8f Update package.json 2024-06-26 18:14:29 +07:00
9717315b79 Update README.md 2024-06-26 08:44:16 +07:00
e176ef6fc0 Update package.json 2024-06-23 17:59:24 +07:00
52694d8f8e Update better-xcloud.user.js 2024-06-23 17:30:00 +07:00
b7928ebe68 Fix stream badge showing "1h60m" instead of "2h" 2024-06-23 17:27:11 +07:00
05eddce11e Update better-xcloud.user.js 2024-06-22 16:43:22 +07:00
057da5b3ea Fix exception with navigator.vibrate() on start up 2024-06-22 16:43:18 +07:00
11ef014c74 Update build.ts 2024-06-22 16:30:22 +07:00
fa82f0ba95 Update better-xcloud.user.js 2024-06-22 16:30:02 +07:00
36db8db1e7 Minify syntax in dist file 2024-06-22 16:29:54 +07:00
d906de7803 Update better-xcloud.user.js 2024-06-22 10:36:27 +07:00
cf546123db Show "Off" when Sharpness is 0 2024-06-22 10:36:02 +07:00
d6a4d1741b Update NumberStepper 2024-06-22 10:35:45 +07:00
11 changed files with 1703 additions and 3416 deletions

View File

@ -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) > The Android app is in development at [redphx/better-xcloud-android](https://github.com/redphx/better-xcloud-android)
> [!IMPORTANT] > [!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:** **Supported platforms:**
- Windows - Windows
@ -21,50 +23,15 @@ If you like this project please give it a 🌟. Thank you 🙏.
[![Total downloads](https://img.shields.io/github/downloads/redphx/better-xcloud/total?color=%23e15f2c)](https://github.com/redphx/better-xcloud/releases) [![Total downloads](https://img.shields.io/github/downloads/redphx/better-xcloud/total?color=%23e15f2c)](https://github.com/redphx/better-xcloud/releases)
[![Total stars](https://img.shields.io/github/stars/redphx/better-xcloud?color=%23cca400)](https://github.com/redphx/better-xcloud/stargazers) [![Total stars](https://img.shields.io/github/stars/redphx/better-xcloud?color=%23cca400)](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 ## 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) - [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. ⚠️ 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">
&nbsp;
**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 ## 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). 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. Or you can give this project a star, that's also helpful.

111
build.ts
View File

@ -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" }; import txtMetaHeader from "./src/assets/header_meta.txt" with { type: "text" };
enum BuildTarget { enum BuildTarget {
ALL = 'all', ALL = 'all',
ANDROID_APP = 'android-app', ANDROID_APP = 'android-app',
MOBILE = 'mobile', MOBILE = 'mobile',
WEBOS = 'webos', WEBOS = 'webos',
} }
const postProcess = (str: string): string => { const postProcess = (str: string): string => {
@ -21,83 +21,86 @@ const postProcess = (str: string): string => {
// Replace "globalThis." with "var"; // Replace "globalThis." with "var";
str = str.replaceAll('globalThis.', 'var '); str = str.replaceAll('globalThis.', 'var ');
// Add ADDITIONAL CODE block // Add ADDITIONAL CODE block
str = str.replace('var DEFAULT_FLAGS', '\n/* ADDITIONAL CODE */\n\nvar DEFAULT_FLAGS'); str = str.replace('var DEFAULT_FLAGS', '\n/* ADDITIONAL CODE */\n\nvar DEFAULT_FLAGS');
return str; return str;
} }
const build = async (target: BuildTarget, version: string, config: any={}) => { const build = async (target: BuildTarget, version: string, config: any={}) => {
console.log('-- Target:', target); console.log('-- Target:', target);
const startTime = performance.now(); const startTime = performance.now();
let outputScriptName = 'better-xcloud'; let outputScriptName = 'better-xcloud';
if (target !== BuildTarget.ALL) { if (target !== BuildTarget.ALL) {
outputScriptName += `.${target}`; outputScriptName += `.${target}`;
} }
let outputMetaName = outputScriptName; let outputMetaName = outputScriptName;
outputScriptName += '.user.js'; outputScriptName += '.user.js';
outputMetaName += '.meta.js'; outputMetaName += '.meta.js';
const outDir = './dist'; const outDir = './dist';
let output = await Bun.build({ let output = await Bun.build({
entrypoints: ['src/index.ts'], entrypoints: ['src/index.ts'],
outdir: outDir, outdir: outDir,
naming: outputScriptName, naming: outputScriptName,
define: { minify: {
'Bun.env.BUILD_TARGET': JSON.stringify(target), syntax: true,
'Bun.env.SCRIPT_VERSION': JSON.stringify(version), },
}, define: {
}); 'Bun.env.BUILD_TARGET': JSON.stringify(target),
'Bun.env.SCRIPT_VERSION': JSON.stringify(version),
},
});
if (!output.success) { if (!output.success) {
console.log(output); console.log(output);
process.exit(1); process.exit(1);
} }
const {path} = output.outputs[0]; const {path} = output.outputs[0];
// Get generated file // Get generated file
let result = postProcess(await readFile(path, 'utf-8')); let result = postProcess(await readFile(path, 'utf-8'));
// Replace [[VERSION]] with real value // Replace [[VERSION]] with real value
const scriptHeader = txtScriptHeader.replace('[[VERSION]]', version); const scriptHeader = txtScriptHeader.replace('[[VERSION]]', version);
// Save to script // Save to script
await Bun.write(path, scriptHeader + result); await Bun.write(path, scriptHeader + result);
console.log(`---- [${target}] done in ${performance.now() - startTime} ms`); console.log(`---- [${target}] done in ${performance.now() - startTime} ms`);
// Create meta file // Create meta file
await Bun.write(outDir + '/' + outputMetaName, txtMetaHeader.replace('[[VERSION]]', version)); await Bun.write(outDir + '/' + outputMetaName, txtMetaHeader.replace('[[VERSION]]', version));
} }
const buildTargets = [ const buildTargets = [
BuildTarget.ALL, BuildTarget.ALL,
// BuildTarget.ANDROID_APP, // BuildTarget.ANDROID_APP,
// BuildTarget.MOBILE, // BuildTarget.MOBILE,
// BuildTarget.WEBOS, // BuildTarget.WEBOS,
]; ];
const { values, positionals } = parseArgs({ const { values, positionals } = parseArgs({
args: Bun.argv, args: Bun.argv,
options: { options: {
version: { version: {
type: 'string', type: 'string',
}, },
}, },
strict: true, strict: true,
allowPositionals: true, allowPositionals: true,
}); });
if (!values['version']) { if (!values['version']) {
console.log('Missing --version param'); console.log('Missing --version param');
sys.exit(-1); sys.exit(-1);
} }
console.log('Building: ', values['version']); console.log('Building: ', values['version']);
const config = {}; const config = {};
for (const target of buildTargets) { for (const target of buildTargets) {
await build(target, values['version'], config); await build(target, values['version']!!, config);
} }

View File

@ -1,5 +1,5 @@
// ==UserScript== // ==UserScript==
// @name Better xCloud // @name Better xCloud
// @namespace https://github.com/redphx // @namespace https://github.com/redphx
// @version 5.0.0 // @version 5.0.1
// ==/UserScript== // ==/UserScript==

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.1.5", "@types/bun": "^1.1.5",
"@types/node": "^20.14.7", "@types/node": "^20.14.9",
"@types/stylus": "^0.48.42", "@types/stylus": "^0.48.42",
"stylus": "^0.63.0" "stylus": "^0.63.0"
}, },

View File

@ -158,19 +158,26 @@ export class StreamBadges {
} }
#secondsToHm(seconds: number) { #secondsToHm(seconds: number) {
const h = Math.floor(seconds / 3600); let h = Math.floor(seconds / 3600);
const m = Math.floor(seconds % 3600 / 60) + 1; let m = Math.floor(seconds % 3600 / 60) + 1;
const hDisplay = h > 0 ? `${h}h`: ''; if (m === 60) {
const mDisplay = m > 0 ? `${m}m`: ''; h += 1;
return hDisplay + mDisplay; m = 0;
}
const output = [];
h > 0 && output.push(`${h}h`);
m > 0 && output.push(`${m}m`);
return output.join(' ');
} }
// https://stackoverflow.com/a/20732091 // https://stackoverflow.com/a/20732091
#humanFileSize(size: number) { #humanFileSize(size: number) {
const units = ['B', 'KB', 'MB', 'GB', 'TB']; 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]; return (size / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
} }

View File

@ -128,19 +128,19 @@ function setupStreamSettingsDialog() {
{ {
pref: PrefKey.CONTROLLER_ENABLE_VIBRATION, pref: PrefKey.CONTROLLER_ENABLE_VIBRATION,
unsupported: !VibrationManager.supportControllerVibration(), unsupported: !VibrationManager.supportControllerVibration(),
onChange: VibrationManager.updateGlobalVars, onChange: () => VibrationManager.updateGlobalVars(),
}, },
{ {
pref: PrefKey.CONTROLLER_DEVICE_VIBRATION, pref: PrefKey.CONTROLLER_DEVICE_VIBRATION,
unsupported: !VibrationManager.supportDeviceVibration(), unsupported: !VibrationManager.supportDeviceVibration(),
onChange: VibrationManager.updateGlobalVars, onChange: () => VibrationManager.updateGlobalVars(),
}, },
(VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) && { (VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) && {
pref: PrefKey.CONTROLLER_VIBRATION_INTENSITY, pref: PrefKey.CONTROLLER_VIBRATION_INTENSITY,
unsupported: !VibrationManager.supportDeviceVibration(), unsupported: !VibrationManager.supportDeviceVibration(),
onChange: VibrationManager.updateGlobalVars, onChange: () => VibrationManager.updateGlobalVars(),
}, },
], ],
}, },

View File

@ -53,7 +53,7 @@ export class VibrationManager {
return !!window.navigator.vibrate; 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_ENABLE_CONTROLLER_VIBRATION = VibrationManager.supportControllerVibration() ? getPref(PrefKey.CONTROLLER_ENABLE_VIBRATION) : false;
window.BX_VIBRATION_INTENSITY = getPref(PrefKey.CONTROLLER_VIBRATION_INTENSITY) / 100; window.BX_VIBRATION_INTENSITY = getPref(PrefKey.CONTROLLER_VIBRATION_INTENSITY) / 100;
@ -63,7 +63,7 @@ export class VibrationManager {
} }
// Stop vibration // Stop vibration
window.navigator.vibrate(0); stopVibration && window.navigator.vibrate(0);
const value = getPref(PrefKey.CONTROLLER_DEVICE_VIBRATION); const value = getPref(PrefKey.CONTROLLER_DEVICE_VIBRATION);
let enabled; let enabled;
@ -134,10 +134,10 @@ export class VibrationManager {
} }
static initialSetup() { static initialSetup() {
window.addEventListener('gamepadconnected', VibrationManager.updateGlobalVars); window.addEventListener('gamepadconnected', e => VibrationManager.updateGlobalVars());
window.addEventListener('gamepaddisconnected', VibrationManager.updateGlobalVars); window.addEventListener('gamepaddisconnected', e => VibrationManager.updateGlobalVars());
VibrationManager.updateGlobalVars(); VibrationManager.updateGlobalVars(false);
window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => { window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => {
const dataChannel = (e as any).dataChannel; const dataChannel = (e as any).dataChannel;

View File

@ -4,7 +4,8 @@ import { renderStylus } from "@macros/build" with {type: "macro"};
export function addCss() { export function addCss() {
let css = renderStylus(); const STYLUS_CSS = renderStylus();
let css = STYLUS_CSS;
if (getPref(PrefKey.BLOCK_SOCIAL_FEATURES)) { if (getPref(PrefKey.BLOCK_SOCIAL_FEATURES)) {
css += ` css += `

View File

@ -598,6 +598,10 @@ export class Preferences {
max: 10, max: 10,
params: { params: {
hideSlider: true, hideSlider: true,
customTextValue: (value: any) => {
value = parseInt(value);
return value === 0 ? t('off') : value.toString();
},
}, },
}, },
[PrefKey.VIDEO_RATIO]: { [PrefKey.VIDEO_RATIO]: {

View File

@ -132,10 +132,12 @@ export class SettingElement {
options.hideSlider = !!options.hideSlider; options.hideSlider = !!options.hideSlider;
let $text: HTMLSpanElement; let $text: HTMLSpanElement;
let $decBtn: HTMLButtonElement; let $btnDec: HTMLButtonElement;
let $incBtn: HTMLButtonElement; let $btnInc: HTMLButtonElement;
let $range: HTMLInputElement; let $range: HTMLInputElement;
let controlValue = value;
const MIN = setting.min!; const MIN = setting.min!;
const MAX = setting.max!; const MAX = setting.max!;
const STEPS = Math.max(setting.steps || 1, 1); const STEPS = Math.max(setting.steps || 1, 1);
@ -155,14 +157,19 @@ export class SettingElement {
return textContent; 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}`}, const $wrapper = CE('div', {'class': 'bx-number-stepper', id: `bx_setting_${key}`},
$decBtn = CE('button', { $btnDec = CE('button', {
'data-type': 'dec', 'data-type': 'dec',
type: 'button', type: 'button',
tabindex: -1, tabindex: -1,
}, '-') as HTMLButtonElement, }, '-') as HTMLButtonElement,
$text = CE('span', {}, renderTextValue(value)) as HTMLSpanElement, $text = CE('span', {}, renderTextValue(value)) as HTMLSpanElement,
$incBtn = CE('button', { $btnInc = CE('button', {
'data-type': 'inc', 'data-type': 'inc',
type: 'button', type: 'button',
tabindex: -1, tabindex: -1,
@ -182,6 +189,9 @@ export class SettingElement {
$range.addEventListener('input', e => { $range.addEventListener('input', e => {
value = parseInt((e.target as HTMLInputElement).value); value = parseInt((e.target as HTMLInputElement).value);
controlValue = value;
updateButtonsVisibility();
$text.textContent = renderTextValue(value); $text.textContent = renderTextValue(value);
!(e as any).ignoreOnChange && onChange && onChange(e, value); !(e as any).ignoreOnChange && onChange && onChange(e, value);
}); });
@ -212,14 +222,16 @@ export class SettingElement {
} }
if (options.disabled) { if (options.disabled) {
$incBtn.disabled = true; $btnInc.disabled = true;
$incBtn.classList.add('bx-hidden'); $btnInc.classList.add('bx-hidden');
$decBtn.disabled = true; $btnDec.disabled = true;
$decBtn.classList.add('bx-hidden'); $btnDec.classList.add('bx-hidden');
return $wrapper; return $wrapper;
} }
updateButtonsVisibility();
let interval: number; let interval: number;
let isHolding = false; let isHolding = false;
@ -231,19 +243,19 @@ export class SettingElement {
return; return;
} }
let value: number; const $btn = e.target as HTMLElement;
if ($range) { let value = parseInt(controlValue);
value = parseInt($range.value);
} else { const btnType = $btn.dataset.type;
value = parseInt($text.textContent!);
}
const btnType = (e.target as HTMLElement).getAttribute('data-type');
if (btnType === 'dec') { if (btnType === 'dec') {
value = Math.max(MIN, value - STEPS); value = Math.max(MIN, value - STEPS);
} else { } else {
value = Math.min(MAX, value + STEPS); value = Math.min(MAX, value + STEPS);
} }
controlValue = value;
updateButtonsVisibility();
$text.textContent = renderTextValue(value); $text.textContent = renderTextValue(value);
$range && ($range.value = value.toString()); $range && ($range.value = value.toString());
@ -277,19 +289,21 @@ export class SettingElement {
// Custom method // Custom method
($wrapper as any).setValue = (value: any) => { ($wrapper as any).setValue = (value: any) => {
controlValue = parseInt(value);
$text.textContent = renderTextValue(value); $text.textContent = renderTextValue(value);
$range && ($range.value = value); $range && ($range.value = value);
}; };
$decBtn.addEventListener('click', onClick); $btnDec.addEventListener('click', onClick);
$decBtn.addEventListener('pointerdown', onMouseDown); $btnDec.addEventListener('pointerdown', onMouseDown);
$decBtn.addEventListener('pointerup', onMouseUp); $btnDec.addEventListener('pointerup', onMouseUp);
$decBtn.addEventListener('contextmenu', onContextMenu); $btnDec.addEventListener('contextmenu', onContextMenu);
$incBtn.addEventListener('click', onClick); $btnInc.addEventListener('click', onClick);
$incBtn.addEventListener('pointerdown', onMouseDown); $btnInc.addEventListener('pointerdown', onMouseDown);
$incBtn.addEventListener('pointerup', onMouseUp); $btnInc.addEventListener('pointerup', onMouseUp);
$incBtn.addEventListener('contextmenu', onContextMenu); $btnInc.addEventListener('contextmenu', onContextMenu);
return $wrapper; return $wrapper;
} }