mirror of
https://github.com/LizardByte/Sunshine.git
synced 2026-11-10 20:51:06 +08:00
refactor(web): sonar fixes (#5740)
This commit is contained in:
1 parent
7c8926d701
commit
e9af4a1aa4
34 files changed
+568
-239
No files matched your search
+13
-5
@@ -6,7 +6,7 @@ function generateExamples(endpoint, method, body = null) {
|
||||
let psBodyParams = '';
|
||||
|
||||
if (body) {
|
||||
const curlJsonString = JSON.stringify(body).replace(/"/g, '\\"');
|
||||
const curlJsonString = JSON.stringify(body).replaceAll('"', String.raw`\"`);
|
||||
curlBodyString = ` -d "${curlJsonString}"`;
|
||||
curlHeaderString = ' -H "Content-Type: application/json"';
|
||||
psBodyString = `-Body (ConvertTo-Json ${JSON.stringify(body)})`;
|
||||
@@ -43,12 +43,20 @@ requests.${method.trim().toLowerCase()}(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create a stable signed 32-bit hash for a string.
|
||||
*
|
||||
* @param {string} str String to hash.
|
||||
* @return {number} Signed 32-bit hash value.
|
||||
*/
|
||||
function hashString(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
const signedHash = new Int32Array(1);
|
||||
for (const character of str) {
|
||||
const codePoint = character.codePointAt(0);
|
||||
hash = hash * 31 + codePoint;
|
||||
signedHash[0] = hash;
|
||||
hash = signedHash[0];
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
}
|
||||
|
||||
let previousElement = table.previousElementSibling;
|
||||
while (previousElement && previousElement.tagName !== "H2") {
|
||||
while (previousElement?.tagName && previousElement.tagName !== "H2") {
|
||||
previousElement = previousElement.previousElementSibling;
|
||||
}
|
||||
if (previousElement && previousElement.textContent) {
|
||||
if (previousElement?.textContent) {
|
||||
const sectionId = previousElement.textContent.trim().toLowerCase();
|
||||
const newRow = document.createElement("tr");
|
||||
|
||||
|
||||
@@ -82,6 +82,20 @@ cover-img:
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% capture github_release_package_icons %}
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/icons/windows.svg" alt="Windows"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/macos.svg" alt="macOS"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/freebsd.svg" alt="FreeBSD"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/appimage.svg" alt="AppImage"/> <!-- //NOSONAR(Web:S6851) -->
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/alpinelinux.svg" alt="Alpine Linux"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/archlinux.svg" alt="Arch Linux"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/debian.svg" alt="Debian"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/fedora.svg" alt="Fedora"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/opensuse.svg" alt="openSUSE"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/ubuntu.svg" alt="Ubuntu"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/flatpak.svg" alt="Flatpak"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/homebrew.svg" alt="Homebrew"/>
|
||||
{% endcapture %}
|
||||
<div class="card-footer p-3 px-4">
|
||||
<a class="btn btn-outline-theme me-3 mb-3" href="https://docs.lizardbyte.dev/projects/sunshine" target="_blank">
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/readthedocs.svg" alt="ReadTheDocs"/>
|
||||
@@ -107,22 +121,45 @@ cover-img:
|
||||
</div>
|
||||
<div class="card-footer p-3 px-4">
|
||||
<a class="latest-button btn btn-outline-theme me-3 mb-3 d-none" href="https://github.com/LizardByte/Sunshine/releases/latest" target="_blank">
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/github.svg" alt="GitHub"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/icons/windows.svg" alt="Windows"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/debian.svg" alt="Debian"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/ubuntu.svg" alt="Ubuntu"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/flatpak.svg" alt="Flatpak"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/linux.svg" alt="AppImage"/>
|
||||
<span class="d-flex align-items-center justify-content-center gap-1">
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/github.svg" alt="GitHub"/>
|
||||
Latest: <span id="latest-version" class="crowdin-ignore"></span>
|
||||
</span>
|
||||
<span class="d-flex align-items-center justify-content-center gap-1 mt-1">
|
||||
{{ github_release_package_icons }}
|
||||
</span>
|
||||
</a>
|
||||
<a class="beta-button btn btn-outline-theme me-3 mb-3 d-none" href="#" target="_blank">
|
||||
<i class="fa-fw fa-lg fas fa-flask"></i>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/icons/windows.svg" alt="Windows"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/debian.svg" alt="Debian"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/ubuntu.svg" alt="Ubuntu"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/flatpak.svg" alt="Flatpak"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/linux.svg" alt="AppImage"/>
|
||||
<span class="d-flex align-items-center justify-content-center gap-1">
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/github.svg" alt="GitHub"/>
|
||||
<i class="fa-fw fa-lg fas fa-flask"></i>
|
||||
Beta: <span id="beta-version" class="crowdin-ignore"></span>
|
||||
</span>
|
||||
<span class="d-flex align-items-center justify-content-center gap-1 mt-1">
|
||||
{{ github_release_package_icons }}
|
||||
</span>
|
||||
</a>
|
||||
<a class="btn btn-outline-theme me-3 mb-3" href="https://cloudsmith.io/~lizardbyte/repos/" target="_blank">
|
||||
<span class="d-flex align-items-center justify-content-center gap-1">
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/cloudsmith.svg" alt="Cloudsmith"/>
|
||||
Cloudsmith
|
||||
</span>
|
||||
<span class="d-flex align-items-center justify-content-center gap-1 mt-1">
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/alpinelinux.svg" alt="Alpine Linux"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/debian.svg" alt="Debian"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/fedora.svg" alt="Fedora"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/opensuse.svg" alt="openSUSE"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/ubuntu.svg" alt="Ubuntu"/>
|
||||
</span>
|
||||
</a>
|
||||
<a class="btn btn-outline-theme me-3 mb-3" href="https://copr.fedorainfracloud.org/coprs/lizardbyte/" target="_blank">
|
||||
<span class="d-flex align-items-center justify-content-center">
|
||||
Copr
|
||||
</span>
|
||||
<span class="d-flex align-items-center justify-content-center gap-1 mt-1">
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/fedora.svg" alt="Fedora"/>
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/opensuse.svg" alt="openSUSE"/>
|
||||
</span>
|
||||
</a>
|
||||
<a class="btn btn-outline-theme me-3 mb-3" href="https://github.com/LizardByte/pacman-repo" target="_blank">
|
||||
<img class="icon-sm invert" src="https://cdn.jsdelivr.net/npm/simple-icons@16/icons/archlinux.svg" alt="Arch Linux"/>
|
||||
@@ -146,63 +183,24 @@ cover-img:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TODO: Move this to website repo, and make it accept arguments for the repo name -->
|
||||
<script>
|
||||
// Fetch the releases from the GitHub API
|
||||
fetch('https://api.github.com/repos/LizardByte/Sunshine/releases')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Filter the releases to get only the pre-releases
|
||||
const preReleases = data.filter(release => release.prerelease);
|
||||
// Filter the releases to get only the stable releases
|
||||
const stableReleases = data.filter(release => !release.prerelease);
|
||||
|
||||
const latestButton = document.querySelector('.latest-button');
|
||||
const latestVersion = document.querySelector('#latest-version');
|
||||
const betaButton = document.querySelector('.beta-button');
|
||||
const betaVersion = document.querySelector('#beta-version');
|
||||
|
||||
// If there are no stable releases, hide the latest download button
|
||||
if (stableReleases.length === 0) {
|
||||
latestButton.classList.add('d-none');
|
||||
} else {
|
||||
// Show the latest download button
|
||||
latestButton.classList.remove('d-none');
|
||||
|
||||
// Get the latest stable release
|
||||
const latestStableRelease = stableReleases[0];
|
||||
latestVersion.textContent = latestStableRelease.tag_name;
|
||||
|
||||
// If there is a pre-release, update the href attribute of the anchor tag
|
||||
if (preReleases.length > 0) {
|
||||
const latestPreRelease = preReleases[0];
|
||||
|
||||
// Compare the date of the latest pre-release with the date of the latest stable release
|
||||
const preReleaseDate = new Date(latestPreRelease.published_at);
|
||||
const stableReleaseDate = new Date(latestStableRelease.published_at);
|
||||
|
||||
// If the pre-release is newer, update the href attribute of the anchor tag
|
||||
if (preReleaseDate > stableReleaseDate) {
|
||||
betaButton.href = latestPreRelease.html_url;
|
||||
betaVersion.textContent = latestPreRelease.tag_name;
|
||||
betaButton.classList.remove('d-none');
|
||||
} else {
|
||||
// If the pre-release is older, hide the button
|
||||
betaButton.classList.add('d-none');
|
||||
}
|
||||
} else {
|
||||
// If there is no pre-release, hide the button
|
||||
betaButton.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
<script src="{{ '/Sunshine/assets/js/release-versions.js' | relative_url }}"></script>
|
||||
<script type="module">
|
||||
try {
|
||||
await globalThis.updateGitHubReleaseVersions({
|
||||
repository: 'LizardByte/Sunshine',
|
||||
stableButtonSelector: '.latest-button',
|
||||
stableVersionSelector: '#latest-version',
|
||||
prereleaseButtonSelector: '.beta-button',
|
||||
prereleaseVersionSelector: '#beta-version',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update release versions', error);
|
||||
}
|
||||
|
||||
// Fetch clients from app directory
|
||||
fetch('https://app.lizardbyte.dev/app-directory/sunshine.json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const clientsContainer = document.getElementById('clients-container');
|
||||
if (!clientsContainer) return;
|
||||
const clientsContainer = document.getElementById('clients-container');
|
||||
if (clientsContainer) {
|
||||
const clientsResponse = await fetch('https://app.lizardbyte.dev/app-directory/sunshine.json');
|
||||
const data = await clientsResponse.json();
|
||||
|
||||
const apps = data.apps || [];
|
||||
|
||||
@@ -302,5 +300,5 @@ cover-img:
|
||||
`;
|
||||
clientsContainer.innerHTML += clientHtml;
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,8 @@
|
||||
<script setup>
|
||||
const model = defineModel({ required: true });
|
||||
const model = defineModel({
|
||||
type: [Boolean, Number, String],
|
||||
required: true,
|
||||
});
|
||||
const slots = defineSlots();
|
||||
const props = defineProps({
|
||||
class: {
|
||||
|
||||
@@ -24,8 +24,3 @@ const props = defineProps({
|
||||
<slot name="macos"></slot>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -14,8 +14,7 @@ import { GitHubIcon, DiscordIcon } from 'vue3-simple-icons'
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: 'GitHub'
|
||||
required: true
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
|
||||
@@ -152,6 +152,3 @@ onMounted(() => {
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -2,11 +2,11 @@
|
||||
import { ref } from 'vue'
|
||||
import PlatformLayout from '../../PlatformLayout.vue'
|
||||
|
||||
const props = defineProps([
|
||||
'platform',
|
||||
'config',
|
||||
'global_prep_cmd'
|
||||
])
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
global_prep_cmd: Array,
|
||||
})
|
||||
|
||||
const config = ref(props.config)
|
||||
</script>
|
||||
@@ -117,7 +117,3 @@ const config = ref(props.config)
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -8,10 +8,10 @@ import DisplayDeviceOptions from "./audiovideo/DisplayDeviceOptions.vue";
|
||||
import DisplayModesSettings from "./audiovideo/DisplayModesSettings.vue";
|
||||
import Checkbox from "../../Checkbox.vue";
|
||||
|
||||
const props = defineProps([
|
||||
'platform',
|
||||
'config',
|
||||
])
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const config = ref(props.config)
|
||||
</script>
|
||||
@@ -98,6 +98,3 @@ const config = ref(props.config)
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -8,11 +8,11 @@ import SoftwareEncoder from './encoders/SoftwareEncoder.vue'
|
||||
import VAAPIEncoder from './encoders/VAAPIEncoder.vue'
|
||||
import VulkanEncoder from './encoders/VulkanEncoder.vue'
|
||||
|
||||
const props = defineProps([
|
||||
'platform',
|
||||
'config',
|
||||
'currentTab'
|
||||
])
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
currentTab: String,
|
||||
})
|
||||
|
||||
const config = ref(props.config)
|
||||
</script>
|
||||
@@ -69,6 +69,3 @@ const config = ref(props.config)
|
||||
/>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps([
|
||||
'platform',
|
||||
'config'
|
||||
])
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const config = ref(props.config)
|
||||
</script>
|
||||
@@ -56,7 +56,3 @@ const config = ref(props.config)
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -89,7 +89,7 @@ function removeCmd(index) {
|
||||
|
||||
<!-- Global Prep Commands -->
|
||||
<div id="global_prep_cmd" class="mb-3 d-flex flex-column">
|
||||
<label class="form-label">{{ $t('config.global_prep_cmd') }}</label>
|
||||
<div class="form-label">{{ $t('config.global_prep_cmd') }}</div>
|
||||
<div class="form-text">{{ $t('config.global_prep_cmd_desc') }}</div>
|
||||
<table class="table" v-if="config.global_prep_cmd.length > 0">
|
||||
<thead>
|
||||
@@ -103,12 +103,14 @@ function removeCmd(index) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(c, i) in config.global_prep_cmd">
|
||||
<tr v-for="(c, i) in config.global_prep_cmd" :key="i">
|
||||
<td>
|
||||
<input type="text" class="form-control monospace" v-model="c.do" />
|
||||
<label :for="`prep-cmd-do-${i}`" class="visually-hidden">{{ $t('_common.do_cmd') }}</label>
|
||||
<input :id="`prep-cmd-do-${i}`" type="text" class="form-control monospace" v-model="c.do" />
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control monospace" v-model="c.undo" />
|
||||
<label :for="`prep-cmd-undo-${i}`" class="visually-hidden">{{ $t('_common.undo_cmd') }}</label>
|
||||
<input :id="`prep-cmd-undo-${i}`" type="text" class="form-control monospace" v-model="c.undo" />
|
||||
</td>
|
||||
<td v-if="platform === 'windows'" class="align-middle">
|
||||
<Checkbox :id="'prep-cmd-admin-' + i"
|
||||
@@ -150,7 +152,3 @@ function removeCmd(index) {
|
||||
></Checkbox>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -65,10 +65,10 @@ function serializeKeybindings(pairs) {
|
||||
return `[${values.join(',')}]`
|
||||
}
|
||||
|
||||
const props = defineProps([
|
||||
'platform',
|
||||
'config'
|
||||
])
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const config = ref(props.config)
|
||||
const keybindingPairs = ref(parseKeybindings(config.value.keybindings))
|
||||
|
||||
@@ -6,15 +6,15 @@ import {
|
||||
} from '@lucide/vue'
|
||||
import Checkbox from "../../Checkbox.vue";
|
||||
|
||||
const props = defineProps([
|
||||
'platform',
|
||||
'config'
|
||||
])
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const defaultMoonlightPort = 47989
|
||||
|
||||
const config = ref(props.config)
|
||||
const effectivePort = computed(() => +config.value?.port ?? defaultMoonlightPort)
|
||||
const effectivePort = computed(() => Number(config.value?.port ?? defaultMoonlightPort))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -102,12 +102,6 @@ const effectivePort = computed(() => +config.value?.port ?? defaultMoonlightPort
|
||||
<td>{{+effectivePort + 9}} - {{+effectivePort + 11}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<!-- <tr>-->
|
||||
<!-- <!– Mic –>-->
|
||||
<!-- <td>UDP</td>-->
|
||||
<!-- <td>{{+effectivePort + 13}}</td>-->
|
||||
<!-- <td></td>-->
|
||||
<!-- </tr>-->
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- add warning about exposing web ui to the internet -->
|
||||
@@ -182,7 +176,3 @@ const effectivePort = computed(() => +config.value?.port ?? defaultMoonlightPort
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -3,10 +3,10 @@ import { ref } from 'vue'
|
||||
import { $tp } from '../../../platform-i18n'
|
||||
import PlatformLayout from '../../../PlatformLayout.vue'
|
||||
|
||||
const props = defineProps([
|
||||
'platform',
|
||||
'config'
|
||||
])
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const config = ref(props.config)
|
||||
</script>
|
||||
|
||||
@@ -94,9 +94,9 @@ function addRemappingEntry() {
|
||||
|
||||
<!-- Manual resolution -->
|
||||
<div class="mt-2 ps-4" v-if="config.dd_resolution_option === 'manual'">
|
||||
<div class="form-text">
|
||||
<label for="dd_manual_resolution" class="form-label">
|
||||
{{ $t('config.dd_manual_resolution') }}
|
||||
</div>
|
||||
</label>
|
||||
<input type="text" class="form-control" id="dd_manual_resolution" placeholder="2560x1440"
|
||||
v-model="config.dd_manual_resolution" />
|
||||
</div>
|
||||
@@ -115,9 +115,9 @@ function addRemappingEntry() {
|
||||
|
||||
<!-- Manual refresh rate -->
|
||||
<div class="mt-2 ps-4" v-if="config.dd_refresh_rate_option === 'manual'">
|
||||
<div class="form-text">
|
||||
<label for="dd_manual_refresh_rate" class="form-label">
|
||||
{{ $t('config.dd_manual_refresh_rate') }}
|
||||
</div>
|
||||
</label>
|
||||
<input type="text" class="form-control" id="dd_manual_refresh_rate" placeholder="59.9558"
|
||||
v-model="config.dd_manual_refresh_rate" />
|
||||
</div>
|
||||
@@ -208,21 +208,33 @@ function addRemappingEntry() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(value, idx) in config.dd_mode_remapping[getRemappingType()]">
|
||||
<tr v-for="(value, idx) in config.dd_mode_remapping[getRemappingType()]" :key="idx">
|
||||
<td v-if="getRemappingType() !== REFRESH_RATE_ONLY">
|
||||
<input type="text" class="form-control monospace" v-model="value.requested_resolution"
|
||||
<label :for="`dd-requested-resolution-${idx}`" class="visually-hidden">
|
||||
{{ $t('config.dd_mode_remapping_requested_resolution') }}
|
||||
</label>
|
||||
<input :id="`dd-requested-resolution-${idx}`" type="text" class="form-control monospace" v-model="value.requested_resolution"
|
||||
:placeholder="'1920x1080'" />
|
||||
</td>
|
||||
<td v-if="getRemappingType() !== RESOLUTION_ONLY">
|
||||
<input type="text" class="form-control monospace" v-model="value.requested_fps"
|
||||
<label :for="`dd-requested-fps-${idx}`" class="visually-hidden">
|
||||
{{ $t('config.dd_mode_remapping_requested_fps') }}
|
||||
</label>
|
||||
<input :id="`dd-requested-fps-${idx}`" type="text" class="form-control monospace" v-model="value.requested_fps"
|
||||
:placeholder="'60'" />
|
||||
</td>
|
||||
<td v-if="getRemappingType() !== REFRESH_RATE_ONLY">
|
||||
<input type="text" class="form-control monospace" v-model="value.final_resolution"
|
||||
<label :for="`dd-final-resolution-${idx}`" class="visually-hidden">
|
||||
{{ $t('config.dd_mode_remapping_final_resolution') }}
|
||||
</label>
|
||||
<input :id="`dd-final-resolution-${idx}`" type="text" class="form-control monospace" v-model="value.final_resolution"
|
||||
:placeholder="'2560x1440'" />
|
||||
</td>
|
||||
<td v-if="getRemappingType() !== RESOLUTION_ONLY">
|
||||
<input type="text" class="form-control monospace" v-model="value.final_refresh_rate"
|
||||
<label :for="`dd-final-refresh-rate-${idx}`" class="visually-hidden">
|
||||
{{ $t('config.dd_mode_remapping_final_refresh_rate') }}
|
||||
</label>
|
||||
<input :id="`dd-final-refresh-rate-${idx}`" type="text" class="form-control monospace" v-model="value.final_refresh_rate"
|
||||
:placeholder="'119.95'" />
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { $tp } from '../../../platform-i18n'
|
||||
import PlatformLayout from '../../../PlatformLayout.vue'
|
||||
|
||||
const props = defineProps([
|
||||
'platform',
|
||||
'config',
|
||||
])
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
})
|
||||
const config = ref(props.config)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import { ref } from 'vue'
|
||||
import { $tp } from '../../../platform-i18n'
|
||||
import PlatformLayout from '../../../PlatformLayout.vue'
|
||||
|
||||
const props = defineProps([
|
||||
'platform',
|
||||
'config'
|
||||
])
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const config = ref(props.config)
|
||||
let _outputNamePlaceholder = '0';
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import { ref } from 'vue'
|
||||
import Checkbox from "../../../Checkbox.vue";
|
||||
|
||||
const props = defineProps([
|
||||
'platform',
|
||||
'config',
|
||||
])
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const config = ref(props.config)
|
||||
</script>
|
||||
@@ -124,7 +124,3 @@ const config = ref(props.config)
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -2,10 +2,10 @@
|
||||
import { ref } from 'vue'
|
||||
import Checkbox from "../../../Checkbox.vue";
|
||||
|
||||
const props = defineProps([
|
||||
'platform',
|
||||
'config',
|
||||
])
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const config = ref(props.config)
|
||||
</script>
|
||||
@@ -45,7 +45,3 @@ const config = ref(props.config)
|
||||
></Checkbox>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -2,10 +2,10 @@
|
||||
import { ref } from 'vue'
|
||||
import Checkbox from "../../../Checkbox.vue";
|
||||
|
||||
const props = defineProps([
|
||||
'platform',
|
||||
'config',
|
||||
])
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const config = ref(props.config)
|
||||
</script>
|
||||
@@ -125,7 +125,3 @@ const config = ref(props.config)
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps([
|
||||
'platform',
|
||||
'config'
|
||||
])
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const config = ref(props.config)
|
||||
</script>
|
||||
@@ -41,7 +41,3 @@ const config = ref(props.config)
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -2,10 +2,10 @@
|
||||
import { ref } from 'vue'
|
||||
import Checkbox from "../../../Checkbox.vue";
|
||||
|
||||
const props = defineProps([
|
||||
'platform',
|
||||
'config',
|
||||
])
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const config = ref(props.config)
|
||||
</script>
|
||||
@@ -88,7 +88,3 @@ const config = ref(props.config)
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -2,10 +2,10 @@
|
||||
import { ref } from 'vue'
|
||||
import Checkbox from "../../../Checkbox.vue";
|
||||
|
||||
const props = defineProps([
|
||||
'platform',
|
||||
'config',
|
||||
])
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const config = ref(props.config)
|
||||
</script>
|
||||
@@ -39,7 +39,3 @@ const config = ref(props.config)
|
||||
></Checkbox>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps([
|
||||
'platform',
|
||||
'config',
|
||||
])
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const config = ref(props.config)
|
||||
</script>
|
||||
|
||||
@@ -3,21 +3,84 @@ import {createI18n} from "vue-i18n";
|
||||
// Import only the fallback language files
|
||||
import en from './public/assets/locale/en.json'
|
||||
|
||||
export default async function() {
|
||||
let r = await (await fetch("./api/configLocale")).json();
|
||||
let locale = r.locale ?? "en";
|
||||
document.querySelector('html').setAttribute('lang', locale);
|
||||
let messages = {
|
||||
en
|
||||
};
|
||||
const localePaths = new Map([
|
||||
['bg', './assets/locale/bg.json'],
|
||||
['cs', './assets/locale/cs.json'],
|
||||
['de', './assets/locale/de.json'],
|
||||
['en_GB', './assets/locale/en_GB.json'],
|
||||
['en_US', './assets/locale/en_US.json'],
|
||||
['es', './assets/locale/es.json'],
|
||||
['fr', './assets/locale/fr.json'],
|
||||
['hu', './assets/locale/hu.json'],
|
||||
['it', './assets/locale/it.json'],
|
||||
['ja', './assets/locale/ja.json'],
|
||||
['ko', './assets/locale/ko.json'],
|
||||
['pl', './assets/locale/pl.json'],
|
||||
['pt', './assets/locale/pt.json'],
|
||||
['pt_BR', './assets/locale/pt_BR.json'],
|
||||
['ru', './assets/locale/ru.json'],
|
||||
['sv', './assets/locale/sv.json'],
|
||||
['tr', './assets/locale/tr.json'],
|
||||
['uk', './assets/locale/uk.json'],
|
||||
['vi', './assets/locale/vi.json'],
|
||||
['zh', './assets/locale/zh.json'],
|
||||
['zh_TW', './assets/locale/zh_TW.json'],
|
||||
]);
|
||||
|
||||
/**
|
||||
* @brief Resolve a configured locale to a bundled translation file.
|
||||
*
|
||||
* @param {unknown} configuredLocale Locale returned by the configuration API.
|
||||
* @return {string} Supported locale identifier or the English fallback.
|
||||
*/
|
||||
function resolveLocale(configuredLocale) {
|
||||
if (typeof configuredLocale !== 'string') {
|
||||
return 'en';
|
||||
}
|
||||
|
||||
return configuredLocale === 'en' || localePaths.has(configuredLocale) ? configuredLocale : 'en';
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Load messages for a supported locale with an English fallback.
|
||||
*
|
||||
* @param {string} locale Supported locale identifier.
|
||||
* @param {Function} loadTranslation Function that loads a translation path.
|
||||
* @return {Promise<{locale: string, messages: object}>} Loaded locale and messages.
|
||||
*/
|
||||
export async function loadLocaleMessages(
|
||||
locale,
|
||||
loadTranslation = async path => (await fetch(path)).json(),
|
||||
) {
|
||||
let messages = { en };
|
||||
|
||||
try {
|
||||
if (locale !== 'en') {
|
||||
let r = await (await fetch(`./assets/locale/${locale}.json`)).json();
|
||||
messages[locale] = r;
|
||||
const translation = await loadTranslation(localePaths.get(locale));
|
||||
messages = Object.fromEntries([
|
||||
['en', en],
|
||||
[locale, translation],
|
||||
]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to download translations", e);
|
||||
locale = 'en';
|
||||
}
|
||||
|
||||
return { locale, messages };
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create the Vue internationalization instance for the configured locale.
|
||||
*
|
||||
* @return {Promise<import('vue-i18n').I18n>} Configured internationalization instance.
|
||||
*/
|
||||
export default async function createSunshineI18n() {
|
||||
const localeConfig = await (await fetch("./api/configLocale")).json();
|
||||
const configuredLocale = resolveLocale(localeConfig.locale);
|
||||
const { locale, messages } = await loadLocaleMessages(configuredLocale);
|
||||
|
||||
document.documentElement.setAttribute('lang', locale);
|
||||
const i18n = createI18n({
|
||||
locale: locale, // set locale
|
||||
fallbackLocale: 'en', // set fallback locale
|
||||
|
||||
@@ -31,12 +31,12 @@ class PlatformMessageI18n {
|
||||
// We got a message back, return early
|
||||
return message
|
||||
}
|
||||
|
||||
|
||||
// If on Windows, we don't fallback to unix, so return early
|
||||
if (this.platform === 'windows') {
|
||||
return defaultMsg ? defaultMsg : message
|
||||
return defaultMsg || message
|
||||
}
|
||||
|
||||
|
||||
// there's no message for key, check for unix version
|
||||
const unixKey = this.getPlatformKey(key, 'unix')
|
||||
message = i18n.t(unixKey)
|
||||
@@ -59,7 +59,7 @@ export function usePlatformI18n(platform) {
|
||||
}
|
||||
|
||||
if (!platform) {
|
||||
throw 'platform argument missing'
|
||||
throw new TypeError('platform argument missing')
|
||||
}
|
||||
|
||||
return inject(
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
/**
|
||||
* @brief Parsed Sunshine release version and comparison helpers.
|
||||
*/
|
||||
class SunshineVersion {
|
||||
/**
|
||||
* @brief Construct a version from GitHub release data or a version string.
|
||||
*
|
||||
* @param {?object} release GitHub release data.
|
||||
* @param {?string} version Version string when release data is unavailable.
|
||||
*/
|
||||
constructor(release = null, version = null) {
|
||||
if (release) {
|
||||
this.release = release;
|
||||
@@ -19,17 +28,29 @@ class SunshineVersion {
|
||||
this.versionPatch = this.versionParts ? this.versionParts[2] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Split a Sunshine version string into numeric components.
|
||||
*
|
||||
* @param {string} version Version string to parse.
|
||||
* @return {?number[]} Numeric version components.
|
||||
*/
|
||||
parseVersion(version) {
|
||||
if (!version) {
|
||||
return null;
|
||||
}
|
||||
let v = version;
|
||||
if (v.indexOf("v") === 0) {
|
||||
if (v.startsWith("v")) {
|
||||
v = v.substring(1);
|
||||
}
|
||||
return v.split('.').map(Number);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Compare this version with another Sunshine version.
|
||||
*
|
||||
* @param {SunshineVersion|string} otherVersion Version to compare.
|
||||
* @return {boolean} Whether this version is greater.
|
||||
*/
|
||||
isGreater(otherVersion) {
|
||||
let otherVersionParts;
|
||||
if (otherVersion instanceof SunshineVersion) {
|
||||
@@ -37,7 +58,7 @@ class SunshineVersion {
|
||||
} else if (typeof otherVersion === 'string') {
|
||||
otherVersionParts = this.parseVersion(otherVersion);
|
||||
} else {
|
||||
throw new Error('Invalid argument: otherVersion must be a SunshineVersion object or a version string');
|
||||
throw new TypeError('Invalid argument: otherVersion must be a SunshineVersion object or a version string');
|
||||
}
|
||||
|
||||
if (!this.versionParts || !otherVersionParts) {
|
||||
|
||||
@@ -198,6 +198,7 @@ set(INTEGRATION_TEST_FILES
|
||||
"scripts/linux_build.sh"
|
||||
"src/config.cpp"
|
||||
"src_assets/common/assets/web/Config.vue"
|
||||
"src_assets/common/assets/web/locale.js"
|
||||
"docs/building.md"
|
||||
"docs/configuration.md"
|
||||
"docs/getting_started.md"
|
||||
|
||||
@@ -75,6 +75,26 @@ protected:
|
||||
return locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Extract locale codes and referenced file names from the web locale loader.
|
||||
*
|
||||
* @return Map of configured locale codes to referenced JSON file stems.
|
||||
*/
|
||||
static std::map<std::string, std::string, std::less<>> extractLocaleJsPaths() {
|
||||
std::map<std::string, std::string, std::less<>> locales;
|
||||
const std::string content = file_handler::read_file("src_assets/common/assets/web/locale.js");
|
||||
const std::regex localePathPattern(
|
||||
R"delimiter(\[\s*['"]([^'"]+)['"]\s*,\s*['"]\./assets/locale/([^'"]+)\.json['"]\s*\])delimiter"
|
||||
);
|
||||
std::sregex_iterator iter(content.begin(), content.end(), localePathPattern);
|
||||
|
||||
for (const std::sregex_iterator end; iter != end; ++iter) {
|
||||
locales[(*iter)[1].str()] = (*iter)[2].str();
|
||||
}
|
||||
|
||||
return locales;
|
||||
}
|
||||
|
||||
// Get available locale JSON files
|
||||
static std::set<std::string, std::less<>> getAvailableLocaleFiles() {
|
||||
std::set<std::string, std::less<>> locales;
|
||||
@@ -246,6 +266,42 @@ TEST_F(LocaleConsistencyTest, ConfigCppAndGeneralVueLocalesMatch) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LocaleConsistencyTest, LocaleJsPathsMatchAvailableFiles) {
|
||||
const auto localePaths = extractLocaleJsPaths();
|
||||
auto localeFiles = getAvailableLocaleFiles();
|
||||
localeFiles.erase("en"); // English is imported directly as the fallback locale.
|
||||
|
||||
std::vector<std::string> inconsistencies;
|
||||
|
||||
for (const auto &localeFile : localeFiles) {
|
||||
if (!localePaths.contains(localeFile)) {
|
||||
inconsistencies.push_back(std::format("{}.json is missing from locale.js", localeFile));
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &[localeCode, referencedFile] : localePaths) {
|
||||
if (localeCode != referencedFile) {
|
||||
inconsistencies.push_back(
|
||||
std::format("locale.js maps '{}' to '{}.json'", localeCode, referencedFile)
|
||||
);
|
||||
}
|
||||
|
||||
if (!localeFiles.contains(referencedFile)) {
|
||||
inconsistencies.push_back(
|
||||
std::format("locale.js references missing file: {}.json", referencedFile)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!inconsistencies.empty()) {
|
||||
std::string errorMsg = "locale.js path inconsistencies found:\n";
|
||||
for (const auto &inconsistency : inconsistencies) {
|
||||
errorMsg += std::format(" {}\n", inconsistency);
|
||||
}
|
||||
FAIL() << errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LocaleConsistencyTest, AllLocaleFilesAreValid) {
|
||||
const auto localeFiles = getAvailableLocaleFiles();
|
||||
std::vector<std::string> invalidFiles;
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import DisplayDeviceOptions from '../../src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue'
|
||||
import General from '../../src_assets/common/assets/web/configs/tabs/General.vue'
|
||||
import Network from '../../src_assets/common/assets/web/configs/tabs/Network.vue'
|
||||
|
||||
const globalOptions = {
|
||||
mocks: {
|
||||
$t: key => key,
|
||||
},
|
||||
stubs: {
|
||||
Checkbox: true,
|
||||
Info: true,
|
||||
PlatformLayout: {
|
||||
template: '<div><slot name="windows" /></div>',
|
||||
},
|
||||
Play: true,
|
||||
Plus: true,
|
||||
Shield: true,
|
||||
Trash2: true,
|
||||
TriangleAlert: true,
|
||||
Undo: true,
|
||||
},
|
||||
}
|
||||
|
||||
describe('configuration accessibility', () => {
|
||||
it('gives every global prep command input an accessible label', () => {
|
||||
const wrapper = mount(General, {
|
||||
props: {
|
||||
platform: 'windows',
|
||||
config: {
|
||||
global_prep_cmd: [{ do: 'start', elevated: false, undo: 'stop' }],
|
||||
notify_pre_releases: false,
|
||||
system_tray: true,
|
||||
},
|
||||
},
|
||||
global: globalOptions,
|
||||
})
|
||||
|
||||
for (const input of wrapper.findAll('#global_prep_cmd tbody input')) {
|
||||
expect(wrapper.get(`label[for="${input.attributes('id')}"]`).classes()).toContain('visually-hidden')
|
||||
}
|
||||
})
|
||||
|
||||
it('associates labels with manual display settings and remapping inputs', () => {
|
||||
const manualWrapper = mount(DisplayDeviceOptions, {
|
||||
props: {
|
||||
platform: 'windows',
|
||||
config: {
|
||||
dd_configuration_option: 'ensure_active',
|
||||
dd_config_revert_on_disconnect: false,
|
||||
dd_hdr_option: 'disabled',
|
||||
dd_mode_remapping: {
|
||||
mixed: [],
|
||||
refresh_rate_only: [],
|
||||
resolution_only: [],
|
||||
},
|
||||
dd_refresh_rate_option: 'manual',
|
||||
dd_resolution_option: 'manual',
|
||||
},
|
||||
},
|
||||
global: globalOptions,
|
||||
})
|
||||
|
||||
expect(manualWrapper.get('label[for="dd_manual_resolution"]')).toBeDefined()
|
||||
expect(manualWrapper.get('label[for="dd_manual_refresh_rate"]')).toBeDefined()
|
||||
|
||||
const remappingWrapper = mount(DisplayDeviceOptions, {
|
||||
props: {
|
||||
platform: 'windows',
|
||||
config: {
|
||||
dd_configuration_option: 'ensure_active',
|
||||
dd_config_revert_on_disconnect: false,
|
||||
dd_hdr_option: 'disabled',
|
||||
dd_mode_remapping: {
|
||||
mixed: [{
|
||||
final_refresh_rate: '',
|
||||
final_resolution: '',
|
||||
requested_fps: '',
|
||||
requested_resolution: '',
|
||||
}],
|
||||
refresh_rate_only: [],
|
||||
resolution_only: [],
|
||||
},
|
||||
dd_refresh_rate_option: 'auto',
|
||||
dd_resolution_option: 'auto',
|
||||
},
|
||||
},
|
||||
global: globalOptions,
|
||||
})
|
||||
|
||||
for (const input of remappingWrapper.findAll('tbody input')) {
|
||||
expect(remappingWrapper.get(`label[for="${input.attributes('id')}"]`).classes()).toContain('visually-hidden')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('network configuration', () => {
|
||||
it('uses the default Moonlight port when the configured port is absent', () => {
|
||||
const wrapper = mount(Network, {
|
||||
props: {
|
||||
platform: 'linux',
|
||||
config: { upnp: false },
|
||||
},
|
||||
global: globalOptions,
|
||||
})
|
||||
|
||||
const tableText = wrapper.get('table').text()
|
||||
expect(tableText).toContain('47984')
|
||||
expect(tableText).toContain('47989')
|
||||
expect(tableText).toContain('48010')
|
||||
expect(tableText).not.toContain('NaN')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
createI18n: vi.fn(options => options),
|
||||
}))
|
||||
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import createSunshineI18n, { loadLocaleMessages } from '../../src_assets/common/assets/web/locale.js'
|
||||
|
||||
describe('locale initialization', () => {
|
||||
beforeEach(() => {
|
||||
createI18n.mockClear()
|
||||
document.documentElement.removeAttribute('lang')
|
||||
})
|
||||
|
||||
it('loads a bundled configured locale', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn()
|
||||
.mockResolvedValueOnce({ json: async () => ({ locale: 'de' }) })
|
||||
.mockResolvedValueOnce({ json: async () => ({ greeting: 'Hallo' }) }))
|
||||
|
||||
const i18n = await createSunshineI18n()
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('./api/configLocale')
|
||||
expect(fetch).toHaveBeenCalledWith('./assets/locale/de.json')
|
||||
expect(document.documentElement.lang).toBe('de')
|
||||
expect(i18n.locale).toBe('de')
|
||||
expect(i18n.fallbackLocale).toBe('en')
|
||||
expect(i18n.messages.en).toBeDefined()
|
||||
expect(i18n.messages.de).toEqual({ greeting: 'Hallo' })
|
||||
})
|
||||
|
||||
it.each([
|
||||
['an unsupported locale', 'unsupported'],
|
||||
['a prototype property', '__proto__'],
|
||||
['a non-string value', null],
|
||||
])('falls back to English for %s', async (_description, locale) => {
|
||||
vi.stubGlobal('fetch', vi.fn(async () => ({
|
||||
json: async () => ({ locale }),
|
||||
})))
|
||||
|
||||
const i18n = await createSunshineI18n()
|
||||
|
||||
expect(document.documentElement.lang).toBe('en')
|
||||
expect(i18n.locale).toBe('en')
|
||||
expect(Object.keys(i18n.messages)).toEqual(['en'])
|
||||
})
|
||||
|
||||
it('falls back to English when a bundled translation fails to load', async () => {
|
||||
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const loadTranslation = async () => {
|
||||
throw new Error('translation unavailable')
|
||||
}
|
||||
|
||||
const result = await loadLocaleMessages('de', loadTranslation)
|
||||
|
||||
expect(result.locale).toBe('en')
|
||||
expect(Object.keys(result.messages)).toEqual(['en'])
|
||||
expect(error).toHaveBeenCalledWith('Failed to download translations', expect.any(Error))
|
||||
error.mockRestore()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import SunshineVersion from '../../src_assets/common/assets/web/sunshine_version.js'
|
||||
|
||||
describe('SunshineVersion', () => {
|
||||
it('parses release metadata', () => {
|
||||
const version = new SunshineVersion({
|
||||
name: 'Sunshine release',
|
||||
tag_name: 'v2026.9.16',
|
||||
tag_tag: 'stable',
|
||||
})
|
||||
|
||||
expect(version.release).toBeDefined()
|
||||
expect(version.version).toBe('v2026.9.16')
|
||||
expect(version.versionName).toBe('Sunshine release')
|
||||
expect(version.versionTag).toBe('stable')
|
||||
expect(version.versionParts).toEqual([2026, 9, 16])
|
||||
expect(version.versionMajor).toBe(2026)
|
||||
expect(version.versionMinor).toBe(9)
|
||||
expect(version.versionPatch).toBe(16)
|
||||
})
|
||||
|
||||
it('parses a version string without release metadata', () => {
|
||||
const version = new SunshineVersion(null, '1.2.3')
|
||||
|
||||
expect(version.release).toBeNull()
|
||||
expect(version.versionParts).toEqual([1, 2, 3])
|
||||
expect(version.versionName).toBeNull()
|
||||
expect(version.versionTag).toBeNull()
|
||||
})
|
||||
|
||||
it('requires either release metadata or a version string', () => {
|
||||
expect(() => new SunshineVersion()).toThrow('Either release or version must be provided')
|
||||
})
|
||||
|
||||
it('compares version objects and strings', () => {
|
||||
const version = new SunshineVersion(null, '2.0.0')
|
||||
|
||||
expect(version.isGreater('1.9.9')).toBe(true)
|
||||
expect(version.isGreater(new SunshineVersion(null, '2.0.0'))).toBe(false)
|
||||
expect(version.isGreater('3.0.0')).toBe(false)
|
||||
})
|
||||
|
||||
it('handles absent and invalid comparison values', () => {
|
||||
const version = new SunshineVersion(null, '2.0.0')
|
||||
const unparsedVersion = new SunshineVersion(null, 'placeholder')
|
||||
unparsedVersion.versionParts = null
|
||||
|
||||
expect(version.parseVersion(null)).toBeNull()
|
||||
expect(unparsedVersion.isGreater('1.0.0')).toBe(false)
|
||||
expect(() => version.isGreater(2)).toThrow(TypeError)
|
||||
})
|
||||
})
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import fs from 'fs';
|
||||
import { resolve } from 'path'
|
||||
import fs from 'node:fs';
|
||||
import { resolve } from 'node:path'
|
||||
import { defineConfig } from 'vite'
|
||||
import { codecovVitePlugin } from "@codecov/vite-plugin";
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import process from 'process'
|
||||
import process from 'node:process'
|
||||
|
||||
let assetsSrcPath = 'src_assets/common/assets/web';
|
||||
let assetsDstPath = 'build/assets/web';
|
||||
|
||||
Reference in new issue
Block a user