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)
> [!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 🙏.
[![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)
## 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">
&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
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
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" };
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);
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -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"
},

View File

@ -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];
}

View File

@ -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(),
},
],
},

View File

@ -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;

View File

@ -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 += `

View File

@ -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]: {

View File

@ -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;
}