mirror of
https://github.com/LizardByte/Sunshine.git
synced 2025-08-10 00:52:16 +00:00
Deploy site from a70cf5e12e
This commit is contained in:
2
404.html
2
404.html
@@ -419,6 +419,8 @@
|
||||
<a title="Privacy Policy" href="/privacy" class="text_muted">Privacy</a>
|
||||
•
|
||||
<a title="Terms and Conditions" href="/terms" class="text_muted">Terms</a>
|
||||
•
|
||||
<a title="Licenses and Attribution" href="/licenses" class="text_muted">Licenses</a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,362 +0,0 @@
|
||||
// GamepadHelper library
|
||||
const GamepadHelper = (function() {
|
||||
// Controller type definitions
|
||||
const CONTROLLER_TYPES = {
|
||||
XBOX: 'xbox',
|
||||
PLAYSTATION: 'playstation',
|
||||
SWITCH: 'switch',
|
||||
STANDARD: 'standard'
|
||||
};
|
||||
|
||||
// Exact gamepad ID mappings with numbered indices
|
||||
const exactGamepadMappings = {
|
||||
0: {
|
||||
name: "Generic Gamepad",
|
||||
gamepad_api_ids: [
|
||||
"USB Gamepad (STANDARD GAMEPAD Vendor: 0079 Product: 0011)",
|
||||
"Logitech Cordless RumblePad 2 (STANDARD GAMEPAD Vendor: 046d Product: c219)",
|
||||
"Unknown Gamepad (Vendor: 2563 Product: 0575)",
|
||||
"PC/PS3/Android (Vendor: 2563 Product: 0575)",
|
||||
"Core (Plus) Wired Controller (Vendor: 20d6 Product: a711)",
|
||||
"Wireless Controller Extended Gamepad",
|
||||
],
|
||||
type: CONTROLLER_TYPES.STANDARD,
|
||||
},
|
||||
1: {
|
||||
name: "Sony PlayStation 3",
|
||||
gamepad_api_ids: [
|
||||
"54c-268-PLAYSTATION(R)3 Controller",
|
||||
"PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268)",
|
||||
"PLAYSTATION(R)3 Controller (Vendor: 054c Product: 0268)",
|
||||
"PS3 GamePad (Vendor: 054c Product: 0268)",
|
||||
"PS3/PC Wired GamePad (Vendor: 2563 Product: 0523)",
|
||||
],
|
||||
type: CONTROLLER_TYPES.PLAYSTATION
|
||||
},
|
||||
2: {
|
||||
name: "Sony DualShock (PS4)",
|
||||
gamepad_api_ids: [
|
||||
"054c-05c4-Wireless Controller",
|
||||
"Wireless controller (STANDARD GAMEPAD Vendor: 054c Product: 05c4)",
|
||||
"054c-09cc-Unknown Gamepad",
|
||||
"Unknown Gamepad (STANDARD GAMEPAD Vendor: 054c Product: 09cc)",
|
||||
"DS4 Wired Controller (Vendor: 7545 Product: 0104)",
|
||||
],
|
||||
type: CONTROLLER_TYPES.PLAYSTATION
|
||||
},
|
||||
3: {
|
||||
name: "Sony DualSense (PS5)",
|
||||
gamepad_api_ids: [
|
||||
"054c-0ce6-Wireless Controller",
|
||||
"Wireless Controller (Vendor: 054c Product: 0ce6)",
|
||||
"Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 0ce6)",
|
||||
],
|
||||
type: CONTROLLER_TYPES.PLAYSTATION
|
||||
},
|
||||
4: {
|
||||
name: "Xbox",
|
||||
gamepad_api_ids: [
|
||||
"xinput",
|
||||
"Xbox Wireless Controller Extended Gamepad",
|
||||
"Xbox Wireless Controller",
|
||||
],
|
||||
type: CONTROLLER_TYPES.XBOX
|
||||
},
|
||||
5: {
|
||||
name: "Xbox 360",
|
||||
gamepad_api_ids: [
|
||||
],
|
||||
type: CONTROLLER_TYPES.XBOX
|
||||
},
|
||||
6: {
|
||||
name: "Xbox One/Series",
|
||||
gamepad_api_ids: [
|
||||
"HID-compliant game controller (STANDARD GAMEPAD Vendor: 045e Product: 0b13)",
|
||||
],
|
||||
type: CONTROLLER_TYPES.XBOX
|
||||
},
|
||||
7: {
|
||||
name: "Nintendo Switch Pro Controller",
|
||||
gamepad_api_ids: [
|
||||
"Pro Controller (STANDARD GAMEPAD Vendor: 057e Product: 2009)",
|
||||
],
|
||||
type: CONTROLLER_TYPES.SWITCH
|
||||
},
|
||||
8: {
|
||||
name: "Stadia Controller",
|
||||
gamepad_api_ids: [
|
||||
"Stadia Controller rev. A (STANDARD GAMEPAD Vendor: 18d1 Product: 9400)",
|
||||
],
|
||||
type: CONTROLLER_TYPES.STANDARD,
|
||||
},
|
||||
9: {
|
||||
name: "SNES Gamepad",
|
||||
gamepad_api_ids: [
|
||||
"usb gamepad (Vendor: 0810 Product: e501)",
|
||||
],
|
||||
type: CONTROLLER_TYPES.STANDARD,
|
||||
}
|
||||
};
|
||||
|
||||
// Build a lookup map for fast exact matching
|
||||
const exactIdLookup = {};
|
||||
Object.values(exactGamepadMappings).forEach(mapping => {
|
||||
mapping.gamepad_api_ids.forEach(id => {
|
||||
exactIdLookup[id] = mapping;
|
||||
});
|
||||
});
|
||||
|
||||
// Controller mappings with regex patterns for fallback detection
|
||||
const controllerMappings = {
|
||||
// Xbox controllers
|
||||
[CONTROLLER_TYPES.XBOX]: {
|
||||
buttonMap: {
|
||||
0: 'A',
|
||||
1: 'B',
|
||||
2: 'X',
|
||||
3: 'Y',
|
||||
4: 'LB',
|
||||
5: 'RB',
|
||||
6: 'LT',
|
||||
7: 'RT',
|
||||
8: 'Back',
|
||||
9: 'Start',
|
||||
10: 'LS',
|
||||
11: 'RS',
|
||||
12: 'DUp',
|
||||
13: 'DDown',
|
||||
14: 'DLeft',
|
||||
15: 'DRight',
|
||||
16: 'Home',
|
||||
},
|
||||
axisMap: {
|
||||
0: 'Left Stick X',
|
||||
1: 'Left Stick Y',
|
||||
2: 'Right Stick X',
|
||||
3: 'Right Stick Y'
|
||||
}
|
||||
},
|
||||
|
||||
// PlayStation controllers
|
||||
[CONTROLLER_TYPES.PLAYSTATION]: {
|
||||
buttonMap: {
|
||||
0: '×',
|
||||
1: '○',
|
||||
2: '□',
|
||||
3: '△',
|
||||
4: 'L1',
|
||||
5: 'R1',
|
||||
6: 'L2',
|
||||
7: 'R2',
|
||||
8: 'Share',
|
||||
9: 'Options',
|
||||
10: 'L3',
|
||||
11: 'R3',
|
||||
12: 'DUp',
|
||||
13: 'DDown',
|
||||
14: 'DLeft',
|
||||
15: 'DRight',
|
||||
16: 'PS',
|
||||
17: 'TouchPad',
|
||||
},
|
||||
axisMap: {
|
||||
0: 'Left Stick X',
|
||||
1: 'Left Stick Y',
|
||||
2: 'Right Stick X',
|
||||
3: 'Right Stick Y'
|
||||
}
|
||||
},
|
||||
|
||||
// Nintendo Switch controllers
|
||||
[CONTROLLER_TYPES.SWITCH]: {
|
||||
buttonMap: {
|
||||
0: 'B',
|
||||
1: 'A',
|
||||
2: 'Y',
|
||||
3: 'X',
|
||||
4: 'L',
|
||||
5: 'R',
|
||||
6: 'ZL',
|
||||
7: 'ZR',
|
||||
8: 'Minus',
|
||||
9: 'Plus',
|
||||
10: 'LS',
|
||||
11: 'RS',
|
||||
12: 'DUp',
|
||||
13: 'DDown',
|
||||
14: 'DLeft',
|
||||
15: 'DRight',
|
||||
16: 'Home',
|
||||
17: 'Capture',
|
||||
},
|
||||
axisMap: {
|
||||
0: 'Left Stick X',
|
||||
1: 'Left Stick Y',
|
||||
2: 'Right Stick X',
|
||||
3: 'Right Stick Y'
|
||||
}
|
||||
},
|
||||
|
||||
// Default mapping for unknown controllers
|
||||
[CONTROLLER_TYPES.STANDARD]: {
|
||||
buttonMap: {}, // Will use index numbers by default
|
||||
axisMap: {
|
||||
0: 'Axis 0',
|
||||
1: 'Axis 1',
|
||||
2: 'Axis 2',
|
||||
3: 'Axis 3'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check if Gamepad API is supported
|
||||
function isSupported() {
|
||||
return !!navigator.getGamepads;
|
||||
}
|
||||
|
||||
// Get gamepad info - combines exact matching with regex fallback
|
||||
function getGamepadInfo(gamepadId) {
|
||||
if (!gamepadId) {
|
||||
return {
|
||||
type: CONTROLLER_TYPES.STANDARD,
|
||||
name: 'Generic Controller'
|
||||
};
|
||||
}
|
||||
|
||||
// Check for exact match first
|
||||
const exactMatch = exactIdLookup[gamepadId];
|
||||
if (exactMatch) {
|
||||
return {
|
||||
type: exactMatch.type,
|
||||
name: exactMatch.name
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: CONTROLLER_TYPES.STANDARD,
|
||||
name: 'Generic Controller'
|
||||
};
|
||||
}
|
||||
|
||||
// Detect controller type from gamepad ID (simplified version for backward compatibility)
|
||||
function detectControllerType(gamepadId) {
|
||||
return getGamepadInfo(gamepadId).type;
|
||||
}
|
||||
|
||||
// Get button name for given controller type and button index
|
||||
function getButtonName(controllerType, buttonIndex) {
|
||||
const mapping = controllerMappings[controllerType] || controllerMappings[CONTROLLER_TYPES.STANDARD];
|
||||
return mapping.buttonMap[buttonIndex] || `B${buttonIndex}`;
|
||||
}
|
||||
|
||||
// Get axis name for given controller type and axis index
|
||||
function getAxisName(controllerType, axisIndex) {
|
||||
const mapping = controllerMappings[controllerType] || controllerMappings[CONTROLLER_TYPES.STANDARD];
|
||||
return mapping.axisMap && mapping.axisMap[axisIndex] || `Axis ${axisIndex}`;
|
||||
}
|
||||
|
||||
/// Detect if vibration is supported and return support information
|
||||
function isVibrationSupported(gamepad) {
|
||||
if (!gamepad || !gamepad.vibrationActuator) return false;
|
||||
|
||||
// Return true for any actuator type, not just dual-rumble
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get detailed information about vibration capabilities
|
||||
function getVibrationCapabilities(gamepad) {
|
||||
if (!gamepad || !gamepad.vibrationActuator) {
|
||||
return { supported: false, type: null };
|
||||
}
|
||||
|
||||
return {
|
||||
supported: true,
|
||||
type: gamepad.vibrationActuator.type || 'unknown'
|
||||
};
|
||||
}
|
||||
|
||||
// Apply vibration if supported, with adaptive behavior for different actuator types
|
||||
function vibrate(gamepad, options = {}) {
|
||||
const { weakMagnitude = 0.5, strongMagnitude = 0.5, duration = 1000, startDelay = 0 } = options;
|
||||
|
||||
if (!gamepad || !gamepad.vibrationActuator) {
|
||||
return Promise.reject(new Error('Vibration not supported on this gamepad'));
|
||||
}
|
||||
|
||||
const actuator = gamepad.vibrationActuator;
|
||||
const actuatorType = actuator.type || 'unknown';
|
||||
|
||||
// Different handling based on actuator type
|
||||
switch (actuatorType) {
|
||||
case 'dual-rumble':
|
||||
return actuator.playEffect("dual-rumble", {
|
||||
startDelay: startDelay,
|
||||
duration: duration,
|
||||
weakMagnitude: weakMagnitude,
|
||||
strongMagnitude: strongMagnitude
|
||||
});
|
||||
|
||||
case 'vibration':
|
||||
// Some devices just have a simple vibration effect
|
||||
const magnitude = Math.max(weakMagnitude, strongMagnitude);
|
||||
return actuator.playEffect("vibration", {
|
||||
startDelay: startDelay,
|
||||
duration: duration,
|
||||
magnitude: magnitude
|
||||
});
|
||||
|
||||
default:
|
||||
// Try the default effect type for unknown actuators
|
||||
try {
|
||||
return actuator.playEffect(actuator.type, {
|
||||
startDelay: startDelay,
|
||||
duration: duration,
|
||||
weakMagnitude: weakMagnitude,
|
||||
strongMagnitude: strongMagnitude,
|
||||
magnitude: Math.max(weakMagnitude, strongMagnitude)
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`Attempted to use unknown actuator type: ${actuatorType}`);
|
||||
// Fallback to dual-rumble as it's the most common
|
||||
return actuator.playEffect("dual-rumble", {
|
||||
startDelay: startDelay,
|
||||
duration: duration,
|
||||
weakMagnitude: weakMagnitude,
|
||||
strongMagnitude: strongMagnitude
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop vibration
|
||||
function stopVibration(gamepad) {
|
||||
if (gamepad && gamepad.vibrationActuator) {
|
||||
return vibrate(gamepad, { weakMagnitude: 0, strongMagnitude: 0 });
|
||||
}
|
||||
|
||||
return Promise.reject(new Error('Vibration not supported on this browser or gamepad'));
|
||||
}
|
||||
|
||||
// Get all connected gamepads
|
||||
function getConnectedGamepads() {
|
||||
if (!isSupported()) return [];
|
||||
|
||||
const gamepads = navigator.getGamepads();
|
||||
return Array.from(gamepads).filter(gamepad => gamepad !== null);
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
isSupported,
|
||||
detectControllerType,
|
||||
getGamepadInfo,
|
||||
getButtonName,
|
||||
getAxisName,
|
||||
isVibrationSupported,
|
||||
getVibrationCapabilities,
|
||||
vibrate,
|
||||
stopVibration,
|
||||
getConnectedGamepads,
|
||||
CONTROLLER_TYPES
|
||||
};
|
||||
})();
|
||||
@@ -1,4 +1,6 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const gamepadHelper = new GamepadHelper()
|
||||
const gamepadHelperVersion = window.gamepadHelperVersion;
|
||||
let gamepads = {};
|
||||
let activeGamepadIndex = null;
|
||||
let animationFrameId = null;
|
||||
@@ -8,27 +10,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
let gamepadStatus = document.getElementById('gamepad-status');
|
||||
|
||||
// Check if the Gamepad API is supported
|
||||
if (!GamepadHelper.isSupported()) {
|
||||
if (!gamepadHelper.isSupported()) {
|
||||
gamepadStatus.textContent = 'Gamepad API not supported in this browser';
|
||||
gamepadStatus.classList.remove('alert-warning');
|
||||
gamepadStatus.classList.add('alert-danger');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize button colors for various states
|
||||
const buttonColors = {
|
||||
'standard': {
|
||||
'inactive': 'btn-outline-primary',
|
||||
'active': 'btn-primary'
|
||||
},
|
||||
};
|
||||
|
||||
// Axis names mapping
|
||||
const axisNames = [
|
||||
'Left Stick X', 'Left Stick Y',
|
||||
'Right Stick X', 'Right Stick Y'
|
||||
];
|
||||
|
||||
// Setup gamepad event listeners
|
||||
window.addEventListener("gamepadconnected", function(e) {
|
||||
gamepads[e.gamepad.index] = e.gamepad;
|
||||
@@ -129,27 +117,52 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const gamepad = navigator.getGamepads()[activeGamepadIndex];
|
||||
if (!gamepad) return;
|
||||
|
||||
const controllerType = GamepadHelper.detectControllerType(gamepad.id);
|
||||
const controllerType = gamepadHelper.detectControllerType(gamepad.id);
|
||||
|
||||
for (let i = 0; i < gamepad.buttons.length; i++) {
|
||||
const buttonName = GamepadHelper.getButtonName(controllerType, i);
|
||||
const buttonName = gamepadHelper.getButtonName(controllerType, i);
|
||||
const buttonImagePath = gamepadHelper.getButtonImagePath(
|
||||
controllerType,
|
||||
i,
|
||||
`https://cdn.jsdelivr.net/npm/@lizardbyte/gamepad-helper@${gamepadHelperVersion}/assets/img/gamepads/`,
|
||||
);
|
||||
|
||||
const buttonDiv = document.createElement('div');
|
||||
buttonDiv.className = 'circular-button';
|
||||
buttonDiv.id = `button-${i}`;
|
||||
|
||||
buttonDiv.innerHTML = `
|
||||
<span class="progress-left">
|
||||
<span class="progress-bar" id="progress-bar-left-${i}"></span>
|
||||
</span>
|
||||
<span class="progress-right">
|
||||
<span class="progress-bar" id="progress-bar-right-${i}"></span>
|
||||
</span>
|
||||
<div class="button-content">
|
||||
// Create the progress elements
|
||||
const progressLeft = document.createElement('span');
|
||||
progressLeft.className = 'progress-left';
|
||||
progressLeft.innerHTML = `<span class="progress-bar" id="progress-bar-left-${i}"></span>`;
|
||||
|
||||
const progressRight = document.createElement('span');
|
||||
progressRight.className = 'progress-right';
|
||||
progressRight.innerHTML = `<span class="progress-bar" id="progress-bar-right-${i}"></span>`;
|
||||
|
||||
// Create the button content
|
||||
const buttonContent = document.createElement('div');
|
||||
buttonContent.className = 'button-content';
|
||||
|
||||
// Add either image with fallback text, or just text
|
||||
if (buttonImagePath) {
|
||||
buttonContent.innerHTML = `
|
||||
<div class="button-image-container">
|
||||
<img src="${buttonImagePath}" alt="${buttonName}" class="button-image" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
|
||||
<div class="button-name" style="display: none;">${buttonName}</div>
|
||||
</div>
|
||||
<div class="button-value" id="button-value-${i}">0.00</div>
|
||||
`;
|
||||
} else {
|
||||
buttonContent.innerHTML = `
|
||||
<div class="button-name">${buttonName}</div>
|
||||
<div class="button-value" id="button-value-${i}">0.00</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
}
|
||||
|
||||
buttonDiv.appendChild(progressLeft);
|
||||
buttonDiv.appendChild(progressRight);
|
||||
buttonDiv.appendChild(buttonContent);
|
||||
|
||||
buttonsContainer.appendChild(buttonDiv);
|
||||
}
|
||||
@@ -165,13 +178,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const gamepad = navigator.getGamepads()[activeGamepadIndex];
|
||||
if (!gamepad) return;
|
||||
|
||||
const controllerType = GamepadHelper.detectControllerType(gamepad.id);
|
||||
const controllerType = gamepadHelper.detectControllerType(gamepad.id);
|
||||
|
||||
for (let i = 0; i < gamepad.axes.length; i++) {
|
||||
const colDiv = document.createElement('div');
|
||||
colDiv.className = 'col-md-6 col-lg-3 mb-3';
|
||||
|
||||
const axisName = GamepadHelper.getAxisName(controllerType, i);
|
||||
const axisName = gamepadHelper.getAxisName(controllerType, i);
|
||||
|
||||
colDiv.innerHTML = `
|
||||
<div class="mb-1">${axisName}: <span id="axis-value-${i}">0.00</span></div>
|
||||
@@ -201,8 +214,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Update the gamepad info function to include vibration status
|
||||
function updateGamepadInfo(gamepad) {
|
||||
const gamepadInfo = GamepadHelper.getGamepadInfo(gamepad.id);
|
||||
const vibrationCapabilities = GamepadHelper.getVibrationCapabilities(gamepad);
|
||||
const gamepadInfo = gamepadHelper.getGamepadInfo(gamepad.id);
|
||||
const vibrationCapabilities = gamepadHelper.getVibrationCapabilities(gamepad);
|
||||
|
||||
document.getElementById('gamepad-id').textContent = gamepad.id;
|
||||
document.getElementById('gamepad-index').textContent = gamepad.index;
|
||||
@@ -367,7 +380,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
return `Axis ${index}: ${axis.toFixed(2)}`;
|
||||
});
|
||||
|
||||
rawDataElement.textContent = `Gamepad: ${gamepad.id}\nType: ${GamepadHelper.detectControllerType(gamepad.id)}\n\nButtons:\n${buttons.join('\n')}\n\nAxes:\n${axes.join('\n')}`;
|
||||
rawDataElement.textContent = `Gamepad: ${gamepad.id}\nType: ${gamepadHelper.detectControllerType(gamepad.id)}\n\nButtons:\n${buttons.join('\n')}\n\nAxes:\n${axes.join('\n')}`;
|
||||
}
|
||||
|
||||
// Event listeners for vibration controls
|
||||
@@ -393,7 +406,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const gamepad = navigator.getGamepads()[activeGamepadIndex];
|
||||
if (!gamepad) return;
|
||||
|
||||
const vibrationCapabilities = GamepadHelper.getVibrationCapabilities(gamepad);
|
||||
const vibrationCapabilities = gamepadHelper.getVibrationCapabilities(gamepad);
|
||||
if (!vibrationCapabilities.supported) return;
|
||||
|
||||
const duration = parseInt(document.getElementById('vibration-duration').value);
|
||||
@@ -409,7 +422,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
vibrationOptions.magnitude = magnitude;
|
||||
}
|
||||
|
||||
GamepadHelper.vibrate(gamepad, vibrationOptions)
|
||||
gamepadHelper.vibrate(gamepad, vibrationOptions)
|
||||
.then(() => console.log('Vibration started'))
|
||||
.catch(e => console.error('Vibration error:', e));
|
||||
}
|
||||
@@ -420,7 +433,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (activeGamepadIndex !== null) {
|
||||
const gamepad = navigator.getGamepads()[activeGamepadIndex];
|
||||
if (gamepad) {
|
||||
GamepadHelper.stopVibration(gamepad).catch(e => {
|
||||
gamepadHelper.stopVibration(gamepad).catch(e => {
|
||||
console.error('Stop vibration error:', e);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1115,6 +1115,8 @@
|
||||
<a title="Privacy Policy" href="/privacy" class="text_muted">Privacy</a>
|
||||
•
|
||||
<a title="Terms and Conditions" href="/terms" class="text_muted">Terms</a>
|
||||
•
|
||||
<a title="Licenses and Attribution" href="/licenses" class="text_muted">Licenses</a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@@ -447,6 +447,8 @@
|
||||
<a title="Privacy Policy" href="/privacy" class="text_muted">Privacy</a>
|
||||
•
|
||||
<a title="Terms and Conditions" href="/terms" class="text_muted">Terms</a>
|
||||
•
|
||||
<a title="Licenses and Attribution" href="/licenses" class="text_muted">Licenses</a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user