mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-17 12:47:21 -04:00
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e95a4226eb | ||
|
|
26ca374c47 | ||
|
|
64d78aa812 | ||
|
|
522e755b37 | ||
|
|
9c9486400b | ||
|
|
62192203e9 | ||
|
|
d8df18df82 | ||
|
|
953c11ed48 | ||
|
|
c796c7cd48 | ||
|
|
89a0e4d25f | ||
|
|
59a5afa21d | ||
|
|
66529d9026 | ||
|
|
abd89f9954 | ||
|
|
f7ffab1345 | ||
|
|
8886304894 | ||
|
|
0b8d55d434 | ||
|
|
99381a8f3e | ||
|
|
a422698f92 | ||
|
|
dce7e7ece6 | ||
|
|
e3fe63aee6 | ||
|
|
22c85c0cb4 | ||
|
|
a548d381b8 | ||
|
|
9abb21e389 | ||
|
|
4e4cc9d5e3 | ||
|
|
a353b15364 | ||
|
|
86b92c5d0e | ||
|
|
e24e07d0ce | ||
|
|
71da1c90c5 | ||
|
|
cd4a26b5f1 | ||
|
|
66ec776b0b | ||
|
|
2847910f22 | ||
|
|
d21466be6b | ||
|
|
21a78a1eb2 | ||
|
|
5aa245cf39 | ||
|
|
507ddac39d | ||
|
|
ef16de6280 |
No files matched your search
@@ -193,7 +193,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq gcc-aarch64-linux-gnu g++-aarch64-linux-gnu >/dev/null
|
||||
sudo apt-get install -y -qq gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libvulkan1 mesa-vulkan-drivers >/dev/null
|
||||
D=app/src/main/assets/linuxfs
|
||||
mkdir -p "$D"
|
||||
aarch64-linux-gnu-g++ -shared -fPIC -O2 -Wall -Wno-attributes -Wno-nonnull-compare \
|
||||
@@ -396,6 +396,30 @@ jobs:
|
||||
set -euo pipefail
|
||||
bash tools/linuxfs/check-preloads.sh app/src/main/assets/linuxfs
|
||||
|
||||
- name: Check Quest tracking IPC
|
||||
run: |
|
||||
set -euo pipefail
|
||||
NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
|
||||
VK_INCLUDE=$(ls -d "$NDK"/toolchains/llvm/prebuilt/*/sysroot/usr/include | head -1)
|
||||
mkdir -p build/vr/include
|
||||
cp -R "$VK_INCLUDE/vulkan" build/vr/include/
|
||||
if [ -d "$VK_INCLUDE/vk_video" ]; then cp -R "$VK_INCLUDE/vk_video" build/vr/include/; fi
|
||||
g++ -std=c++17 -Wall -Wextra -Werror -pthread tools/vr/tests/test_xr_tracking.cpp -o /tmp/test-xr-tracking
|
||||
/tmp/test-xr-tracking
|
||||
g++ -std=c++17 -Wall -Wextra -Werror tools/vr/tests/test_xr_touch.cpp -o /tmp/test-xr-touch
|
||||
/tmp/test-xr-touch
|
||||
g++ -std=c++17 -Wall -Wextra -Werror -pthread tools/vr/tests/test_xr_vsync.cpp -o /tmp/test-xr-vsync
|
||||
/tmp/test-xr-vsync
|
||||
g++ -std=c++17 -Wall -Wextra -Werror -pthread tools/vr/tests/test_xr_frames.cpp -o /tmp/test-xr-frames
|
||||
/tmp/test-xr-frames
|
||||
cc -Itools/vr/third_party/wayland -c tools/vr/src/wayland_client_stub.c -o /tmp/test-wayland-stub.o
|
||||
cc -Itools/vr/third_party/wayland -c app/src/main/cpp/waylandcomp/generated/xdg-shell-protocol.c -o /tmp/test-xdg-shell.o
|
||||
g++ -std=c++17 -Wall -Wextra -Werror -Wno-unused-parameter -Wno-missing-field-initializers -pthread -Ibuild/vr/include \
|
||||
-Itools/vr/third_party/wayland -Iapp/src/main/cpp/waylandcomp/generated \
|
||||
tools/vr/tests/test_presenter_shutdown.cpp tools/vr/src/presenter.cpp /tmp/test-wayland-stub.o /tmp/test-xdg-shell.o \
|
||||
-ldl -o /tmp/test-presenter-shutdown
|
||||
timeout 10 /tmp/test-presenter-shutdown
|
||||
|
||||
- name: Rust toolchain for the store engines (aarch64-linux-android)
|
||||
id: rust
|
||||
run: |
|
||||
@@ -429,7 +453,7 @@ jobs:
|
||||
path: |
|
||||
app/.cxx
|
||||
app/build/intermediates/cxx
|
||||
key: cxx-${{ env.NDK_VERSION }}-${{ hashFiles('app/src/main/cpp/**') }}
|
||||
key: cxx-${{ env.NDK_VERSION }}-${{ hashFiles('app/src/main/cpp/**', 'tools/vr/src/xr_*.h') }}
|
||||
restore-keys: |
|
||||
cxx-${{ env.NDK_VERSION }}-
|
||||
|
||||
|
||||
@@ -37,4 +37,4 @@ Compatibility and performance vary by device; hardware validation is limited. De
|
||||
|
||||
## Credits and licence
|
||||
|
||||
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence. See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
|
||||
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence. mmap ring-buffer controller input was developed by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)). See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
|
||||
+4
-2
@@ -150,6 +150,7 @@ android {
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
prefab true
|
||||
compose true
|
||||
buildConfig true
|
||||
}
|
||||
@@ -225,7 +226,8 @@ if (!skipRust) android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir)
|
||||
tasks.named('preBuild').configure { dependsOn(buildRustStores) }
|
||||
|
||||
dependencies {
|
||||
implementation 'com.github.luben:zstd-jni:1.5.2-3@aar'
|
||||
implementation 'org.khronos.openxr:openxr_loader_for_android:1.1.49'
|
||||
implementation 'com.github.luben:zstd-jni:1.5.7-4@aar'
|
||||
implementation 'org.apache.commons:commons-compress:1.27.1'
|
||||
implementation 'org.tukaani:xz:1.10'
|
||||
implementation platform('androidx.compose:compose-bom:2024.02.00')
|
||||
@@ -244,5 +246,5 @@ dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.robolectric:robolectric:4.16.1'
|
||||
testImplementation 'androidx.compose.ui:ui-test-junit4'
|
||||
testRuntimeOnly 'com.github.luben:zstd-jni:1.5.2-3@jar'
|
||||
testRuntimeOnly 'com.github.luben:zstd-jni:1.5.7-4@jar'
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.vr.headtracking" android:version="1" android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
@@ -37,6 +38,19 @@
|
||||
android:isGame="true">
|
||||
|
||||
<meta-data android:name="android.game_mode_config" android:resource="@xml/game_mode_config" />
|
||||
<meta-data android:name="com.oculus.supportedDevices" android:value="quest2|quest3|quest3s" />
|
||||
<activity android:name=".QuestXrActivity" android:exported="true" android:launchMode="singleTask"
|
||||
android:permission="android.permission.DUMP"
|
||||
android:resizeableActivity="false" android:screenOrientation="landscape"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard|navigation|uiMode"
|
||||
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
|
||||
<meta-data android:name="android.app.lib_name" android:value="droiddeckxr" />
|
||||
<intent-filter>
|
||||
<action android:name="com.droiddeck.launcher.XR" />
|
||||
<category android:name="com.oculus.intent.category.VR" />
|
||||
<category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
@@ -46,11 +60,13 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.CAR_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.CAR_LAUNCHER" />
|
||||
<data android:scheme="droiddeck" android:host="game" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
@@ -60,6 +76,7 @@
|
||||
<intent-filter>
|
||||
<action android:name="com.droiddeck.launcher.LAUNCH_GAME" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.CAR_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
@@ -106,7 +123,7 @@
|
||||
android:name=".stores.StoreLoginActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
|
||||
Binary file not shown.
@@ -30,6 +30,7 @@ string(SUBSTRING "${WINFG_CHAIN_HASH_FULL}" 0 12 WINFG_CHAIN_HASH)
|
||||
message(STATUS "win-fg chain: ${WINFG_UPSTREAM} (src ${WINFG_CHAIN_HASH})")
|
||||
|
||||
add_subdirectory(waylandcomp)
|
||||
add_subdirectory(questxr)
|
||||
add_library(sdnative SHARED sdnative/sdnative.c)
|
||||
target_link_libraries(sdnative log)
|
||||
|
||||
|
||||
@@ -371,6 +371,17 @@ send_vibration(int strong, int weak, uint16_t duration_ms, uint16_t slot) {
|
||||
syscall(SYS_close, sock);
|
||||
}
|
||||
|
||||
// Must match RumbleComponent.HOLD_MS: play until the slot's next effect or stop.
|
||||
static constexpr uint16_t RUMBLE_HOLD_MS = 0xffff;
|
||||
|
||||
__attribute__((visibility("hidden"))) static void send_ff_effect(const struct ff_effect &effect, uint16_t slot) {
|
||||
uint16_t duration = effect.replay.length ? effect.replay.length : RUMBLE_HOLD_MS;
|
||||
if (effect.type == FF_RUMBLE)
|
||||
send_vibration(effect.u.rumble.strong_magnitude, effect.u.rumble.weak_magnitude, duration, slot);
|
||||
else if (effect.type == FF_PERIODIC)
|
||||
send_vibration(effect.u.periodic.magnitude, effect.u.periodic.magnitude, duration, slot);
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static void
|
||||
check_ff_event(const struct input_event *ev, uint16_t slot) {
|
||||
if (ev->type != EV_FF)
|
||||
@@ -381,15 +392,7 @@ check_ff_event(const struct input_event *ev, uint16_t slot) {
|
||||
auto it = ff_effects().find(id);
|
||||
if (it == ff_effects().end())
|
||||
return;
|
||||
|
||||
uint16_t duration = it->second.replay.length;
|
||||
if (it->second.type == FF_RUMBLE) {
|
||||
send_vibration(it->second.u.rumble.strong_magnitude,
|
||||
it->second.u.rumble.weak_magnitude, duration, slot);
|
||||
} else if (it->second.type == FF_PERIODIC) {
|
||||
send_vibration(it->second.u.periodic.magnitude,
|
||||
it->second.u.periodic.magnitude, duration, slot);
|
||||
}
|
||||
send_ff_effect(it->second, slot);
|
||||
} else {
|
||||
send_vibration(0, 0, 0, slot);
|
||||
}
|
||||
@@ -3469,16 +3472,7 @@ EXPORT int ioctl(int fd, ioctl_request_t op, ...) {
|
||||
if (effect->id == -1)
|
||||
effect->id = next_ff_id++;
|
||||
ff_effects()[effect->id] = *effect;
|
||||
|
||||
uint16_t duration = effect->replay.length;
|
||||
uint16_t slot = static_cast<uint16_t>(event_number);
|
||||
if (effect->type == FF_RUMBLE) {
|
||||
send_vibration(effect->u.rumble.strong_magnitude,
|
||||
effect->u.rumble.weak_magnitude, duration, slot);
|
||||
} else if (effect->type == FF_PERIODIC) {
|
||||
send_vibration(effect->u.periodic.magnitude, effect->u.periodic.magnitude,
|
||||
duration, slot);
|
||||
}
|
||||
send_ff_effect(*effect, static_cast<uint16_t>(event_number));
|
||||
return 0;
|
||||
} else if (type == 0x45 && number == 0x81) {
|
||||
int id = (intptr_t)argp;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
project(droiddeckxr C CXX)
|
||||
find_package(OpenXR REQUIRED CONFIG)
|
||||
add_library(droiddeckxr_glue STATIC ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c)
|
||||
target_include_directories(droiddeckxr_glue PUBLIC ${ANDROID_NDK}/sources/android/native_app_glue)
|
||||
add_library(droiddeckxr SHARED quest_xr.cpp)
|
||||
target_compile_features(droiddeckxr PRIVATE cxx_std_17)
|
||||
target_compile_definitions(droiddeckxr PRIVATE XR_USE_PLATFORM_ANDROID XR_USE_GRAPHICS_API_OPENGL_ES XR_USE_TIMESPEC)
|
||||
target_compile_options(droiddeckxr PRIVATE -Wall -Wextra -Wno-missing-field-initializers -Werror=return-type)
|
||||
target_link_options(droiddeckxr PRIVATE -Wl,-z,max-page-size=16384 -uANativeActivity_onCreate)
|
||||
target_link_libraries(droiddeckxr PRIVATE droiddeckxr_glue OpenXR::openxr_loader EGL GLESv3 android log)
|
||||
|
||||
target_include_directories(droiddeckxr PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../tools/vr/src)
|
||||
@@ -0,0 +1,201 @@
|
||||
#pragma once
|
||||
#include <EGL/eglext.h>
|
||||
#include <GLES3/gl31.h>
|
||||
#include <GLES2/gl2ext.h>
|
||||
#include <android/hardware_buffer.h>
|
||||
#include "xr_gpu_channel.h"
|
||||
|
||||
class QuestGpuFrames {
|
||||
public:
|
||||
bool Start(const std::string &filename, EGLDisplay display) {
|
||||
const char *extensions = reinterpret_cast<const char *>(glGetString(GL_EXTENSIONS));
|
||||
const char *eglExtensions = eglQueryString(display, EGL_EXTENSIONS);
|
||||
if (!extensions || !eglExtensions || !strstr(extensions, "GL_EXT_external_buffer")
|
||||
|| !strstr(eglExtensions, "EGL_ANDROID_get_native_client_buffer")) return false;
|
||||
storage_ = reinterpret_cast<PFNGLBUFFERSTORAGEEXTERNALEXTPROC>(eglGetProcAddress("glBufferStorageExternalEXT"));
|
||||
client_ = reinterpret_cast<PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC>(eglGetProcAddress("eglGetNativeClientBufferANDROID"));
|
||||
if (strstr(extensions, "GL_OES_texture_view")) view_ = reinterpret_cast<PFNGLTEXTUREVIEWOESPROC>(eglGetProcAddress("glTextureViewOES"));
|
||||
else if (strstr(extensions, "GL_EXT_texture_view")) view_ = reinterpret_cast<PFNGLTEXTUREVIEWOESPROC>(eglGetProcAddress("glTextureViewEXT"));
|
||||
if (!storage_ || !client_ || !view_ || filename.size() >= sizeof(sockaddr_un::sun_path)) return false;
|
||||
const char *source = "#version 310 es\nlayout(local_size_x=8,local_size_y=8)in;"
|
||||
"layout(std430,binding=0)readonly buffer Source{uint pixels[];};"
|
||||
"layout(rgba8,binding=0)writeonly uniform highp image2D target;uniform bool bgra;"
|
||||
"void main(){ivec2 p=ivec2(gl_GlobalInvocationID.xy),size=imageSize(target);"
|
||||
"if(any(greaterThanEqual(p,size)))return;vec4 value=unpackUnorm4x8(pixels[p.y*size.x+p.x]);"
|
||||
"imageStore(target,p,bgra?value.bgra:value);}";
|
||||
GLuint shader = glCreateShader(GL_COMPUTE_SHADER); glShaderSource(shader, 1, &source, nullptr); glCompileShader(shader);
|
||||
program_ = glCreateProgram(); glAttachShader(program_, shader); glLinkProgram(program_); glDeleteShader(shader);
|
||||
GLint linked{}; glGetProgramiv(program_, GL_LINK_STATUS, &linked);
|
||||
if (!linked) { Stop(); return false; }
|
||||
lock_ = open((filename + ".lock").c_str(), O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600);
|
||||
struct stat status{};
|
||||
if (lock_ < 0 || fstat(lock_, &status) || !S_ISREG(status.st_mode) || status.st_uid != getuid()
|
||||
|| flock(lock_, LOCK_EX | LOCK_NB)) { Stop(); return false; }
|
||||
if (lstat(filename.c_str(), &status) == 0) {
|
||||
if (!S_ISSOCK(status.st_mode) || status.st_uid != getuid() || unlink(filename.c_str())) { Stop(); return false; }
|
||||
} else if (errno != ENOENT) { Stop(); return false; }
|
||||
listener_ = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
|
||||
sockaddr_un address{}; address.sun_family = AF_UNIX; strcpy(address.sun_path, filename.c_str());
|
||||
if (listener_ < 0 || bind(listener_, reinterpret_cast<sockaddr *>(&address), sizeof(address))) { Stop(); return false; }
|
||||
filename_ = filename;
|
||||
if (chmod(filename.c_str(), 0600) || listen(listener_, 2)) { Stop(); return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Take(DroidDeckXrFrameHeader &header, GLuint &texture) {
|
||||
if (listener_ < 0) return false;
|
||||
if (peer_ < 0) {
|
||||
peer_ = accept4(listener_, nullptr, nullptr, SOCK_CLOEXEC | SOCK_NONBLOCK);
|
||||
if (peer_ < 0) return false;
|
||||
if (!xr_frame_peer(peer_)) { Disconnect(); return false; }
|
||||
configured_ = false; deadline_ = xr_frame_now() + 2000000000LL;
|
||||
}
|
||||
pollfd ready{peer_, POLLIN, 0};
|
||||
if (poll(&ready, 1, 0) <= 0) {
|
||||
if (!configured_ && xr_frame_now() > deadline_) Disconnect();
|
||||
return false;
|
||||
}
|
||||
if (!(ready.revents & POLLIN)) { Disconnect(); return false; }
|
||||
if (!configured_) {
|
||||
XrGpuRequest request;
|
||||
if (!xr_gpu_receive(peer_, &request, sizeof(request)) || request.magic != XR_GPU_MAGIC
|
||||
|| request.version != XR_GPU_VERSION || !xr_gpu_dimensions(request.width, request.height)
|
||||
|| !Allocate(request.width, request.height)) { Disconnect(); return false; }
|
||||
XrGpuPoolInfo info; info.width = width_; info.height = height_;
|
||||
for (unsigned index = 0; index < XR_GPU_SLOTS; index++) info.sizes[index] = sizes_[index];
|
||||
configured_ = xr_gpu_send(peer_, &info, sizeof(info), fds_.data(), fds_.size());
|
||||
for (int &fd : fds_) { if (fd >= 0) close(fd); fd = -1; }
|
||||
if (!configured_) Disconnect();
|
||||
return false;
|
||||
}
|
||||
XrGpuFrame frame;
|
||||
if (!xr_gpu_receive(peer_, &frame, sizeof(frame))) { Disconnect(); return false; }
|
||||
auto state = xr_gpu_frame_state(frame, width_, height_, lastFrame_, xr_frame_now());
|
||||
if (state == XrGpuFrameState::Invalid) {
|
||||
rejected_++; Disconnect(); return false;
|
||||
}
|
||||
lastFrame_ = frame.header.frame_id;
|
||||
if (state == XrGpuFrameState::Stale) {
|
||||
// No GPU read was submitted, so the producer can safely reclaim a dropped frame.
|
||||
dropped_++;
|
||||
if (!xr_gpu_send(peer_, &frame.header.frame_id, sizeof(frame.header.frame_id))) Disconnect();
|
||||
return false;
|
||||
}
|
||||
if (!Output()) { Disconnect(); return false; }
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
glUseProgram(program_); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, buffers_[frame.slot]);
|
||||
glUniform1i(glGetUniformLocation(program_, "bgra"), !!(frame.flags & XR_GPU_BGRA));
|
||||
glBindImageTexture(0, raw_, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8);
|
||||
glDispatchCompute((width_ + 7) / 8, (height_ + 7) / 8, 1);
|
||||
glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
|
||||
// The producer may overwrite this slot only after the native GPU has finished reading it.
|
||||
glFinish();
|
||||
if (glGetError() != GL_NO_ERROR) { rejected_++; Disconnect(); return false; }
|
||||
if (!xr_gpu_send(peer_, &frame.header.frame_id, sizeof(frame.header.frame_id))) Disconnect();
|
||||
received_++;
|
||||
header = frame.header;
|
||||
texture = header.color_space == DROIDDECK_XR_COLOR_SRGB_RGBA8 ? srgb_ : raw_;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t Received() const { return received_; }
|
||||
uint64_t Rejected() const { return rejected_; }
|
||||
uint64_t Dropped() const { return dropped_; }
|
||||
void Stop() {
|
||||
Disconnect(); DropPool();
|
||||
glDeleteTextures(1, &srgb_); glDeleteTextures(1, &raw_); srgb_ = raw_ = 0;
|
||||
if (program_) glDeleteProgram(program_); program_ = 0;
|
||||
if (listener_ >= 0) close(listener_); listener_ = -1;
|
||||
if (!filename_.empty()) unlink(filename_.c_str()); filename_.clear();
|
||||
if (lock_ >= 0) close(lock_); lock_ = -1;
|
||||
}
|
||||
private:
|
||||
int listener_ = -1, peer_ = -1, lock_ = -1;
|
||||
std::string filename_;
|
||||
bool configured_ = false;
|
||||
int64_t deadline_{};
|
||||
uint32_t width_{}, height_{}, outputWidth_{}, outputHeight_{};
|
||||
uint64_t lastFrame_{}, received_{}, rejected_{}, dropped_{};
|
||||
std::array<AHardwareBuffer *, XR_GPU_SLOTS> hardware_{};
|
||||
std::array<GLuint, XR_GPU_SLOTS> buffers_{};
|
||||
std::array<int, XR_GPU_SLOTS> fds_{{-1, -1, -1}};
|
||||
std::array<uint64_t, XR_GPU_SLOTS> sizes_{};
|
||||
GLuint raw_{}, srgb_{}, program_{};
|
||||
PFNGLBUFFERSTORAGEEXTERNALEXTPROC storage_{};
|
||||
PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC client_{};
|
||||
PFNGLTEXTUREVIEWOESPROC view_{};
|
||||
|
||||
void Disconnect() { if (peer_ >= 0) close(peer_); peer_ = -1; configured_ = false; lastFrame_ = 0; }
|
||||
void DropPool() {
|
||||
if (buffers_[0]) glFinish();
|
||||
glDeleteBuffers(buffers_.size(), buffers_.data()); buffers_.fill(0);
|
||||
for (auto *&buffer : hardware_) { if (buffer) AHardwareBuffer_release(buffer); buffer = nullptr; }
|
||||
for (int &fd : fds_) { if (fd >= 0) close(fd); fd = -1; }
|
||||
}
|
||||
static int AllocationFd(AHardwareBuffer *buffer, uint64_t bytes, uint64_t &allocation) {
|
||||
int pair[2];
|
||||
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair)) return -1;
|
||||
if (AHardwareBuffer_sendHandleToUnixSocket(buffer, pair[0])) { close(pair[0]); close(pair[1]); return -1; }
|
||||
close(pair[0]);
|
||||
int selected = -1; unsigned candidates = 0;
|
||||
for (;;) {
|
||||
unsigned char data[1024]; iovec bytesReceived{data, sizeof(data)};
|
||||
alignas(cmsghdr) unsigned char control[CMSG_SPACE(32 * sizeof(int))]{};
|
||||
msghdr message{}; message.msg_iov = &bytesReceived; message.msg_iovlen = 1;
|
||||
message.msg_control = control; message.msg_controllen = sizeof(control);
|
||||
ssize_t result = recvmsg(pair[1], &message, MSG_CMSG_CLOEXEC);
|
||||
if (result < 0 && errno == EINTR) continue;
|
||||
if (result <= 0) break;
|
||||
for (auto *item = CMSG_FIRSTHDR(&message); item; item = CMSG_NXTHDR(&message, item)) {
|
||||
if (item->cmsg_level != SOL_SOCKET || item->cmsg_type != SCM_RIGHTS) continue;
|
||||
auto *fds = reinterpret_cast<int *>(CMSG_DATA(item));
|
||||
for (unsigned index = 0; index < (item->cmsg_len - CMSG_LEN(0)) / sizeof(int); index++) {
|
||||
struct stat status{};
|
||||
if (!fstat(fds[index], &status) && status.st_size >= static_cast<int64_t>(bytes)
|
||||
&& status.st_size <= static_cast<int64_t>(bytes + 65536)) {
|
||||
candidates++; if (selected >= 0) close(selected);
|
||||
selected = fds[index]; allocation = status.st_size;
|
||||
} else close(fds[index]);
|
||||
}
|
||||
}
|
||||
if (message.msg_flags & (MSG_TRUNC | MSG_CTRUNC)) { candidates = 0; break; }
|
||||
}
|
||||
close(pair[1]);
|
||||
if (candidates != 1) { if (selected >= 0) close(selected); return -1; }
|
||||
return selected;
|
||||
}
|
||||
bool Allocate(uint32_t width, uint32_t height) {
|
||||
DropPool(); width_ = width; height_ = height;
|
||||
const uint64_t bytes = uint64_t(width) * height * 4;
|
||||
GLint64 limit{}; glGetInteger64v(GL_MAX_SHADER_STORAGE_BLOCK_SIZE, &limit);
|
||||
if (bytes > static_cast<uint64_t>(limit)) return false;
|
||||
AHardwareBuffer_Desc description{}; description.width = bytes; description.height = description.layers = 1;
|
||||
description.format = AHARDWAREBUFFER_FORMAT_BLOB;
|
||||
description.usage = AHARDWAREBUFFER_USAGE_GPU_DATA_BUFFER;
|
||||
glGenBuffers(buffers_.size(), buffers_.data());
|
||||
for (unsigned index = 0; index < XR_GPU_SLOTS; index++) {
|
||||
if (AHardwareBuffer_allocate(&description, &hardware_[index])) { DropPool(); return false; }
|
||||
fds_[index] = AllocationFd(hardware_[index], bytes, sizes_[index]);
|
||||
if (fds_[index] < 0) { DropPool(); return false; }
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffers_[index]);
|
||||
storage_(GL_SHADER_STORAGE_BUFFER, 0, bytes, client_(hardware_[index]), 0);
|
||||
if (glGetError() != GL_NO_ERROR) { DropPool(); return false; }
|
||||
}
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
return true;
|
||||
}
|
||||
bool Output() {
|
||||
if (raw_ && outputWidth_ == width_ && outputHeight_ == height_) return true;
|
||||
glDeleteTextures(1, &srgb_); glDeleteTextures(1, &raw_); srgb_ = raw_ = 0;
|
||||
glGenTextures(1, &raw_); glBindTexture(GL_TEXTURE_2D, raw_);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width_, height_);
|
||||
glGenTextures(1, &srgb_); view_(srgb_, GL_TEXTURE_2D, raw_, GL_SRGB8_ALPHA8, 0, 1, 0, 1);
|
||||
for (GLuint texture : {raw_, srgb_}) {
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
outputWidth_ = width_; outputHeight_ = height_;
|
||||
return glGetError() == GL_NO_ERROR;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,777 @@
|
||||
#include <jni.h>
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES3/gl3.h>
|
||||
#include <android/log.h>
|
||||
#include <android_native_app_glue.h>
|
||||
#include <time.h>
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_platform.h>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <cerrno>
|
||||
#include <cstddef>
|
||||
#include <ctime>
|
||||
#include "xr_tracking.h"
|
||||
#include "xr_frame_channel.h"
|
||||
#include "xr_frame_math.h"
|
||||
#include "gpu_frames.h"
|
||||
|
||||
static void check(XrResult result, const char *operation) {
|
||||
if (XR_FAILED(result)) throw std::runtime_error(std::string(operation) + ": " + std::to_string(result));
|
||||
}
|
||||
|
||||
struct Eye {
|
||||
XrSwapchain swapchain = XR_NULL_HANDLE;
|
||||
int width = 0, height = 0;
|
||||
std::vector<XrSwapchainImageOpenGLESKHR> images;
|
||||
};
|
||||
|
||||
class QuestXR {
|
||||
public:
|
||||
explicit QuestXR(android_app *app) : app_(app) {}
|
||||
~QuestXR() { cleanup(); }
|
||||
|
||||
void run() {
|
||||
initialize();
|
||||
while (!app_->destroyRequested && !exit_) {
|
||||
android_poll_source *source = nullptr;
|
||||
int events = 0;
|
||||
int pollTimeout = running_ ? 0 : 100;
|
||||
for (int eventCount = 0; eventCount < 32; eventCount++) {
|
||||
if (ALooper_pollOnce(pollTimeout, nullptr, &events, reinterpret_cast<void **>(&source)) < 0) break;
|
||||
if (source) source->process(app_, source);
|
||||
if (app_->destroyRequested) break;
|
||||
pollTimeout = 0;
|
||||
}
|
||||
receiveFrame();
|
||||
pollEvents();
|
||||
if (running_ && !app_->destroyRequested) frame();
|
||||
}
|
||||
if (exit_ && !app_->destroyRequested) ANativeActivity_finish(app_->activity);
|
||||
}
|
||||
|
||||
void health(const char *error = "") {
|
||||
int steamvrState = -1;
|
||||
if (statusCallback_) {
|
||||
steamvrState = env_->CallIntMethod(app_->activity->clazz, statusCallback_);
|
||||
if (env_->ExceptionCheck()) { env_->ExceptionClear(); steamvrState = -1; }
|
||||
}
|
||||
std::string path = std::string(app_->activity->externalDataPath) + "/xr-health.json";
|
||||
std::string temp = path + ".tmp";
|
||||
FILE *file = std::fopen(temp.c_str(), "w");
|
||||
if (!file) return;
|
||||
std::fprintf(file,
|
||||
"{\"frames\":%llu,\"rendered\":%llu,\"sessionState\":%d,\"headFlags\":%llu,"
|
||||
"\"headPosition\":[%.6f,%.6f,%.6f],\"controllersActive\":[%s,%s],"
|
||||
"\"periodNs\":%lld,\"surfaceSwapchain\":%s,\"source\":\"%s\",\"guestFrames\":%lld,\"steamvrState\":%d,"
|
||||
"\"receivedFrames\":%llu,\"rejectedFrames\":%llu,\"uploadedFrames\":%llu,\"sourceFrameId\":%llu,"
|
||||
"\"gpuReceivedFrames\":%llu,\"gpuDroppedFrames\":%llu,\"gpuVerifiedFrameId\":%llu,\"gpuChecksum\":%u,\"error\":\"%s\"}\n",
|
||||
(unsigned long long)frames_, (unsigned long long)rendered_, state_,
|
||||
(unsigned long long)head_.locationFlags,
|
||||
head_.pose.position.x, head_.pose.position.y, head_.pose.position.z,
|
||||
handActive_[0] ? "true" : "false", handActive_[1] ? "true" : "false",
|
||||
(long long)period_, "false", "frames",
|
||||
0LL, steamvrState, (unsigned long long)(receiver_.Received() + gpuFrames_.Received()),
|
||||
(unsigned long long)(receiver_.Rejected() + gpuFrames_.Rejected()),
|
||||
(unsigned long long)uploadedFrames_, (unsigned long long)sourceHeader_.frame_id,
|
||||
(unsigned long long)gpuFrames_.Received(), (unsigned long long)gpuFrames_.Dropped(),
|
||||
(unsigned long long)verifiedFrame_, gpuChecksum_, error);
|
||||
std::fclose(file);
|
||||
std::rename(temp.c_str(), path.c_str());
|
||||
}
|
||||
|
||||
private:
|
||||
android_app *app_;
|
||||
JNIEnv *env_ = nullptr;
|
||||
XrInstance instance_ = XR_NULL_HANDLE;
|
||||
XrSession session_ = XR_NULL_HANDLE;
|
||||
XrSystemId system_ = XR_NULL_SYSTEM_ID;
|
||||
XrSpace world_ = XR_NULL_HANDLE, view_ = XR_NULL_HANDLE;
|
||||
XrActionSet actions_ = XR_NULL_HANDLE;
|
||||
XrAction grip_ = XR_NULL_HANDLE, trigger_ = XR_NULL_HANDLE, vibration_ = XR_NULL_HANDLE;
|
||||
XrAction squeeze_ = XR_NULL_HANDLE, stick_ = XR_NULL_HANDLE, stickClick_ = XR_NULL_HANDLE;
|
||||
XrAction primary_ = XR_NULL_HANDLE, secondary_ = XR_NULL_HANDLE, menu_ = XR_NULL_HANDLE;
|
||||
XrAction triggerTouch_ = XR_NULL_HANDLE, stickTouch_ = XR_NULL_HANDLE, primaryTouch_ = XR_NULL_HANDLE;
|
||||
XrAction secondaryTouch_ = XR_NULL_HANDLE, thumbrestTouch_ = XR_NULL_HANDLE;
|
||||
DroidDeckXrHand inputs_[2]{};
|
||||
PFN_xrConvertTimeToTimespecTimeKHR convertTime_ = nullptr;
|
||||
bool timespecSupported_ = false;
|
||||
PFN_xrPerfSettingsSetPerformanceLevelEXT setPerformance_ = nullptr;
|
||||
bool performanceSupported_ = false;
|
||||
XrPath hands_[2]{};
|
||||
XrSpace handSpaces_[2]{};
|
||||
XrSpaceLocation head_{XR_TYPE_SPACE_LOCATION};
|
||||
XrSpaceLocation handLocations_[2]{{XR_TYPE_SPACE_LOCATION}, {XR_TYPE_SPACE_LOCATION}};
|
||||
bool handActive_[2]{};
|
||||
bool running_ = false, exit_ = false, quitRequested_ = false;
|
||||
bool guestAttached_ = false, verifyPixels_ = false;
|
||||
XrFrameReceiver receiver_;
|
||||
QuestGpuFrames gpuFrames_;
|
||||
DroidDeckXrFrameHeader sourceHeader_{};
|
||||
uint64_t uploadedFrames_ = 0, verifiedFrame_ = 0;
|
||||
uint32_t gpuChecksum_ = 0;
|
||||
GLuint sourceTexture_ = 0, sourceProgram_ = 0;
|
||||
GLuint cpuTexture_ = 0;
|
||||
jmethodID frameCallback_ = nullptr;
|
||||
jmethodID activeCallback_ = nullptr;
|
||||
jmethodID statusCallback_ = nullptr;
|
||||
DroidDeckXrTracking *tracking_ = nullptr;
|
||||
DroidDeckXrHaptics *haptics_ = nullptr;
|
||||
uint64_t lastPulse_[2]{};
|
||||
XrSessionState state_ = XR_SESSION_STATE_UNKNOWN;
|
||||
uint64_t frames_ = 0, rendered_ = 0;
|
||||
XrDuration period_ = 0;
|
||||
EGLDisplay display_ = EGL_NO_DISPLAY;
|
||||
EGLContext context_ = EGL_NO_CONTEXT;
|
||||
EGLSurface surface_ = EGL_NO_SURFACE;
|
||||
std::array<Eye, 2> eyes_;
|
||||
GLuint framebuffer_ = 0, vertexArray_ = 0;
|
||||
GLint sourceEncoding_ = 0, eyeEncoding_ = 0;
|
||||
|
||||
std::string captureRequest() {
|
||||
if (frames_ % 9) return {};
|
||||
std::string file = std::string(app_->activity->externalDataPath) + "/xr-capture.request";
|
||||
FILE *request = std::fopen(file.c_str(), "r");
|
||||
if (!request) return {};
|
||||
char token[34]{};
|
||||
size_t size = std::fread(token, 1, sizeof(token) - 1, request);
|
||||
std::fclose(request);
|
||||
unlink(file.c_str());
|
||||
if (size != 32 || std::strspn(token, "0123456789abcdef") != 32) return {};
|
||||
unlink((std::string(app_->activity->externalDataPath) + "/xr-capture.json").c_str());
|
||||
return token;
|
||||
}
|
||||
|
||||
bool captureTexture(GLuint texture, int width, int height, const char *name) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
GLint encoding = 0;
|
||||
glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING, &encoding);
|
||||
if (!std::strcmp(name, "source")) sourceEncoding_ = encoding;
|
||||
else eyeEncoding_ = encoding;
|
||||
std::vector<unsigned char> pixels((size_t)width * height * 4);
|
||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
bool ok = glGetError() == GL_NO_ERROR;
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (!ok) return false;
|
||||
std::string file = std::string(app_->activity->externalDataPath) +
|
||||
"/xr-capture-" + name + ".ppm";
|
||||
std::string temporary = file + ".tmp";
|
||||
FILE *output = std::fopen(temporary.c_str(), "wb");
|
||||
if (!output) return false;
|
||||
ok = std::fprintf(output, "P6\n%d %d\n255\n", width, height) > 0;
|
||||
std::vector<unsigned char> row((size_t)width * 3);
|
||||
for (int y = height - 1; y >= 0 && ok; y--) {
|
||||
for (int x = 0; x < width; x++)
|
||||
std::memcpy(row.data() + x * 3, pixels.data() + ((size_t)y * width + x) * 4, 3);
|
||||
ok = std::fwrite(row.data(), 1, row.size(), output) == row.size();
|
||||
}
|
||||
if (std::fclose(output) != 0) ok = false;
|
||||
if (!ok) { unlink(temporary.c_str()); return false; }
|
||||
return std::rename(temporary.c_str(), file.c_str()) == 0;
|
||||
}
|
||||
|
||||
void captureResult(const std::string &token, bool ok, bool valid,
|
||||
const XrCompositionLayerProjectionView (&views)[2], XrTime displayTime) {
|
||||
std::string file = std::string(app_->activity->externalDataPath) + "/xr-capture.json";
|
||||
std::string temporary = file + ".tmp";
|
||||
FILE *output = std::fopen(temporary.c_str(), "w");
|
||||
if (!output) return;
|
||||
std::fprintf(output, "{\"id\":\"%s\",\"ok\":%s,\"validLayer\":%s,\"frame\":%llu,"
|
||||
"\"sourceFrameId\":%llu,\"displayTime\":%lld,\"sessionState\":%d,\"eyes\":[",
|
||||
token.c_str(), ok ? "true" : "false", valid ? "true" : "false",
|
||||
(unsigned long long)frames_, (unsigned long long)sourceHeader_.frame_id, (long long)displayTime, state_);
|
||||
for (int eye = 0; eye < 2; eye++) {
|
||||
const auto &pose = views[eye].pose;
|
||||
const auto &fov = views[eye].fov;
|
||||
std::fprintf(output, "%s{\"width\":%d,\"height\":%d,\"position\":[%g,%g,%g],"
|
||||
"\"orientation\":[%g,%g,%g,%g],\"fov\":[%g,%g,%g,%g]}",
|
||||
eye ? "," : "", eyes_[eye].width, eyes_[eye].height,
|
||||
pose.position.x, pose.position.y, pose.position.z,
|
||||
pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w,
|
||||
fov.angleLeft, fov.angleRight, fov.angleUp, fov.angleDown);
|
||||
}
|
||||
const char *extensions = reinterpret_cast<const char *>(glGetString(GL_EXTENSIONS));
|
||||
const bool srgbControl = extensions && std::strstr(extensions, "GL_EXT_sRGB_write_control");
|
||||
std::fprintf(output, "],\"sourceImage\":true,\"sourceColorSpace\":\"%s\",\"sourceEncoding\":%d,\"eyeEncoding\":%d,"
|
||||
"\"srgbWriteControl\":%s,\"srgbWriteEnabled\":%s}\n",
|
||||
sourceHeader_.color_space == DROIDDECK_XR_COLOR_SRGB_RGBA8 ? "srgb" : "linear", sourceEncoding_, eyeEncoding_,
|
||||
srgbControl ? "true" : "false", srgbControl && glIsEnabled(0x8DB9) ? "true" : "false");
|
||||
if (std::fclose(output) == 0) std::rename(temporary.c_str(), file.c_str());
|
||||
}
|
||||
|
||||
XrPath path(const char *text) {
|
||||
XrPath result{};
|
||||
check(xrStringToPath(instance_, text, &result), "xrStringToPath");
|
||||
return result;
|
||||
}
|
||||
|
||||
void initialize() {
|
||||
app_->userData = this;
|
||||
app_->onInputEvent = [](android_app *app, AInputEvent *event) -> int32_t {
|
||||
if (AInputEvent_getType(event) != AINPUT_EVENT_TYPE_KEY || AKeyEvent_getKeyCode(event) != AKEYCODE_BACK
|
||||
|| AKeyEvent_getAction(event) != AKEY_EVENT_ACTION_UP) return 0;
|
||||
auto *runtime = static_cast<QuestXR *>(app->userData);
|
||||
if (runtime->running_) {
|
||||
XrResult result = xrRequestExitSession(runtime->session_);
|
||||
if (XR_SUCCEEDED(result)) runtime->quitRequested_ = true;
|
||||
else __android_log_print(ANDROID_LOG_ERROR, "DroidDeckXR", "exit request failed: %d", result);
|
||||
} else ANativeActivity_finish(app->activity);
|
||||
return 1;
|
||||
};
|
||||
app_->activity->vm->AttachCurrentThread(&env_, nullptr);
|
||||
jclass activityClass = env_->GetObjectClass(app_->activity->clazz);
|
||||
jmethodID getIntent = env_->GetMethodID(activityClass, "getIntent", "()Landroid/content/Intent;");
|
||||
jobject intent = env_->CallObjectMethod(app_->activity->clazz, getIntent);
|
||||
jclass intentClass = env_->GetObjectClass(intent);
|
||||
jmethodID getBoolean = env_->GetMethodID(intentClass, "getBooleanExtra", "(Ljava/lang/String;Z)Z");
|
||||
jstring extra = env_->NewStringUTF("frame_verify");
|
||||
verifyPixels_ = env_->CallBooleanMethod(intent, getBoolean, extra, JNI_FALSE);
|
||||
env_->DeleteLocalRef(extra);
|
||||
activeCallback_ = env_->GetMethodID(activityClass, "onXrActive", "(Z)V");
|
||||
frameCallback_ = env_->GetMethodID(activityClass, "onXrFrame", "(J)V");
|
||||
statusCallback_ = env_->GetMethodID(activityClass, "onXrStatus", "()I");
|
||||
if (env_->ExceptionCheck()) { env_->ExceptionClear(); throw std::runtime_error("XR activity bridge methods unavailable"); }
|
||||
env_->DeleteLocalRef(intentClass); env_->DeleteLocalRef(intent); env_->DeleteLocalRef(activityClass);
|
||||
PFN_xrInitializeLoaderKHR initializeLoader = nullptr;
|
||||
check(xrGetInstanceProcAddr(XR_NULL_HANDLE, "xrInitializeLoaderKHR",
|
||||
reinterpret_cast<PFN_xrVoidFunction *>(&initializeLoader)), "loader init entrypoint");
|
||||
XrLoaderInitInfoAndroidKHR loader{XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR};
|
||||
loader.applicationVM = app_->activity->vm;
|
||||
loader.applicationContext = app_->activity->clazz;
|
||||
check(initializeLoader(reinterpret_cast<XrLoaderInitInfoBaseHeaderKHR *>(&loader)), "xrInitializeLoaderKHR");
|
||||
uint32_t count = 0;
|
||||
check(xrEnumerateInstanceExtensionProperties(nullptr, 0, &count, nullptr), "extensions count");
|
||||
std::vector<XrExtensionProperties> extensions(count, {XR_TYPE_EXTENSION_PROPERTIES});
|
||||
check(xrEnumerateInstanceExtensionProperties(nullptr, count, &count, extensions.data()), "extensions");
|
||||
for (const auto &extension : extensions) {
|
||||
if (!std::strcmp(extension.extensionName, XR_KHR_CONVERT_TIMESPEC_TIME_EXTENSION_NAME))
|
||||
timespecSupported_ = true;
|
||||
if (!std::strcmp(extension.extensionName, XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME))
|
||||
performanceSupported_ = true;
|
||||
}
|
||||
std::vector<const char *> enabled{XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME, XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME};
|
||||
if (timespecSupported_) enabled.push_back(XR_KHR_CONVERT_TIMESPEC_TIME_EXTENSION_NAME);
|
||||
if (performanceSupported_) enabled.push_back(XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME);
|
||||
XrInstanceCreateInfoAndroidKHR androidInfo{XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR};
|
||||
androidInfo.applicationVM = app_->activity->vm;
|
||||
androidInfo.applicationActivity = app_->activity->clazz;
|
||||
XrInstanceCreateInfo create{XR_TYPE_INSTANCE_CREATE_INFO};
|
||||
create.next = &androidInfo;
|
||||
std::strcpy(create.applicationInfo.applicationName, "DroidDeck XR");
|
||||
create.applicationInfo.applicationVersion = 1;
|
||||
create.applicationInfo.apiVersion = XR_MAKE_VERSION(1, 0, 0);
|
||||
create.enabledExtensionCount = enabled.size();
|
||||
create.enabledExtensionNames = enabled.data();
|
||||
check(xrCreateInstance(&create, &instance_), "xrCreateInstance");
|
||||
if (performanceSupported_) {
|
||||
XrResult result = xrGetInstanceProcAddr(instance_, "xrPerfSettingsSetPerformanceLevelEXT",
|
||||
reinterpret_cast<PFN_xrVoidFunction *>(&setPerformance_));
|
||||
if (XR_FAILED(result)) setPerformance_ = nullptr;
|
||||
}
|
||||
if (timespecSupported_) check(xrGetInstanceProcAddr(instance_, "xrConvertTimeToTimespecTimeKHR",
|
||||
reinterpret_cast<PFN_xrVoidFunction *>(&convertTime_)), "time conversion entrypoint");
|
||||
XrSystemGetInfo systemInfo{XR_TYPE_SYSTEM_GET_INFO};
|
||||
systemInfo.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
|
||||
check(xrGetSystem(instance_, &systemInfo, &system_), "xrGetSystem");
|
||||
PFN_xrGetOpenGLESGraphicsRequirementsKHR requirementsFunction = nullptr;
|
||||
check(xrGetInstanceProcAddr(instance_, "xrGetOpenGLESGraphicsRequirementsKHR",
|
||||
reinterpret_cast<PFN_xrVoidFunction *>(&requirementsFunction)), "GLES requirements entrypoint");
|
||||
XrGraphicsRequirementsOpenGLESKHR requirements{XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR};
|
||||
check(requirementsFunction(instance_, system_, &requirements), "GLES requirements");
|
||||
if (requirements.minApiVersionSupported > XR_MAKE_VERSION(3, 0, 0)
|
||||
|| requirements.maxApiVersionSupported < XR_MAKE_VERSION(3, 0, 0))
|
||||
throw std::runtime_error("Runtime does not support GLES 3.0");
|
||||
display_ = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
if (!eglInitialize(display_, nullptr, nullptr)) throw std::runtime_error("eglInitialize failed");
|
||||
const EGLint attributes[] = {EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_SURFACE_TYPE, EGL_PBUFFER_BIT | EGL_WINDOW_BIT,
|
||||
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_DEPTH_SIZE, 24, EGL_NONE};
|
||||
EGLConfig config{};
|
||||
EGLint found = 0;
|
||||
if (!eglChooseConfig(display_, attributes, &config, 1, &found) || found != 1)
|
||||
throw std::runtime_error("EGL config unavailable");
|
||||
const EGLint contextAttributes[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
|
||||
context_ = eglCreateContext(display_, config, EGL_NO_CONTEXT, contextAttributes);
|
||||
const EGLint surfaceAttributes[] = {EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE};
|
||||
surface_ = eglCreatePbufferSurface(display_, config, surfaceAttributes);
|
||||
if (!eglMakeCurrent(display_, surface_, surface_, context_)) throw std::runtime_error("eglMakeCurrent failed");
|
||||
XrGraphicsBindingOpenGLESAndroidKHR binding{XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR};
|
||||
binding.display = display_; binding.config = config; binding.context = context_;
|
||||
XrSessionCreateInfo sessionInfo{XR_TYPE_SESSION_CREATE_INFO};
|
||||
sessionInfo.next = &binding; sessionInfo.systemId = system_;
|
||||
check(xrCreateSession(instance_, &sessionInfo, &session_), "xrCreateSession");
|
||||
XrReferenceSpaceCreateInfo reference{XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
||||
reference.poseInReferenceSpace.orientation.w = 1;
|
||||
reference.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE;
|
||||
XrResult referenceResult = xrCreateReferenceSpace(session_, &reference, &world_);
|
||||
if (referenceResult == XR_ERROR_REFERENCE_SPACE_UNSUPPORTED) {
|
||||
reference.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL;
|
||||
reference.poseInReferenceSpace.position.y = -1.6f;
|
||||
check(xrCreateReferenceSpace(session_, &reference, &world_), "local standing space");
|
||||
} else check(referenceResult, "stage space");
|
||||
reference.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW;
|
||||
reference.poseInReferenceSpace.position.y = 0;
|
||||
check(xrCreateReferenceSpace(session_, &reference, &view_), "view space");
|
||||
initializeActions();
|
||||
initializeImages();
|
||||
initializeGraphics();
|
||||
initializeTracking();
|
||||
{
|
||||
std::string filename = std::string(app_->activity->internalDataPath) + "/.wayland-rt/" DROIDDECK_XR_FRAME_SOCKET;
|
||||
if (!receiver_.Start(filename)) throw std::runtime_error("XR frame receiver unavailable");
|
||||
bool gpu = !verifyPixels_ && gpuFrames_.Start(std::string(app_->activity->internalDataPath) + "/.wayland-rt/" DROIDDECK_XR_GPU_SOCKET, display_);
|
||||
__android_log_print(ANDROID_LOG_INFO, "DroidDeckXR", "GPU frame channel available=%d", gpu);
|
||||
}
|
||||
health();
|
||||
}
|
||||
|
||||
void initializeActions() {
|
||||
hands_[0] = path("/user/hand/left"); hands_[1] = path("/user/hand/right");
|
||||
XrActionSetCreateInfo setInfo{XR_TYPE_ACTION_SET_CREATE_INFO};
|
||||
std::strcpy(setInfo.actionSetName, "quest"); std::strcpy(setInfo.localizedActionSetName, "Quest controllers");
|
||||
check(xrCreateActionSet(instance_, &setInfo, &actions_), "action set");
|
||||
auto action = [&](const char *name, XrActionType type, XrAction *handle) {
|
||||
XrActionCreateInfo info{XR_TYPE_ACTION_CREATE_INFO};
|
||||
std::strcpy(info.actionName, name); std::strcpy(info.localizedActionName, name);
|
||||
info.actionType = type; info.countSubactionPaths = 2; info.subactionPaths = hands_;
|
||||
check(xrCreateAction(actions_, &info, handle), "create action");
|
||||
};
|
||||
action("grip_pose", XR_ACTION_TYPE_POSE_INPUT, &grip_);
|
||||
action("trigger", XR_ACTION_TYPE_FLOAT_INPUT, &trigger_);
|
||||
action("squeeze", XR_ACTION_TYPE_FLOAT_INPUT, &squeeze_);
|
||||
action("stick", XR_ACTION_TYPE_VECTOR2F_INPUT, &stick_);
|
||||
action("stick_click", XR_ACTION_TYPE_BOOLEAN_INPUT, &stickClick_);
|
||||
action("primary", XR_ACTION_TYPE_BOOLEAN_INPUT, &primary_);
|
||||
action("secondary", XR_ACTION_TYPE_BOOLEAN_INPUT, &secondary_);
|
||||
action("menu", XR_ACTION_TYPE_BOOLEAN_INPUT, &menu_);
|
||||
action("trigger_touch", XR_ACTION_TYPE_BOOLEAN_INPUT, &triggerTouch_);
|
||||
action("stick_touch", XR_ACTION_TYPE_BOOLEAN_INPUT, &stickTouch_);
|
||||
action("primary_touch", XR_ACTION_TYPE_BOOLEAN_INPUT, &primaryTouch_);
|
||||
action("secondary_touch", XR_ACTION_TYPE_BOOLEAN_INPUT, &secondaryTouch_);
|
||||
action("thumbrest_touch", XR_ACTION_TYPE_BOOLEAN_INPUT, &thumbrestTouch_);
|
||||
action("vibration", XR_ACTION_TYPE_VIBRATION_OUTPUT, &vibration_);
|
||||
std::vector<XrActionSuggestedBinding> bindings;
|
||||
for (const char *hand : {"left", "right"}) {
|
||||
std::string root = std::string("/user/hand/") + hand;
|
||||
bindings.push_back({grip_, path((root + "/input/grip/pose").c_str())});
|
||||
bindings.push_back({trigger_, path((root + "/input/trigger/value").c_str())});
|
||||
bindings.push_back({triggerTouch_, path((root + "/input/trigger/touch").c_str())});
|
||||
bindings.push_back({squeeze_, path((root + "/input/squeeze/value").c_str())});
|
||||
bindings.push_back({stick_, path((root + "/input/thumbstick").c_str())});
|
||||
bindings.push_back({stickClick_, path((root + "/input/thumbstick/click").c_str())});
|
||||
bindings.push_back({stickTouch_, path((root + "/input/thumbstick/touch").c_str())});
|
||||
bindings.push_back({thumbrestTouch_, path((root + "/input/thumbrest/touch").c_str())});
|
||||
bindings.push_back({primary_, path((root + (std::strcmp(hand, "left") == 0 ? "/input/x/click" : "/input/a/click")).c_str())});
|
||||
bindings.push_back({secondary_, path((root + (std::strcmp(hand, "left") == 0 ? "/input/y/click" : "/input/b/click")).c_str())});
|
||||
bindings.push_back({primaryTouch_, path((root + (std::strcmp(hand, "left") == 0 ? "/input/x/touch" : "/input/a/touch")).c_str())});
|
||||
bindings.push_back({secondaryTouch_, path((root + (std::strcmp(hand, "left") == 0 ? "/input/y/touch" : "/input/b/touch")).c_str())});
|
||||
bindings.push_back({vibration_, path((root + "/output/haptic").c_str())});
|
||||
}
|
||||
bindings.push_back({menu_, path("/user/hand/left/input/menu/click")});
|
||||
XrInteractionProfileSuggestedBinding suggestion{XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
|
||||
suggestion.interactionProfile = path("/interaction_profiles/oculus/touch_controller");
|
||||
suggestion.countSuggestedBindings = bindings.size(); suggestion.suggestedBindings = bindings.data();
|
||||
check(xrSuggestInteractionProfileBindings(instance_, &suggestion), "Touch bindings");
|
||||
XrSessionActionSetsAttachInfo attach{XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO};
|
||||
attach.countActionSets = 1; attach.actionSets = &actions_;
|
||||
check(xrAttachSessionActionSets(session_, &attach), "attach actions");
|
||||
for (int hand = 0; hand < 2; hand++) {
|
||||
XrActionSpaceCreateInfo space{XR_TYPE_ACTION_SPACE_CREATE_INFO};
|
||||
space.action = grip_; space.subactionPath = hands_[hand]; space.poseInActionSpace.orientation.w = 1;
|
||||
check(xrCreateActionSpace(session_, &space, &handSpaces_[hand]), "controller space");
|
||||
}
|
||||
}
|
||||
|
||||
void initializeImages() {
|
||||
uint32_t count = 0;
|
||||
check(xrEnumerateViewConfigurationViews(instance_, system_, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
|
||||
0, &count, nullptr), "view count");
|
||||
if (count != 2) throw std::runtime_error("Expected two stereo views");
|
||||
std::vector<XrViewConfigurationView> views(count, {XR_TYPE_VIEW_CONFIGURATION_VIEW});
|
||||
check(xrEnumerateViewConfigurationViews(instance_, system_, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
|
||||
count, &count, views.data()), "view configuration");
|
||||
check(xrEnumerateSwapchainFormats(session_, 0, &count, nullptr), "format count");
|
||||
std::vector<int64_t> formats(count);
|
||||
check(xrEnumerateSwapchainFormats(session_, count, &count, formats.data()), "formats");
|
||||
int64_t format = 0;
|
||||
for (int64_t candidate : formats) if (candidate == GL_SRGB8_ALPHA8) format = candidate;
|
||||
if (!format) throw std::runtime_error("sRGB swapchain unavailable");
|
||||
for (int eye = 0; eye < 2; eye++) {
|
||||
Eye &target = eyes_[eye];
|
||||
target.width = views[eye].recommendedImageRectWidth;
|
||||
target.height = views[eye].recommendedImageRectHeight;
|
||||
XrSwapchainCreateInfo create{XR_TYPE_SWAPCHAIN_CREATE_INFO};
|
||||
create.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;
|
||||
create.format = format; create.sampleCount = 1;
|
||||
create.width = target.width; create.height = target.height;
|
||||
create.faceCount = 1; create.arraySize = 1; create.mipCount = 1;
|
||||
check(xrCreateSwapchain(session_, &create, &target.swapchain), "swapchain");
|
||||
check(xrEnumerateSwapchainImages(target.swapchain, 0, &count, nullptr), "image count");
|
||||
target.images.resize(count, {XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR});
|
||||
check(xrEnumerateSwapchainImages(target.swapchain, count, &count,
|
||||
reinterpret_cast<XrSwapchainImageBaseHeader *>(target.images.data())), "images");
|
||||
}
|
||||
}
|
||||
|
||||
void initializeTracking() {
|
||||
std::string directory = std::string(app_->activity->internalDataPath) + "/.wayland-rt";
|
||||
if (mkdir(directory.c_str(), 0700) != 0 && errno != EEXIST)
|
||||
throw std::runtime_error("XR tracking directory unavailable");
|
||||
auto mapFile = [&](const char *name, size_t size) {
|
||||
std::string filename = directory + "/" + name;
|
||||
int descriptor = open(filename.c_str(), O_CREAT | O_RDWR | O_CLOEXEC, 0600);
|
||||
if (descriptor < 0) throw std::runtime_error("XR IPC file unavailable");
|
||||
if (ftruncate(descriptor, size) != 0) {
|
||||
close(descriptor); throw std::runtime_error("XR IPC file size failed");
|
||||
}
|
||||
void *mapping = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, descriptor, 0);
|
||||
close(descriptor);
|
||||
if (mapping == MAP_FAILED) throw std::runtime_error("XR IPC map failed");
|
||||
return mapping;
|
||||
};
|
||||
tracking_ = static_cast<DroidDeckXrTracking *>(mapFile(DROIDDECK_XR_FILE, sizeof(DroidDeckXrTracking)));
|
||||
haptics_ = static_cast<DroidDeckXrHaptics *>(mapFile(DROIDDECK_XR_HAPTIC_FILE, sizeof(DroidDeckXrHaptics)));
|
||||
haptics_->magic = DROIDDECK_XR_HAPTIC_MAGIC; haptics_->version = DROIDDECK_XR_VERSION;
|
||||
for (int hand = 0; hand < 2; hand++) lastPulse_[hand] = haptics_->hands[hand].id;
|
||||
}
|
||||
|
||||
static DroidDeckXrPose posePacket(const XrPosef &pose, XrSpaceLocationFlags flags) {
|
||||
return {{pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w},
|
||||
{pose.position.x, pose.position.y, pose.position.z}, (uint32_t)flags};
|
||||
}
|
||||
|
||||
void publishTracking(const XrView views[2], XrTime predicted) {
|
||||
if (!tracking_) return;
|
||||
timespec now{}; clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
DroidDeckXrTracking packet{};
|
||||
packet.magic = DROIDDECK_XR_MAGIC; packet.version = DROIDDECK_XR_VERSION;
|
||||
packet.active = state_ == XR_SESSION_STATE_VISIBLE || state_ == XR_SESSION_STATE_FOCUSED;
|
||||
packet.sampled_ns = (int64_t)now.tv_sec * 1000000000LL + now.tv_nsec;
|
||||
if (convertTime_) {
|
||||
timespec displayTime{};
|
||||
check(convertTime_(instance_, predicted, &displayTime), "display time conversion");
|
||||
packet.predicted_ns = (int64_t)displayTime.tv_sec * 1000000000LL + displayTime.tv_nsec;
|
||||
}
|
||||
packet.period_ns = period_;
|
||||
packet.head = posePacket(head_.pose, head_.locationFlags);
|
||||
for (int eye = 0; eye < 2; eye++) {
|
||||
packet.eyes[eye].pose = posePacket(views[eye].pose, head_.locationFlags);
|
||||
packet.eyes[eye].fov[0] = views[eye].fov.angleLeft;
|
||||
packet.eyes[eye].fov[1] = views[eye].fov.angleRight;
|
||||
packet.eyes[eye].fov[2] = views[eye].fov.angleUp;
|
||||
packet.eyes[eye].fov[3] = views[eye].fov.angleDown;
|
||||
packet.eyes[eye].width = eyes_[eye].width; packet.eyes[eye].height = eyes_[eye].height;
|
||||
packet.hands[eye] = inputs_[eye];
|
||||
packet.hands[eye].pose = posePacket(handLocations_[eye].pose, handLocations_[eye].locationFlags);
|
||||
packet.hands[eye].active = handActive_[eye];
|
||||
}
|
||||
uint32_t sequence = __atomic_load_n(&tracking_->seq, __ATOMIC_RELAXED);
|
||||
__atomic_store_n(&tracking_->seq, sequence | 1u, __ATOMIC_SEQ_CST);
|
||||
__atomic_thread_fence(__ATOMIC_SEQ_CST);
|
||||
std::memcpy(tracking_, &packet, offsetof(DroidDeckXrTracking, seq));
|
||||
std::memcpy(reinterpret_cast<char *>(tracking_) + offsetof(DroidDeckXrTracking, active),
|
||||
reinterpret_cast<const char *>(&packet) + offsetof(DroidDeckXrTracking, active),
|
||||
sizeof(packet) - offsetof(DroidDeckXrTracking, active));
|
||||
__atomic_store_n(&tracking_->seq, (sequence | 1u) + 1u, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
void attachGuest(bool attach) {
|
||||
if (attach == guestAttached_) return;
|
||||
env_->CallVoidMethod(app_->activity->clazz, activeCallback_, static_cast<jboolean>(attach));
|
||||
if (env_->ExceptionCheck()) {
|
||||
env_->ExceptionDescribe(); env_->ExceptionClear(); throw std::runtime_error("Guest XR handshake failed");
|
||||
}
|
||||
guestAttached_ = attach;
|
||||
}
|
||||
|
||||
void pollHaptics() {
|
||||
if (!haptics_) return;
|
||||
DroidDeckXrHaptics packet{};
|
||||
uint32_t sequence = __atomic_load_n(&haptics_->seq, __ATOMIC_ACQUIRE);
|
||||
if (sequence & 1u) return;
|
||||
std::memcpy(&packet, haptics_, sizeof(packet));
|
||||
__atomic_thread_fence(__ATOMIC_ACQUIRE);
|
||||
if (__atomic_load_n(&haptics_->seq, __ATOMIC_RELAXED) != sequence
|
||||
|| packet.magic != DROIDDECK_XR_HAPTIC_MAGIC || packet.version != DROIDDECK_XR_VERSION) return;
|
||||
for (int hand = 0; hand < 2; hand++) {
|
||||
const auto &pulse = packet.hands[hand];
|
||||
if (pulse.id == lastPulse_[hand]) continue;
|
||||
lastPulse_[hand] = pulse.id;
|
||||
if (state_ != XR_SESSION_STATE_FOCUSED || !handActive_[hand]
|
||||
|| pulse.duration_ns < 1000000 || pulse.duration_ns > 1000000000
|
||||
|| !std::isfinite(pulse.amplitude) || pulse.amplitude < 0 || pulse.amplitude > 1
|
||||
|| !std::isfinite(pulse.frequency) || pulse.frequency < 0) continue;
|
||||
XrHapticActionInfo info{XR_TYPE_HAPTIC_ACTION_INFO}; info.action = vibration_; info.subactionPath = hands_[hand];
|
||||
XrHapticVibration vibration{XR_TYPE_HAPTIC_VIBRATION};
|
||||
vibration.duration = pulse.duration_ns; vibration.amplitude = pulse.amplitude; vibration.frequency = pulse.frequency;
|
||||
check(xrApplyHapticFeedback(session_, &info, reinterpret_cast<XrHapticBaseHeader *>(&vibration)), "guest haptics");
|
||||
}
|
||||
}
|
||||
|
||||
GLuint shader(GLenum type, const char *text) {
|
||||
GLuint handle = glCreateShader(type);
|
||||
glShaderSource(handle, 1, &text, nullptr); glCompileShader(handle);
|
||||
GLint success = 0; glGetShaderiv(handle, GL_COMPILE_STATUS, &success);
|
||||
if (!success) { glDeleteShader(handle); throw std::runtime_error("Shader compile failed"); }
|
||||
return handle;
|
||||
}
|
||||
|
||||
void initializeGraphics() {
|
||||
glGenFramebuffers(1, &framebuffer_);
|
||||
glGenVertexArrays(1, &vertexArray_);
|
||||
GLuint vertex = shader(GL_VERTEX_SHADER,
|
||||
"#version 300 es\nout vec2 uv; uniform float eye; void main(){vec2 coordinate=vec2(float((gl_VertexID<<1)&2),float(gl_VertexID&2));uv=vec2(coordinate.x*0.5+eye*0.5,1.0-coordinate.y);gl_Position=vec4(coordinate*2.0-1.0,0.0,1.0);}");
|
||||
GLuint fragment = shader(GL_FRAGMENT_SHADER,
|
||||
"#version 300 es\nprecision highp float; in vec2 uv; uniform sampler2D source; uniform float eye; out vec4 outputColor; void main(){float edge=0.5/float(textureSize(source,0).x);vec2 coordinate=vec2(clamp(uv.x,eye*0.5+edge,(eye+1.0)*0.5-edge),uv.y);outputColor=texture(source,coordinate);}");
|
||||
sourceProgram_ = glCreateProgram(); glAttachShader(sourceProgram_, vertex); glAttachShader(sourceProgram_, fragment);
|
||||
glLinkProgram(sourceProgram_); glDeleteShader(vertex); glDeleteShader(fragment);
|
||||
GLint linked = 0; glGetProgramiv(sourceProgram_, GL_LINK_STATUS, &linked);
|
||||
if (!linked) throw std::runtime_error("Frame shader link failed");
|
||||
glGenTextures(1, &sourceTexture_); glBindTexture(GL_TEXTURE_2D, sourceTexture_);
|
||||
cpuTexture_ = sourceTexture_;
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
|
||||
void receiveFrame() {
|
||||
if (gpuFrames_.Take(sourceHeader_, sourceTexture_)) {
|
||||
uploadedFrames_++;
|
||||
if (uploadedFrames_ % 72 == 1) health();
|
||||
return;
|
||||
}
|
||||
DroidDeckXrPixels frame;
|
||||
if (!receiver_.Take(&frame)) return;
|
||||
GLint maximum = 0; glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maximum);
|
||||
if (frame.header.width > static_cast<uint32_t>(maximum) || frame.header.height > static_cast<uint32_t>(maximum))
|
||||
throw std::runtime_error("XR source exceeds GPU texture limit");
|
||||
sourceTexture_ = cpuTexture_;
|
||||
glBindTexture(GL_TEXTURE_2D, sourceTexture_);
|
||||
GLenum encoding = frame.header.color_space == DROIDDECK_XR_COLOR_SRGB_RGBA8 ? GL_SRGB8_ALPHA8 : GL_RGBA8;
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, encoding, frame.header.width, frame.header.height, 0,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, frame.pixels.data());
|
||||
if (verifyPixels_) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, 0);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, sourceTexture_, 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
throw std::runtime_error("Frame readback framebuffer incomplete");
|
||||
std::vector<unsigned char> readback(frame.header.bytes);
|
||||
glReadPixels(0, 0, frame.header.width, frame.header.height, GL_RGBA, GL_UNSIGNED_BYTE, readback.data());
|
||||
if (readback != frame.pixels) throw std::runtime_error("Frame GPU readback does not match owned pixels");
|
||||
gpuChecksum_ = 2166136261u;
|
||||
for (unsigned char value : readback) gpuChecksum_ = (gpuChecksum_ ^ value) * 16777619u;
|
||||
verifiedFrame_ = frame.header.frame_id;
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
if (glGetError() != GL_NO_ERROR) throw std::runtime_error("Frame texture upload failed");
|
||||
sourceHeader_ = frame.header;
|
||||
receiver_.Recycle(std::move(frame.pixels));
|
||||
uploadedFrames_++;
|
||||
if (verifyPixels_ || uploadedFrames_ % 72 == 1) health();
|
||||
}
|
||||
|
||||
void render(Eye &eye, uint32_t image) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, eye.images[image].image, 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
throw std::runtime_error("Incomplete eye framebuffer");
|
||||
{
|
||||
glViewport(0, 0, eye.width, eye.height); glDisable(GL_DEPTH_TEST);
|
||||
glUseProgram(sourceProgram_); glBindVertexArray(vertexArray_);
|
||||
glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, sourceTexture_);
|
||||
glUniform1i(glGetUniformLocation(sourceProgram_, "source"), 0);
|
||||
glUniform1f(glGetUniformLocation(sourceProgram_, "eye"), &eye == &eyes_[1] ? 1.f : 0.f);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (glGetError() != GL_NO_ERROR) throw std::runtime_error("OpenGL render error");
|
||||
glFlush();
|
||||
}
|
||||
|
||||
void pollEvents() {
|
||||
XrEventDataBuffer event{XR_TYPE_EVENT_DATA_BUFFER};
|
||||
XrResult result;
|
||||
while ((result = xrPollEvent(instance_, &event)) == XR_SUCCESS) {
|
||||
if (event.type == XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) {
|
||||
state_ = reinterpret_cast<XrEventDataSessionStateChanged *>(&event)->state;
|
||||
__android_log_print(ANDROID_LOG_INFO, "DroidDeckXR", "session state=%d", state_);
|
||||
attachGuest(state_ == XR_SESSION_STATE_VISIBLE || state_ == XR_SESSION_STATE_FOCUSED);
|
||||
if (state_ == XR_SESSION_STATE_READY) {
|
||||
XrSessionBeginInfo begin{XR_TYPE_SESSION_BEGIN_INFO};
|
||||
begin.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
||||
check(xrBeginSession(session_, &begin), "xrBeginSession"); running_ = true;
|
||||
if (setPerformance_) {
|
||||
for (auto domain : {XR_PERF_SETTINGS_DOMAIN_CPU_EXT, XR_PERF_SETTINGS_DOMAIN_GPU_EXT}) {
|
||||
XrResult request = setPerformance_(session_, domain, XR_PERF_SETTINGS_LEVEL_SUSTAINED_HIGH_EXT);
|
||||
__android_log_print(ANDROID_LOG_INFO, "DroidDeckXR", "sustained performance domain=%d result=%d", domain, request);
|
||||
}
|
||||
}
|
||||
} else if (state_ == XR_SESSION_STATE_STOPPING) {
|
||||
check(xrEndSession(session_), "xrEndSession"); running_ = false;
|
||||
if (quitRequested_) exit_ = true;
|
||||
} else if (state_ == XR_SESSION_STATE_EXITING || state_ == XR_SESSION_STATE_LOSS_PENDING) exit_ = true;
|
||||
health();
|
||||
} else if (event.type == XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING) exit_ = true;
|
||||
else if (event.type == XR_TYPE_EVENT_DATA_PERF_SETTINGS_EXT) {
|
||||
const auto &performance = *reinterpret_cast<XrEventDataPerfSettingsEXT *>(&event);
|
||||
__android_log_print(ANDROID_LOG_INFO, "DroidDeckXR", "performance domain=%d subdomain=%d level=%d->%d",
|
||||
performance.domain, performance.subDomain, performance.fromLevel, performance.toLevel);
|
||||
}
|
||||
event = {XR_TYPE_EVENT_DATA_BUFFER};
|
||||
}
|
||||
check(result, "xrPollEvent");
|
||||
}
|
||||
|
||||
void frame() {
|
||||
XrFrameWaitInfo wait{XR_TYPE_FRAME_WAIT_INFO};
|
||||
XrFrameState frameState{XR_TYPE_FRAME_STATE};
|
||||
check(xrWaitFrame(session_, &wait, &frameState), "xrWaitFrame");
|
||||
period_ = frameState.predictedDisplayPeriod;
|
||||
XrFrameBeginInfo begin{XR_TYPE_FRAME_BEGIN_INFO}; check(xrBeginFrame(session_, &begin), "xrBeginFrame");
|
||||
check(xrLocateSpace(view_, world_, frameState.predictedDisplayTime, &head_), "head pose");
|
||||
if (state_ == XR_SESSION_STATE_FOCUSED) {
|
||||
XrActiveActionSet active{actions_, XR_NULL_PATH};
|
||||
XrActionsSyncInfo sync{XR_TYPE_ACTIONS_SYNC_INFO}; sync.countActiveActionSets = 1; sync.activeActionSets = &active;
|
||||
check(xrSyncActions(session_, &sync), "xrSyncActions");
|
||||
for (int hand = 0; hand < 2; hand++) {
|
||||
XrActionStateGetInfo get{XR_TYPE_ACTION_STATE_GET_INFO}; get.action = grip_; get.subactionPath = hands_[hand];
|
||||
XrActionStatePose pose{XR_TYPE_ACTION_STATE_POSE}; check(xrGetActionStatePose(session_, &get, &pose), "grip state");
|
||||
handActive_[hand] = pose.isActive;
|
||||
check(xrLocateSpace(handSpaces_[hand], world_, frameState.predictedDisplayTime, &handLocations_[hand]), "hand pose");
|
||||
get.action = trigger_;
|
||||
XrActionStateFloat trigger{XR_TYPE_ACTION_STATE_FLOAT}; check(xrGetActionStateFloat(session_, &get, &trigger), "trigger state");
|
||||
inputs_[hand].trigger = trigger.isActive ? trigger.currentState : 0;
|
||||
get.action = squeeze_;
|
||||
XrActionStateFloat squeeze{XR_TYPE_ACTION_STATE_FLOAT}; check(xrGetActionStateFloat(session_, &get, &squeeze), "squeeze state");
|
||||
inputs_[hand].squeeze = squeeze.isActive ? squeeze.currentState : 0;
|
||||
get.action = stick_;
|
||||
XrActionStateVector2f stick{XR_TYPE_ACTION_STATE_VECTOR2F}; check(xrGetActionStateVector2f(session_, &get, &stick), "stick state");
|
||||
inputs_[hand].stick[0] = stick.isActive ? stick.currentState.x : 0;
|
||||
inputs_[hand].stick[1] = stick.isActive ? stick.currentState.y : 0;
|
||||
inputs_[hand].buttons = 0;
|
||||
const std::pair<XrAction, uint32_t> buttons[] = {
|
||||
{stickClick_, DROIDDECK_XR_STICK_CLICK}, {primary_, DROIDDECK_XR_PRIMARY_CLICK},
|
||||
{secondary_, DROIDDECK_XR_SECONDARY_CLICK}, {menu_, DROIDDECK_XR_MENU_CLICK},
|
||||
{triggerTouch_, DROIDDECK_XR_TRIGGER_TOUCH}, {stickTouch_, DROIDDECK_XR_STICK_TOUCH},
|
||||
{primaryTouch_, DROIDDECK_XR_PRIMARY_TOUCH}, {secondaryTouch_, DROIDDECK_XR_SECONDARY_TOUCH},
|
||||
{thumbrestTouch_, DROIDDECK_XR_THUMBREST_TOUCH}};
|
||||
for (const auto &button : buttons) {
|
||||
get.action = button.first;
|
||||
XrActionStateBoolean value{XR_TYPE_ACTION_STATE_BOOLEAN}; check(xrGetActionStateBoolean(session_, &get, &value), "button state");
|
||||
if (value.isActive && value.currentState) inputs_[hand].buttons |= button.second;
|
||||
}
|
||||
}
|
||||
} else handActive_[0] = handActive_[1] = false;
|
||||
pollHaptics();
|
||||
XrViewLocateInfo locate{XR_TYPE_VIEW_LOCATE_INFO}; locate.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
||||
locate.displayTime = frameState.predictedDisplayTime; locate.space = world_;
|
||||
XrViewState viewState{XR_TYPE_VIEW_STATE}; uint32_t count = 0;
|
||||
XrView views[2]{{XR_TYPE_VIEW}, {XR_TYPE_VIEW}};
|
||||
check(xrLocateViews(session_, &locate, &viewState, 2, &count, views), "xrLocateViews");
|
||||
publishTracking(views, frameState.predictedDisplayTime);
|
||||
if (guestAttached_) {
|
||||
timespec now{}; clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
env_->CallVoidMethod(app_->activity->clazz, frameCallback_, (jlong)now.tv_sec * 1000000000LL + now.tv_nsec);
|
||||
if (env_->ExceptionCheck()) { env_->ExceptionClear(); throw std::runtime_error("Guest frame callback failed"); }
|
||||
}
|
||||
XrCompositionLayerProjectionView projection[2]{{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}, {XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}};
|
||||
bool valid = frameState.shouldRender && count == 2 &&
|
||||
(viewState.viewStateFlags & (XR_VIEW_STATE_ORIENTATION_VALID_BIT | XR_VIEW_STATE_POSITION_VALID_BIT))
|
||||
== (XR_VIEW_STATE_ORIENTATION_VALID_BIT | XR_VIEW_STATE_POSITION_VALID_BIT);
|
||||
{
|
||||
valid = valid && sourceHeader_.frame_id && xr_frame_now() - sourceHeader_.captured_ns < 500000000LL;
|
||||
if (valid) for (int eye = 0; eye < 2; eye++) {
|
||||
auto source = xr_frame_view(sourceHeader_, eye);
|
||||
views[eye].pose.orientation = {source.orientation[0], source.orientation[1], source.orientation[2], source.orientation[3]};
|
||||
views[eye].pose.position = {source.position[0], source.position[1], source.position[2]};
|
||||
views[eye].fov = {source.fov[0], source.fov[1], source.fov[2], source.fov[3]};
|
||||
}
|
||||
}
|
||||
const std::string capture = captureRequest();
|
||||
bool captured = valid;
|
||||
if (!capture.empty() && valid)
|
||||
captured = captureTexture(sourceTexture_, sourceHeader_.width, sourceHeader_.height, "source");
|
||||
if (valid) for (int eye = 0; eye < 2; eye++) {
|
||||
uint32_t image = 0;
|
||||
XrSwapchainImageAcquireInfo acquire{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
|
||||
check(xrAcquireSwapchainImage(eyes_[eye].swapchain, &acquire, &image), "acquire image");
|
||||
XrSwapchainImageWaitInfo imageWait{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; imageWait.timeout = XR_INFINITE_DURATION;
|
||||
check(xrWaitSwapchainImage(eyes_[eye].swapchain, &imageWait), "wait image");
|
||||
render(eyes_[eye], image);
|
||||
if (!capture.empty()) captured = captureTexture(eyes_[eye].images[image].image, eyes_[eye].width, eyes_[eye].height, eye ? "right" : "left") && captured;
|
||||
XrSwapchainImageReleaseInfo release{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
|
||||
check(xrReleaseSwapchainImage(eyes_[eye].swapchain, &release), "release image");
|
||||
projection[eye].pose = views[eye].pose; projection[eye].fov = views[eye].fov;
|
||||
projection[eye].subImage.swapchain = eyes_[eye].swapchain;
|
||||
projection[eye].subImage.imageRect.extent = {eyes_[eye].width, eyes_[eye].height};
|
||||
}
|
||||
XrCompositionLayerProjection layer{XR_TYPE_COMPOSITION_LAYER_PROJECTION};
|
||||
layer.space = world_; layer.viewCount = 2; layer.views = projection;
|
||||
const XrCompositionLayerBaseHeader *layers[] = {reinterpret_cast<XrCompositionLayerBaseHeader *>(&layer)};
|
||||
XrFrameEndInfo end{XR_TYPE_FRAME_END_INFO}; end.displayTime = frameState.predictedDisplayTime;
|
||||
end.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; end.layerCount = valid ? 1 : 0; end.layers = layers;
|
||||
check(xrEndFrame(session_, &end), "xrEndFrame");
|
||||
if (!capture.empty()) captureResult(capture, captured, valid, projection, frameState.predictedDisplayTime);
|
||||
frames_++; if (valid) rendered_++;
|
||||
if (frames_ % 72 == 0) {
|
||||
health();
|
||||
__android_log_print(ANDROID_LOG_INFO, "DroidDeckXR", "frames=%llu rendered=%llu head=%llu hands=%d,%d period=%lld",
|
||||
(unsigned long long)frames_, (unsigned long long)rendered_, (unsigned long long)head_.locationFlags,
|
||||
handActive_[0], handActive_[1], (long long)period_);
|
||||
}
|
||||
}
|
||||
|
||||
void cleanup() {
|
||||
receiver_.Stop();
|
||||
gpuFrames_.Stop();
|
||||
try { attachGuest(false); } catch (const std::exception &error) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "DroidDeckXR", "cleanup: %s", error.what());
|
||||
}
|
||||
if (tracking_) {
|
||||
__atomic_store_n(&tracking_->active, 0u, __ATOMIC_RELEASE);
|
||||
munmap(tracking_, sizeof(DroidDeckXrTracking));
|
||||
}
|
||||
if (haptics_) munmap(haptics_, sizeof(DroidDeckXrHaptics));
|
||||
if (display_ != EGL_NO_DISPLAY && context_ != EGL_NO_CONTEXT) {
|
||||
glDeleteFramebuffers(1, &framebuffer_); glDeleteVertexArrays(1, &vertexArray_);
|
||||
glDeleteTextures(1, &cpuTexture_); glDeleteProgram(sourceProgram_);
|
||||
}
|
||||
for (auto &eye : eyes_) if (eye.swapchain) xrDestroySwapchain(eye.swapchain);
|
||||
for (auto space : handSpaces_) if (space) xrDestroySpace(space);
|
||||
if (view_) xrDestroySpace(view_); if (world_) xrDestroySpace(world_);
|
||||
if (session_) xrDestroySession(session_);
|
||||
if (actions_) xrDestroyActionSet(actions_);
|
||||
if (instance_) xrDestroyInstance(instance_);
|
||||
if (display_ != EGL_NO_DISPLAY) {
|
||||
eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
if (surface_ != EGL_NO_SURFACE) eglDestroySurface(display_, surface_);
|
||||
if (context_ != EGL_NO_CONTEXT) eglDestroyContext(display_, context_);
|
||||
eglTerminate(display_);
|
||||
}
|
||||
if (env_) app_->activity->vm->DetachCurrentThread();
|
||||
}
|
||||
};
|
||||
|
||||
void android_main(android_app *app) {
|
||||
QuestXR runtime(app);
|
||||
try { runtime.run(); }
|
||||
catch (const std::exception &error) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "DroidDeckXR", "%s", error.what());
|
||||
runtime.health(error.what());
|
||||
ANativeActivity_finish(app->activity);
|
||||
}
|
||||
}
|
||||
@@ -657,6 +657,7 @@ static void take_pixel(struct surface *s, const uint8_t bgra[4], struct wl_resou
|
||||
static struct surface *g_hud_surface;
|
||||
static int64_t g_hud_last_ns, g_hud_alive_ns;
|
||||
static int g_hud_fresh;
|
||||
static uint64_t g_game_frames;
|
||||
extern void droiddeck_on_game_surface(const char *window, const char *gpu);
|
||||
extern void droiddeck_on_game_frame(void);
|
||||
extern void droiddeck_on_game_program(int pid, const char *program);
|
||||
@@ -740,10 +741,12 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re
|
||||
}
|
||||
if (s == g_hud_surface) {
|
||||
g_hud_alive_ns = now_ns();
|
||||
if (!repeat) { g_hud_last_ns = g_hud_alive_ns; g_hud_fresh = 1; droiddeck_on_game_frame(); }
|
||||
if (!repeat) { g_hud_last_ns = g_hud_alive_ns; g_hud_fresh = 1; g_game_frames++; droiddeck_on_game_frame(); }
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t droiddeck_game_frames(void) { return g_game_frames; }
|
||||
|
||||
struct dmabuf_buffer *droiddeck_dmabuf_from_resource(struct wl_resource *buffer) { return get_dmabuf(buffer); }
|
||||
int droiddeck_dmabuf_fd(const struct dmabuf_buffer *b) { return b && b->n_planes > 0 ? b->fd[0] : -1; }
|
||||
void droiddeck_dmabuf_size(const struct dmabuf_buffer *b, int *w, int *h) { *w = b->width; *h = b->height; }
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <android/log.h>
|
||||
|
||||
void droiddeck_log(const char *tag, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
|
||||
uint64_t droiddeck_game_frames(void);
|
||||
#define FGLOG(...) droiddeck_log("framegen", __VA_ARGS__)
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "DroidDeckWayland", __VA_ARGS__)
|
||||
|
||||
@@ -372,7 +373,11 @@ int vkp_framegen_run(VkCommandBuffer cmd, VkImage scene, VkImageView scene_view,
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t n_gen = fge_plan(g_ring_n, ++g_source_frames, g_presented_rate);
|
||||
/* The pacer's source rate must be the game's, not this loop's: a loop slowed by its own
|
||||
* generations would otherwise read as a slower game and justify even more generations. */
|
||||
++g_source_frames;
|
||||
const uint64_t game_frames = droiddeck_game_frames();
|
||||
uint32_t n_gen = fge_plan(g_ring_n, game_frames ? game_frames : g_source_frames, g_presented_rate);
|
||||
if (n_gen > g_ring_n) n_gen = g_ring_n;
|
||||
g_planned = n_gen;
|
||||
|
||||
|
||||
@@ -138,6 +138,9 @@ static pthread_mutex_t g_req_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static ANativeWindow *g_req_window;
|
||||
static int g_req_pending;
|
||||
static int g_req_detached;
|
||||
static uint64_t g_req_generation, g_req_applied;
|
||||
static pthread_cond_t g_req_condition = PTHREAD_COND_INITIALIZER;
|
||||
|
||||
|
||||
static volatile int g_mode = VKP_MODE_STRETCH, g_align = VKP_ALIGN_CENTER;
|
||||
|
||||
@@ -218,26 +221,53 @@ void vk_present_set_window(ANativeWindow *window) {
|
||||
if (g_req_pending && g_req_window && g_req_window != window) superseded = g_req_window;
|
||||
g_req_window = window;
|
||||
g_req_pending = 1;
|
||||
g_req_generation++;
|
||||
if (!window) g_req_detached = 1;
|
||||
pthread_mutex_unlock(&g_req_lock);
|
||||
if (superseded) ANativeWindow_release(superseded);
|
||||
}
|
||||
|
||||
static void window_request_applied(uint64_t generation) {
|
||||
pthread_mutex_lock(&g_req_lock);
|
||||
g_req_applied = generation;
|
||||
pthread_cond_broadcast(&g_req_condition);
|
||||
pthread_mutex_unlock(&g_req_lock);
|
||||
}
|
||||
|
||||
int vkp_wait_window_request(int timeout_ms) {
|
||||
if (timeout_ms <= 0 || timeout_ms > 10000) return 0;
|
||||
struct timespec deadline;
|
||||
clock_gettime(CLOCK_REALTIME, &deadline);
|
||||
deadline.tv_sec += timeout_ms / 1000;
|
||||
deadline.tv_nsec += (timeout_ms % 1000) * 1000000L;
|
||||
if (deadline.tv_nsec >= 1000000000L) { deadline.tv_sec++; deadline.tv_nsec -= 1000000000L; }
|
||||
pthread_mutex_lock(&g_req_lock);
|
||||
const uint64_t generation = g_req_generation;
|
||||
int error = 0;
|
||||
while (g_req_applied < generation && error == 0)
|
||||
error = pthread_cond_timedwait(&g_req_condition, &g_req_lock, &deadline);
|
||||
int applied = g_req_applied >= generation;
|
||||
pthread_mutex_unlock(&g_req_lock);
|
||||
return applied;
|
||||
}
|
||||
|
||||
int vkp_apply_window_request(void) {
|
||||
ANativeWindow *w;
|
||||
pthread_mutex_lock(&g_req_lock);
|
||||
if (!g_req_pending) { pthread_mutex_unlock(&g_req_lock); return 0; }
|
||||
w = g_req_window;
|
||||
const int detached = g_req_detached;
|
||||
const uint64_t generation = g_req_generation;
|
||||
g_req_window = NULL;
|
||||
g_req_pending = 0;
|
||||
g_req_detached = 0;
|
||||
pthread_mutex_unlock(&g_req_lock);
|
||||
if (w == g_window) {
|
||||
if (w) ANativeWindow_release(w);
|
||||
if (!w || !detached) return 0;
|
||||
if (!w || !detached) { window_request_applied(generation); return 0; }
|
||||
sc_layer_window_gone();
|
||||
destroy_swapchain();
|
||||
window_request_applied(generation);
|
||||
droiddeck_log("gpu", "screen surface rebound: rebuilding the swapchain at its new size");
|
||||
return 1;
|
||||
}
|
||||
@@ -248,6 +278,7 @@ int vkp_apply_window_request(void) {
|
||||
g_window = w;
|
||||
g_swap_retry_at_ns = 0;
|
||||
g_swap_fail_logged = 0;
|
||||
window_request_applied(generation);
|
||||
droiddeck_log("gpu", w ? "screen surface attached" : "screen surface gone: presenting paused");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ const char *vkp_gpu_name(void);
|
||||
|
||||
int vkp_ready(void);
|
||||
int vkp_has_window(void);
|
||||
int vkp_wait_window_request(int timeout_ms);
|
||||
int vkp_device_lost(void);
|
||||
|
||||
int vkp_render(int scene_w, int scene_h, const struct vkp_draw *draws, int n);
|
||||
|
||||
@@ -592,6 +592,12 @@ static char *dup_bytes(JNIEnv *env, jbyteArray arr, int *len) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeWaitSurface(
|
||||
JNIEnv *env, jclass clazz, jint timeout_ms) {
|
||||
return vkp_wait_window_request(timeout_ms) ? JNI_TRUE : JNI_FALSE;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetClipboardText(JNIEnv *env, jclass clazz, jbyteArray utf8) {
|
||||
int len;
|
||||
|
||||
@@ -20,6 +20,7 @@ class App : Application() {
|
||||
com.droiddeck.launcher.session.QuestExperimental.initialize(this)
|
||||
com.droiddeck.launcher.session.SessionPrefs.settleGpuDriverMode(this)
|
||||
com.droiddeck.launcher.update.AppUpdates.init(this)
|
||||
com.droiddeck.launcher.core.DownloadSource.init(this)
|
||||
GpuClockPin.clearLeftover(this)
|
||||
BwrapSpawner.start(this)
|
||||
com.droiddeck.launcher.frontend.GameFileSync.start(this)
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import com.droiddeck.launcher.gpu.AutoDriverTrial
|
||||
import com.droiddeck.launcher.gpu.DriverBundle
|
||||
import com.droiddeck.launcher.gpu.DriverPairs
|
||||
import com.droiddeck.launcher.gpu.GpuInfo
|
||||
@@ -21,7 +22,12 @@ import com.droiddeck.launcher.gpu.TurnipReleases
|
||||
import com.droiddeck.launcher.session.SessionService
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.session.QuestExperimental
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
import com.droiddeck.launcher.ui.DriverRow
|
||||
import com.droiddeck.launcher.ui.DriverSlot
|
||||
import com.droiddeck.launcher.ui.GPU_MODE_AUTO
|
||||
import com.droiddeck.launcher.ui.GPU_MODE_MIX
|
||||
import com.droiddeck.launcher.ui.GPU_MODE_PAIR
|
||||
import com.droiddeck.launcher.ui.GpuDriversState
|
||||
import com.droiddeck.launcher.ui.PairRow
|
||||
import com.droiddeck.launcher.wayland.CompositorHost
|
||||
@@ -42,7 +48,18 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
var pairPercent by mutableIntStateOf(-1)
|
||||
var autoStatus by mutableStateOf("")
|
||||
var activeBundle by mutableStateOf<String?>(null)
|
||||
private var autoCheckedThisProcess = false
|
||||
private var lastAutoCheck = 0L
|
||||
private var pairModeOpen by mutableStateOf(false)
|
||||
var restartPending by mutableStateOf(false)
|
||||
private set
|
||||
private var runtimePending by mutableStateOf(false)
|
||||
private var sessionRunning by mutableStateOf(false)
|
||||
|
||||
val uiMode: String get() = when (mode) {
|
||||
SessionPrefs.GPU_DRIVERS_AUTO -> GPU_MODE_AUTO
|
||||
SessionPrefs.GPU_DRIVERS_MIX -> GPU_MODE_MIX
|
||||
else -> if (pairModeOpen || pairRows.any { it.active }) GPU_MODE_PAIR else GPU_MODE_MIX
|
||||
}
|
||||
|
||||
fun state() = GpuDriversState(
|
||||
gpuName = gpu.displayName(activity), gpuFamily = gpu.family.label(activity), soc = gpu.soc, supportText = gpu.supportText(activity),
|
||||
@@ -52,7 +69,56 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
linuxRows = linuxRows, linuxSelected = linuxSelected, androidRows = androidRows, androidSelected = androidSelected,
|
||||
linuxDownloads = linuxDownloads, androidDownloads = androidDownloads, canRestoreBundled = canRestoreBundled,
|
||||
activeBundle = activeBundle,
|
||||
mode = uiMode, display = displaySlot(), runtime = runtimeSlot(),
|
||||
canRestart = !sessionRunning,
|
||||
checkedAt = TurnipReleases.cached(activity)?.checkedAt ?: 0L,
|
||||
)
|
||||
|
||||
private fun slotSource(chosenDefault: Boolean, defaultLabel: Int): String = activity.getString(when {
|
||||
uiMode == GPU_MODE_AUTO -> R.string.gpu_src_auto
|
||||
uiMode == GPU_MODE_PAIR && pairRows.any { it.active } -> R.string.gpu_src_pair
|
||||
activeBundle != null -> R.string.gpu_src_bundle
|
||||
chosenDefault -> defaultLabel
|
||||
else -> R.string.gpu_src_chosen
|
||||
})
|
||||
|
||||
private fun displaySlot(): DriverSlot {
|
||||
val row = androidRows.firstOrNull { it.id == androidSelected }
|
||||
val live = androidRows.firstOrNull { it.id == CompositorHost.displayChoice }?.name
|
||||
return DriverSlot(
|
||||
name = row?.name ?: activity.getString(R.string.gpu_auto_by_gpu),
|
||||
source = slotSource(androidSelected == TurnipDriver.AUTO, R.string.gpu_src_by_gpu),
|
||||
pending = restartPending,
|
||||
status = when {
|
||||
restartPending -> activity.getString(R.string.gpu_display_pending, live ?: activity.getString(R.string.gpu_previous_driver))
|
||||
CompositorHost.isStarted -> activity.getString(R.string.gpu_display_live)
|
||||
else -> activity.getString(R.string.gpu_display_next)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun runtimeSlot(): DriverSlot {
|
||||
val row = linuxRows.firstOrNull { it.id == linuxSelected }
|
||||
val live = SessionState.runtimeDriver?.let { id -> linuxRows.firstOrNull { it.id == id }?.name }
|
||||
return DriverSlot(
|
||||
name = row?.name ?: activity.getString(R.string.gpu_runtime_default),
|
||||
source = slotSource(linuxSelected.isEmpty(), R.string.gpu_src_default),
|
||||
pending = runtimePending,
|
||||
status = when {
|
||||
runtimePending -> activity.getString(R.string.gpu_runtime_pending, live ?: activity.getString(R.string.gpu_previous_driver))
|
||||
sessionRunning -> activity.getString(R.string.gpu_runtime_live)
|
||||
else -> activity.getString(R.string.gpu_runtime_hint)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun restartApp() {
|
||||
if (SessionState.running) return
|
||||
val launch = activity.packageManager.getLaunchIntentForPackage(activity.packageName) ?: return
|
||||
activity.startActivity(android.content.Intent.makeRestartActivityTask(launch.component)
|
||||
.putExtra(com.droiddeck.launcher.stores.download.StoreDownloadService.EXTRA_NAV, "components"))
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
var androidRows by mutableStateOf<List<DriverRow>>(emptyList())
|
||||
var androidSelected by mutableStateOf("")
|
||||
|
||||
@@ -90,14 +156,23 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
canRestoreBundled = td.hiddenBundled().isNotEmpty()
|
||||
androidSelected = SessionPrefs.androidDriver(activity)
|
||||
activeBundle = DriverBundle.active(activity)?.label
|
||||
restartPending = CompositorHost.isStarted && CompositorHost.displayChoice.let { it != null && it != androidSelected }
|
||||
sessionRunning = SessionState.running
|
||||
runtimePending = sessionRunning && SessionState.runtimeDriver.let { it != null && it != linuxSelected }
|
||||
refreshReleaseRows()
|
||||
refreshPairs()
|
||||
}
|
||||
|
||||
fun summary(): String {
|
||||
val active = pairRows.firstOrNull { it.active }?.name ?: activeBundle
|
||||
val label = activity.getString(if (mode == SessionPrefs.GPU_DRIVERS_AUTO) R.string.common_auto else R.string.drivers_manual)
|
||||
return active?.let { activity.getString(R.string.drivers_summary, label, it) } ?: label
|
||||
val display = displaySlot().name
|
||||
val base = when {
|
||||
uiMode == GPU_MODE_AUTO -> activity.getString(R.string.drivers_summary, activity.getString(R.string.common_auto), active ?: display)
|
||||
uiMode == GPU_MODE_PAIR && active != null -> activity.getString(R.string.drivers_summary, activity.getString(R.string.gpu_mode_pair_short), active)
|
||||
else -> activity.getString(R.string.drivers_summary, activity.getString(R.string.gpu_mode_mix_short),
|
||||
runtimeSlot().name.let { if (it == display) it else activity.getString(R.string.gpu_summary_mix, display, it) })
|
||||
}
|
||||
return if (restartPending) activity.getString(R.string.gpu_summary_restart, base) else base
|
||||
}
|
||||
|
||||
fun refreshPairs() {
|
||||
@@ -121,18 +196,48 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
}.sortedByDescending { it.recommended }
|
||||
}
|
||||
|
||||
fun setMode(auto: Boolean) {
|
||||
mode = if (auto) SessionPrefs.GPU_DRIVERS_AUTO else SessionPrefs.GPU_DRIVERS_MANUAL
|
||||
fun chooseMode(next: String) {
|
||||
pairModeOpen = next == GPU_MODE_PAIR
|
||||
setMode(auto = next == GPU_MODE_AUTO, mix = next == GPU_MODE_MIX)
|
||||
}
|
||||
|
||||
fun setMode(auto: Boolean, mix: Boolean = false) {
|
||||
mode = when {
|
||||
auto -> SessionPrefs.GPU_DRIVERS_AUTO
|
||||
mix -> SessionPrefs.GPU_DRIVERS_MIX
|
||||
else -> SessionPrefs.GPU_DRIVERS_MANUAL
|
||||
}
|
||||
SessionPrefs.setGpuDriverMode(activity, mode)
|
||||
autoStatus = ""
|
||||
if (auto) ensureAuto(force = false)
|
||||
}
|
||||
|
||||
fun checkOnOpen() {
|
||||
if (SessionPrefs.gpuDriverMode(activity) == SessionPrefs.GPU_DRIVERS_AUTO) ensureAuto(force = false)
|
||||
else if (!releaseChecking && releasesStale() && com.droiddeck.launcher.frontend.AutoComponents.isOnline(activity)) checkLatestTurnip()
|
||||
}
|
||||
|
||||
private fun releasesStale(): Boolean {
|
||||
val cached = TurnipReleases.cached(activity)
|
||||
return cached == null || System.currentTimeMillis() - cached.checkedAt > RELEASE_CHECK_INTERVAL_MS ||
|
||||
cached.assets.none { it.pair.isNotEmpty() }
|
||||
}
|
||||
|
||||
fun ensureAuto(force: Boolean) {
|
||||
mode = SessionPrefs.gpuDriverMode(activity)
|
||||
if (mode != SessionPrefs.GPU_DRIVERS_AUTO || pairBusy != null || releaseChecking) return
|
||||
if (!force && autoCheckedThisProcess) return
|
||||
autoCheckedThisProcess = true
|
||||
if (SessionState.running) {
|
||||
if (force) autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
|
||||
return
|
||||
}
|
||||
AutoDriverTrial.rollbackDue(activity)?.let { trial ->
|
||||
AutoDriverTrial.rollBack(activity, trial)
|
||||
autoStatus = activity.getString(R.string.drivers_rolled_back, trial.name)
|
||||
refreshDrivers()
|
||||
return
|
||||
}
|
||||
if (!force && System.currentTimeMillis() - lastAutoCheck < RELEASE_CHECK_INTERVAL_MS) return
|
||||
lastAutoCheck = System.currentTimeMillis()
|
||||
val displayChanged = SessionPrefs.androidDriver(activity) != QuestExperimental.defaultAndroidDriver()
|
||||
if (QuestExperimental.applyAutoDrivers(activity)) {
|
||||
autoStatus = activity.getString(
|
||||
@@ -148,10 +253,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
releaseChecking = true
|
||||
autoStatus = activity.getString(R.string.drivers_checking_latest)
|
||||
Thread({
|
||||
val cached = TurnipReleases.cached(activity)
|
||||
val stale = cached == null || System.currentTimeMillis() - cached.checkedAt > 24 * 3_600_000L ||
|
||||
cached.assets.none { it.pair.isNotEmpty() }
|
||||
val problem = if (force || stale) runCatching { TurnipReleases.refresh(activity) }.exceptionOrNull()?.message else null
|
||||
val problem = if (force || releasesStale()) runCatching { TurnipReleases.refresh(activity) }.exceptionOrNull()?.message else null
|
||||
val holdBack = !force && com.droiddeck.launcher.frontend.AutoComponents.constrained(activity)
|
||||
ui.post {
|
||||
releaseChecking = false
|
||||
refreshReleaseRows()
|
||||
@@ -165,6 +268,10 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
?: activity.getString(R.string.upd_current_status)
|
||||
pair == null || !pair.complete ->
|
||||
autoStatus = problem?.let { activity.getString(R.string.upd_couldnt_check, it) } ?: activity.getString(R.string.drivers_recommended_unpublished)
|
||||
AutoDriverTrial.release(pair) in AutoDriverTrial.bad(activity) ->
|
||||
autoStatus = activity.getString(R.string.drivers_skipping_bad, pair.name, pair.version)
|
||||
SessionState.running -> autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
|
||||
holdBack && row?.installed != true -> autoStatus = activity.getString(R.string.drivers_auto_waits_for_network, pair.name, pair.version)
|
||||
else -> installPair(pair, auto = true)
|
||||
}
|
||||
}
|
||||
@@ -174,6 +281,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
fun selectPair(key: String) {
|
||||
if (pairBusy != null) return
|
||||
val pair = DriverPairs.from(TurnipReleases.cached(activity)).firstOrNull { it.key == key && it.complete } ?: return
|
||||
pairModeOpen = true
|
||||
if (mode != SessionPrefs.GPU_DRIVERS_MANUAL) setMode(auto = false)
|
||||
installPair(pair, auto = false)
|
||||
}
|
||||
|
||||
@@ -183,8 +292,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
pairPercent = 0
|
||||
if (auto) autoStatus = activity.getString(R.string.drivers_downloading_pair, pair.name, pair.version)
|
||||
Thread({
|
||||
val lm = LinuxVulkanDriverManager(activity)
|
||||
val td = TurnipDriver(activity)
|
||||
var downloaded = false
|
||||
val result = runCatching {
|
||||
val zips = pair.assets
|
||||
@@ -198,22 +305,27 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
pairBusy = null
|
||||
pairPercent = -1
|
||||
result.onSuccess { (displayId, linuxId) ->
|
||||
val displayChanged = SessionPrefs.androidDriver(activity) != displayId
|
||||
if (auto && SessionState.running) {
|
||||
lastAutoCheck = 0L
|
||||
autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
|
||||
return@onSuccess
|
||||
}
|
||||
val previousDisplay = SessionPrefs.androidDriver(activity)
|
||||
val previousLinux = SessionPrefs.linuxDriver(activity)
|
||||
val displayChanged = previousDisplay != displayId
|
||||
SessionPrefs.setAndroidDriver(activity, displayId)
|
||||
SessionPrefs.setLinuxDriver(activity, linuxId)
|
||||
val restart = displayChanged && CompositorHost.isStarted
|
||||
if (auto) {
|
||||
val previous = SessionPrefs.gpuAutoInstalled(activity)
|
||||
for (id in previous - setOf(displayId, linuxId)) {
|
||||
val bundle = DriverBundle.containing(activity, id, lm.isInstalled(id))
|
||||
when {
|
||||
bundle != null -> { DriverBundle.remove(activity, bundle); TurnipReleases.forget(activity, bundle.id) }
|
||||
lm.isInstalled(id) -> lm.removeDriver(id)
|
||||
else -> td.remove(id)
|
||||
}
|
||||
TurnipReleases.forget(activity, id)
|
||||
}
|
||||
SessionPrefs.setGpuAutoInstalled(activity, setOf(displayId, linuxId))
|
||||
val trial = AutoDriverTrial.current(activity)
|
||||
val lastGood = trial?.let { setOf(it.previousDisplay, it.previousLinux) } ?: setOf(previousDisplay, previousLinux)
|
||||
val changed = previousDisplay != displayId || previousLinux != linuxId
|
||||
SessionPrefs.setGpuAutoInstalled(activity, SessionPrefs.gpuAutoInstalled(activity) + setOf(displayId, linuxId))
|
||||
if (changed) AutoDriverTrial.begin(
|
||||
activity, AutoDriverTrial.release(pair), "${pair.name} ${pair.version}", displayId, linuxId,
|
||||
trial?.previousDisplay ?: previousDisplay, trial?.previousLinux ?: previousLinux,
|
||||
)
|
||||
if (!SessionState.running) AutoDriverTrial.prune(activity, lastGood + setOf(displayId, linuxId))
|
||||
autoStatus = activity.getString(
|
||||
if (downloaded) { if (restart) R.string.drivers_updated_to_restart else R.string.drivers_updated_to }
|
||||
else { if (restart) R.string.drivers_switched_to_restart else R.string.drivers_switched_to },
|
||||
@@ -304,14 +416,15 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
linux -> SessionPrefs.setLinuxDriver(activity, id)
|
||||
else -> SessionPrefs.setAndroidDriver(activity, id)
|
||||
}
|
||||
setMode(false)
|
||||
pairModeOpen = false
|
||||
setMode(auto = false, mix = true)
|
||||
refreshDrivers()
|
||||
}
|
||||
|
||||
private fun useBundle(bundle: DriverBundle.Bundle): String {
|
||||
val displayChanged = SessionPrefs.androidDriver(activity) != bundle.androidId
|
||||
DriverBundle.select(activity, bundle)
|
||||
if (mode == SessionPrefs.GPU_DRIVERS_AUTO) setMode(false)
|
||||
if (mode == SessionPrefs.GPU_DRIVERS_AUTO) setMode(auto = false, mix = true)
|
||||
return activity.getString(
|
||||
if (displayChanged && CompositorHost.isStarted) R.string.drivers_using_bundle_restart else R.string.drivers_using_bundle,
|
||||
bundle.label,
|
||||
@@ -420,6 +533,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
|
||||
private companion object {
|
||||
const val TAG = "MainActivity"
|
||||
const val RELEASE_CHECK_INTERVAL_MS = 3_600_000L
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import com.droiddeck.launcher.session.SessionLogShare
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.session.SteamRepair
|
||||
import com.droiddeck.launcher.core.CpuCores
|
||||
import com.droiddeck.launcher.core.DownloadSource
|
||||
import com.droiddeck.launcher.core.PhantomProcessLimit
|
||||
import com.droiddeck.launcher.core.PhantomProcessStatus
|
||||
import com.droiddeck.launcher.core.WirelessAdbFix
|
||||
@@ -109,6 +110,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var lossless by mutableStateOf(Lossless.State.NONE)
|
||||
private var showRemove by mutableStateOf(false)
|
||||
private var showResetSettings by mutableStateOf(false)
|
||||
private var lowSpace by mutableStateOf<Long?>(null)
|
||||
private var showNonAdreno by mutableStateOf<LinuxRuntimeInstaller.Release?>(null)
|
||||
private var glThread by mutableStateOf(true)
|
||||
private var noGlError by mutableStateOf(true)
|
||||
@@ -154,6 +156,9 @@ class MainActivity : ComponentActivity() {
|
||||
private var stretch16x9 by mutableStateOf(false)
|
||||
private var orientation by mutableStateOf(android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE)
|
||||
private var launcherFullscreen by mutableStateOf(true)
|
||||
private var downloadSource by mutableStateOf(DownloadSource.MODE_AUTO)
|
||||
private var downloadMirror by mutableStateOf("")
|
||||
private var downloadPrefix by mutableStateOf("")
|
||||
private var animationsEnabled by mutableStateOf(true)
|
||||
private var gameStoresEnabled by mutableStateOf(true)
|
||||
private var gameStoresSpeedTier by mutableStateOf("fast")
|
||||
@@ -204,6 +209,9 @@ class MainActivity : ComponentActivity() {
|
||||
private val pickDeckyPluginZip = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
|
||||
if (r.resultCode == RESULT_OK) InAppFilePicker.pickedFile(r.data)?.let { decky.importPluginZip(it) }
|
||||
}
|
||||
private val pickBootMovie = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
|
||||
if (r.resultCode == RESULT_OK) InAppFilePicker.pickedFile(r.data)?.let { importBootMovie(it) }
|
||||
}
|
||||
private val pickAnyDriver = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
|
||||
if (r.resultCode == RESULT_OK) InAppFilePicker.pickedUri(r.data)?.let { drivers.importDriver(it, linux = null) }
|
||||
}
|
||||
@@ -248,6 +256,38 @@ class MainActivity : ComponentActivity() {
|
||||
}, "lossless-sync").start()
|
||||
}
|
||||
|
||||
private fun importBootMovie(file: File) {
|
||||
bootMovieImporting = true
|
||||
bootMovieProgress = -1
|
||||
Thread({
|
||||
val result = com.droiddeck.launcher.session.BootMovie.import(
|
||||
this, file,
|
||||
onStart = { ui.post { android.widget.Toast.makeText(this, getString(R.string.boot_movie_converting_started, file.name), android.widget.Toast.LENGTH_LONG).show() } },
|
||||
onProgress = { percent -> ui.post { bootMovieProgress = percent } },
|
||||
)
|
||||
val message = when (result) {
|
||||
is com.droiddeck.launcher.session.BootMovie.Import.Done -> getString(
|
||||
if (result.audioDropped) R.string.boot_movie_imported_silent else R.string.boot_movie_imported, result.name)
|
||||
com.droiddeck.launcher.session.BootMovie.Import.TooLong ->
|
||||
getString(R.string.boot_movie_too_long, com.droiddeck.launcher.session.BootMovie.MAX_CONVERT_SECONDS)
|
||||
is com.droiddeck.launcher.session.BootMovie.Import.Unsupported -> getString(R.string.boot_movie_unsupported)
|
||||
com.droiddeck.launcher.session.BootMovie.Import.Failed -> getString(R.string.boot_movie_import_failed)
|
||||
}
|
||||
ui.post {
|
||||
bootMovieImporting = false
|
||||
bootMovieProgress = -1
|
||||
loadBootMovie()
|
||||
android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}, "boot-movie-import").start()
|
||||
}
|
||||
|
||||
private fun loadBootMovie() {
|
||||
bootMovie = com.droiddeck.launcher.session.BootMovie.choice(this)
|
||||
bootMovieSound = SessionPrefs.bootMovieSound(this)
|
||||
bootMovieCustomName = com.droiddeck.launcher.session.BootMovie.customName(this)
|
||||
}
|
||||
|
||||
private fun importLossless(dll: File) {
|
||||
Thread({
|
||||
val message = Lossless.message(this, Lossless.import(this, dll))
|
||||
@@ -324,6 +364,11 @@ class MainActivity : ComponentActivity() {
|
||||
private var fexPreset by mutableStateOf("")
|
||||
private var forceSsbs by mutableStateOf(false)
|
||||
private var runSteamAtStartup by mutableStateOf(false)
|
||||
private var bootMovie by mutableStateOf(com.droiddeck.launcher.session.BootMovie.DROIDDECK_30)
|
||||
private var bootMovieSound by mutableStateOf(true)
|
||||
private var bootMovieCustomName by mutableStateOf<String?>(null)
|
||||
private var bootMovieImporting by mutableStateOf(false)
|
||||
private var bootMovieProgress by mutableStateOf(-1)
|
||||
private var theme by mutableStateOf("graphite")
|
||||
private var appScale by mutableStateOf(com.droiddeck.launcher.core.AppUiPrefs.DEFAULT_SCALE)
|
||||
private var hdrOn by mutableStateOf(false)
|
||||
@@ -514,6 +559,10 @@ class MainActivity : ComponentActivity() {
|
||||
orientation = SessionPrefs.orientation(this)
|
||||
requestedOrientation = orientation
|
||||
launcherFullscreen = SessionPrefs.launcherFullscreen(this)
|
||||
downloadSource = DownloadSource.mode(this)
|
||||
downloadMirror = DownloadSource.mirror(this)
|
||||
downloadPrefix = DownloadSource.custom(this)
|
||||
DownloadSource.probe()
|
||||
animationsEnabled = SessionPrefs.animationsEnabled(this)
|
||||
gameStoresEnabled = SessionPrefs.gameStoresEnabled(this)
|
||||
gameStoresSpeedTier = SessionPrefs.gameStoresSpeedTier(this)
|
||||
@@ -570,6 +619,9 @@ class MainActivity : ComponentActivity() {
|
||||
showPhantomGate = showPhantomGate,
|
||||
orientation = orientation,
|
||||
launcherFullscreen = launcherFullscreen,
|
||||
downloadSource = downloadSource,
|
||||
downloadMirror = downloadMirror,
|
||||
downloadPrefix = downloadPrefix,
|
||||
animationsEnabled = animationsEnabled,
|
||||
gameStoresEnabled = gameStoresEnabled,
|
||||
gameStoresSpeedTier = gameStoresSpeedTier,
|
||||
@@ -583,7 +635,12 @@ class MainActivity : ComponentActivity() {
|
||||
storeDownloadsActive = com.droiddeck.launcher.stores.StoresState.activeDownloads,
|
||||
),
|
||||
FrontEndActions(
|
||||
onPlay = { startSteamSession() },
|
||||
onPlay = { playSteam() },
|
||||
onSteamUrl = { url ->
|
||||
if (!SessionState.running) startSession(
|
||||
Intent(this, SessionActivity::class.java).putExtra(SessionService.EXTRA_STEAM_URL, url), steamSession = true,
|
||||
)
|
||||
},
|
||||
onPlayDesktopUi = {
|
||||
startSession(Intent(this, SessionActivity::class.java)
|
||||
.putExtra(SessionService.EXTRA_MODE, SessionService.MODE_STEAM)
|
||||
@@ -604,6 +661,7 @@ class MainActivity : ComponentActivity() {
|
||||
val known = steamGames.mapNotNull { it.gameFiles?.path }.toSet()
|
||||
updateAddedGames(folders, then = { if (folders.any { it !in known }) fetchAddedGameArt() })
|
||||
},
|
||||
onSteamGameArtChanged = { appId -> refreshSteamGameArt(appId) },
|
||||
onAddedGameRemoved = { folder, appId, name -> removeAddedGame(folder, appId, name) },
|
||||
onAddedGamesArt = { on -> SessionPrefs.setAddedGamesArt(this, on); addedGamesArt = on; if (on) refreshAddedGames() },
|
||||
onSgdbKey = { key ->
|
||||
@@ -743,6 +801,18 @@ class MainActivity : ComponentActivity() {
|
||||
animationsEnabled = on
|
||||
com.droiddeck.launcher.ui.Motion.refresh(this)
|
||||
},
|
||||
onDownloadSource = { mode ->
|
||||
DownloadSource.setMode(this, mode)
|
||||
downloadSource = mode
|
||||
},
|
||||
onDownloadMirror = { host ->
|
||||
DownloadSource.setMirror(this, host)
|
||||
downloadMirror = host
|
||||
},
|
||||
onDownloadPrefix = { prefix ->
|
||||
DownloadSource.setCustom(this, prefix)
|
||||
downloadPrefix = DownloadSource.custom(this)
|
||||
},
|
||||
onHomeApp = { manageHomeApp() },
|
||||
onHomeScreen = { on ->
|
||||
HomeApp.setHomeScreenEnabled(this, on)
|
||||
@@ -850,6 +920,14 @@ class MainActivity : ComponentActivity() {
|
||||
onConfirm = { showRemove = false; removeRuntime() },
|
||||
onDismiss = { showRemove = false },
|
||||
)
|
||||
lowSpace?.let { free ->
|
||||
com.droiddeck.launcher.ui.LowSpaceDialog(
|
||||
free,
|
||||
onFreeUp = { lowSpace = null; com.droiddeck.launcher.storage.StorageState.requestOpen() },
|
||||
onPlay = { lowSpace = null; com.droiddeck.launcher.storage.StorageState.lowSpaceDismissed = true; startSteamSession() },
|
||||
onDismiss = { lowSpace = null },
|
||||
)
|
||||
}
|
||||
if (showResetSettings) com.droiddeck.launcher.ui.ResetSettingsDialog(
|
||||
onConfirm = { gameProfiles -> resetSettings(gameProfiles) },
|
||||
onDismiss = { showResetSettings = false },
|
||||
@@ -902,6 +980,14 @@ class MainActivity : ComponentActivity() {
|
||||
if (SessionPrefs.runSteamAtStartup(this) && !isFinishing && !isDestroyed) startSteamSession()
|
||||
}
|
||||
|
||||
private fun playSteam() {
|
||||
val storage = com.droiddeck.launcher.storage.StorageState
|
||||
if (!SessionState.running && !storage.lowSpaceDismissed && storage.lowOnSpace(this)) {
|
||||
lowSpace = storage.freeBytes(this)
|
||||
storage.refresh(this)
|
||||
} else startSteamSession()
|
||||
}
|
||||
|
||||
private fun startSteamSession() {
|
||||
startSession(Intent(this, SessionActivity::class.java), steamSession = true)
|
||||
}
|
||||
@@ -965,7 +1051,7 @@ class MainActivity : ComponentActivity() {
|
||||
refreshPhantomStatus()
|
||||
syncLossless()
|
||||
updates.onResume()
|
||||
drivers.ensureAuto(force = false)
|
||||
drivers.checkOnOpen()
|
||||
protons.checkUpdates()
|
||||
if (!SessionState.running) Thread({
|
||||
val applied = runCatching { ComponentsManager.applyQueued(this) }.getOrDefault(emptyList())
|
||||
@@ -1027,7 +1113,9 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
private fun refreshSecondScreenDisplays() {
|
||||
if (!::displayManager.isInitialized) return
|
||||
secondScreenDisplays = SecondScreenDisplays.available(displayManager)
|
||||
// The launcher can itself be shown on a non-default display (DeX, an external monitor); that one is not a "second screen".
|
||||
val ownDisplayId = if (android.os.Build.VERSION.SDK_INT >= 30) display?.displayId ?: android.view.Display.DEFAULT_DISPLAY else @Suppress("DEPRECATION") windowManager.defaultDisplay.displayId
|
||||
secondScreenDisplays = SecondScreenDisplays.available(displayManager, ownDisplayId)
|
||||
}
|
||||
|
||||
private fun launchAndroidApp(app: HomeApp.LaunchableApp, displayId: Int?) {
|
||||
@@ -1161,6 +1249,8 @@ class MainActivity : ComponentActivity() {
|
||||
onImportAndroid = { pickAndroidDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, getString(R.string.drivers_pick_android))) },
|
||||
onImportZip = { pickAnyDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, getString(R.string.drivers_pick_any))) },
|
||||
onRestoreBundled = { TurnipDriver(this).restoreBundled(); drivers.refreshDrivers() },
|
||||
onMode = { m -> drivers.chooseMode(m) },
|
||||
onRestart = { drivers.restartApp() },
|
||||
),
|
||||
protons = com.droiddeck.launcher.ui.ProtonsState(
|
||||
rows = protons.protonRows, busyId = protons.protonBusyId, stage = protons.protonStage, percent = protons.protonPercent,
|
||||
@@ -1268,6 +1358,11 @@ class MainActivity : ComponentActivity() {
|
||||
mangoapp = mangoapp,
|
||||
steamController = if (mode == SessionService.MODE_STEAM) steamController else null,
|
||||
runSteamAtStartup = mode == SessionService.MODE_STEAM && runSteamAtStartup,
|
||||
bootMovie = if (mode == SessionService.MODE_STEAM) bootMovie else null,
|
||||
bootMovieSound = bootMovieSound,
|
||||
bootMovieCustomName = bootMovieCustomName,
|
||||
bootMovieImporting = bootMovieImporting,
|
||||
bootMovieProgress = bootMovieProgress,
|
||||
wifiDiscovery = if (mode == SessionService.MODE_STEAM) wifiDiscovery else null,
|
||||
wifiDiscoveryPermission = wifiDiscoveryPermission,
|
||||
wifiDiscoveryLocation = wifiDiscoveryLocation,
|
||||
@@ -1348,6 +1443,12 @@ class MainActivity : ComponentActivity() {
|
||||
SessionPrefs.setRunSteamAtStartup(this, on)
|
||||
runSteamAtStartup = on
|
||||
},
|
||||
onBootMovie = { id -> SessionPrefs.setBootMovie(this, id); loadBootMovie() },
|
||||
onBootMovieSound = { on -> SessionPrefs.setBootMovieSound(this, on); bootMovieSound = on },
|
||||
onImportBootMovie = {
|
||||
pickBootMovie.launch(InAppFilePicker.buildIntent(this, com.droiddeck.launcher.session.BootMovie.PICKER_EXTENSIONS, getString(R.string.boot_movie_pick)))
|
||||
},
|
||||
onRemoveBootMovie = { com.droiddeck.launcher.session.BootMovie.removeCustom(this); loadBootMovie() },
|
||||
onDeckyInstall = { release -> decky.installDecky(release) },
|
||||
onDeckyCheck = { decky.refreshDecky() },
|
||||
onDeckyEnabled = { enabled -> DeckyManager.setSupervisorEnabled(this, enabled); decky.deckySupervisor = enabled },
|
||||
@@ -1418,6 +1519,20 @@ class MainActivity : ComponentActivity() {
|
||||
}, "added-games-update").start()
|
||||
}
|
||||
|
||||
private val steamArtRefreshes = HashMap<Int, Int>()
|
||||
|
||||
private fun refreshSteamGameArt(appId: Int) {
|
||||
val version = (steamArtRefreshes[appId] ?: 0) + 1
|
||||
steamArtRefreshes[appId] = version
|
||||
Thread({
|
||||
val fresh = com.droiddeck.launcher.frontend.Library.steamGames(this).firstOrNull { it.appId == appId } ?: return@Thread
|
||||
ui.post {
|
||||
if (steamArtRefreshes[appId] != version) return@post
|
||||
steamGames = steamGames.map { if (it.appId == appId && it.library != Library.ADDED) fresh else it }
|
||||
}
|
||||
}, "steam-art-update").start()
|
||||
}
|
||||
|
||||
private fun removeAddedGame(folder: String, appId: Long?, name: String) {
|
||||
steamGames = steamGames.filterNot { it.library == Library.ADDED && it.gameFiles?.path == folder }
|
||||
Thread({ com.droiddeck.launcher.frontend.AddedGames.remove(this, java.io.File(folder), appId, name) }, "added-game-remove").start()
|
||||
@@ -1465,6 +1580,7 @@ class MainActivity : ComponentActivity() {
|
||||
mangoapp = SessionPrefs.mangoapp(this)
|
||||
steamController = SessionPrefs.steamController(this)
|
||||
runSteamAtStartup = SessionPrefs.runSteamAtStartup(this)
|
||||
loadBootMovie()
|
||||
hdrOn = SessionPrefs.hdr(this, mode)
|
||||
fpsLimit = SessionPrefs.fpsLimit(this, mode)
|
||||
upscaler = SessionPrefs.upscaler(this)
|
||||
@@ -1537,6 +1653,7 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
private fun refresh() {
|
||||
if (!busy && LinuxRuntimeInstaller.isBusy()) followRuntimeOperation { LinuxRuntimeInstaller.attach(it) }
|
||||
if (showComponents) drivers.refreshDrivers()
|
||||
addedGamesArt = SessionPrefs.addedGamesArt(this)
|
||||
desktopInstalled = DesktopCatalog.desktopInstalled(this)
|
||||
desktopPresent = DesktopCatalog.desktopPresent(this)
|
||||
@@ -1556,6 +1673,7 @@ class MainActivity : ComponentActivity() {
|
||||
else -> "Steam"
|
||||
} else null
|
||||
val scanGeneration = ++libraryScanGeneration
|
||||
val artRefreshesAtStart = HashMap(steamArtRefreshes)
|
||||
if (shortcutPicker) shortcutLibraryScanning = true
|
||||
Thread({
|
||||
val emus = Library.emulators(this) { id -> DesktopCatalog.installed(this, id) != null }
|
||||
@@ -1584,7 +1702,11 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
ui.post {
|
||||
if (scanGeneration == libraryScanGeneration) {
|
||||
steamGames = all
|
||||
val current = steamGames.filter { it.library != Library.ADDED }.associateBy { it.appId }
|
||||
steamGames = all.map { g ->
|
||||
val newer = current[g.appId]
|
||||
if (newer != null && g.library != Library.ADDED && steamArtRefreshes[g.appId] != artRefreshesAtStart[g.appId]) g.withArt(newer.art, newer.hero, newer.icon) else g
|
||||
}
|
||||
shortcutLibraryScanning = false
|
||||
handleGameLink(all)
|
||||
}
|
||||
@@ -1613,6 +1735,10 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
private fun startSession(intent: Intent, steamSession: Boolean = false): Boolean {
|
||||
if (busy || LinuxRuntimeInstaller.isBusy()) return false
|
||||
if (com.droiddeck.launcher.storage.StorageState.cleaning) {
|
||||
android.widget.Toast.makeText(this, R.string.storage_cleanup_busy, android.widget.Toast.LENGTH_SHORT).show()
|
||||
return false
|
||||
}
|
||||
refreshPhantomStatus()
|
||||
if (steamSession && PhantomProcessLimit.blocksSteam(phantomProcessStatus)) {
|
||||
showPhantomGate = true
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.droiddeck.launcher
|
||||
|
||||
import android.app.NativeActivity
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.droiddeck.launcher.gpu.SteamVr
|
||||
import com.droiddeck.launcher.session.SessionService
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
import com.droiddeck.launcher.wayland.CompositorHost
|
||||
import com.droiddeck.launcher.wayland.WaylandCompositor
|
||||
|
||||
class QuestXrActivity : NativeActivity() {
|
||||
private var steamVrStarted = false
|
||||
@Volatile private var closing = false
|
||||
@Volatile private var steamVrStatus = SteamVr.Status.UNAVAILABLE
|
||||
private var sessionId: String? = null
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
sessionId = SessionState.sessionId
|
||||
SessionService.setXrActivityVisible(this, true)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
fun onXrActive(active: Boolean) {
|
||||
if (active) {
|
||||
check(CompositorHost.isStarted) { "DroidDeck compositor is not running" }
|
||||
SessionService.resume(this)
|
||||
WaylandCompositor.nativeSetCardboard(false, 1, 0.064f, 3f, 4f, 0f, 0f, 90f, 0f, 0f)
|
||||
WaylandCompositor.nativeSetFrameGenArmed(false, 0, 0)
|
||||
WaylandCompositor.nativeSetZeroCopy(false)
|
||||
CompositorHost.attachXr()
|
||||
WaylandCompositor.nativeVsync(System.nanoTime())
|
||||
check(WaylandCompositor.nativeWaitSurface(3000)) { "XR frame GPU handshake timed out" }
|
||||
}
|
||||
SessionService.setXrActivityVisible(this, active)
|
||||
if (active) startSteamVr()
|
||||
}
|
||||
|
||||
private fun startSteamVr() {
|
||||
if (!steamVrStarted) {
|
||||
steamVrStarted = true
|
||||
Thread({
|
||||
try {
|
||||
val started = synchronized(steamVrLock) {
|
||||
if (closing || !sessionAlive()) false else {
|
||||
steamVrOwner = this
|
||||
SteamVr.run(this, "start")
|
||||
}
|
||||
}
|
||||
if (!started) {
|
||||
if (!closing) leaveXr("SteamVR start failed", sessionAlive())
|
||||
return@Thread
|
||||
}
|
||||
val startupDeadline = SystemClock.elapsedRealtime() + 30_000
|
||||
var lastRunning: Long? = null
|
||||
while (!closing) {
|
||||
if (!sessionAlive()) {
|
||||
leaveXr("Steam session ended", false)
|
||||
break
|
||||
}
|
||||
steamVrStatus = SteamVr.status(this)
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
if (steamVrStatus == SteamVr.Status.RUNNING) lastRunning = now
|
||||
val deadline = lastRunning?.plus(5_000) ?: startupDeadline
|
||||
if (steamVrStatus != SteamVr.Status.RUNNING && now >= deadline) {
|
||||
leaveXr("SteamVR stopped or did not finish starting", true)
|
||||
break
|
||||
}
|
||||
Thread.sleep(1000)
|
||||
}
|
||||
} finally {
|
||||
synchronized(steamVrLock) {
|
||||
// A retiring activity must not stop a replacement's SteamVR session.
|
||||
if (steamVrOwner === this) {
|
||||
if (sessionAlive()) SteamVr.run(this, "stop")
|
||||
steamVrOwner = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}, "quest-steamvr-start").start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sessionAlive() = SessionState.running && !SessionState.stopRequested &&
|
||||
sessionId != null && SessionState.sessionId == sessionId
|
||||
|
||||
private fun leaveXr(reason: String, failed: Boolean) {
|
||||
Log.w("DroidDeckXR", reason)
|
||||
closing = true
|
||||
runOnUiThread {
|
||||
if (failed) Toast.makeText(this, R.string.cardboard_steamvr_failed, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
fun onXrFrame(frameTimeNanos: Long) {
|
||||
WaylandCompositor.nativeVsync(frameTimeNanos)
|
||||
}
|
||||
|
||||
fun onXrStatus(): Int = steamVrStatus.ordinal
|
||||
|
||||
override fun onDestroy() {
|
||||
closing = true
|
||||
SessionService.setXrActivityVisible(this, false)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
SessionService.setXrActivityVisible(this, false)
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val steamVrLock = Any()
|
||||
private var steamVrOwner: QuestXrActivity? = null
|
||||
}
|
||||
}
|
||||
@@ -239,6 +239,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private var leaving by mutableStateOf(false)
|
||||
private var closeAtOnce = false
|
||||
private var drawerComponents by mutableStateOf<ComponentsManager.Snapshot?>(null)
|
||||
private var drawerDefaultProton by mutableStateOf<String?>(null)
|
||||
private val drawerComponentMenu by lazy { ComponentsMenu(this, uiHandler) }
|
||||
private var drawerGameProfileId by mutableStateOf<Long?>(null)
|
||||
private val drawerGameInventory = GameProfileInventoryCache<Library.SteamGame>()
|
||||
@@ -521,8 +522,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
releaseDrawerDirection()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(cardboard.enabled, steamVrSwitchedPicture) {
|
||||
while (cardboard.enabled || steamVrSwitchedPicture) {
|
||||
LaunchedEffect(cardboard.enabled, steamVrSwitchedPicture, drawerOpen, drawerPage) {
|
||||
while (cardboard.enabled || steamVrSwitchedPicture || (drawerOpen && drawerPage == SessionDrawerPage.VR)) {
|
||||
val now = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
|
||||
SteamVr.status(this@SessionActivity)
|
||||
}
|
||||
@@ -559,6 +560,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
onCardboard = { c -> SessionPrefs.setCardboard(this@SessionActivity, c); applyCardboard(c) },
|
||||
onCardboardRecenter = { headTracker?.recenter() },
|
||||
steamVr = steamVrStatus,
|
||||
immersiveSteamVr = com.droiddeck.launcher.session.QuestExperimental.isSupported(),
|
||||
onSteamVr = { action -> steamVrAction(action) },
|
||||
textureAnisotropy = if (drawerGameProfileId == null) textureAnisotropy else drawerTextureAnisotropy,
|
||||
textureLodBias = if (drawerGameProfileId == null) textureLodBias else drawerTextureLodBias,
|
||||
@@ -739,6 +741,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
onGameNoSteam = ::setDrawerGameNoSteam,
|
||||
onComponentSwap = { pid, comp, value -> swapDrawerComponent(pid, comp, value) },
|
||||
onGameComponent = ::setDrawerGameComponent,
|
||||
defaultProtonId = drawerDefaultProton,
|
||||
onDefaultProton = ::chooseDrawerDefaultProton,
|
||||
onGameProton = ::setDrawerGameProton,
|
||||
))
|
||||
LaunchedEffect(achievementsOpen) {
|
||||
if (achievementsOpen) {
|
||||
@@ -858,8 +863,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
Thread({
|
||||
val applied = runCatching { ComponentsManager.applyQueued(this) }.getOrDefault(emptyList())
|
||||
val snap = runCatching { ComponentsManager.snapshot(this) }.getOrNull()
|
||||
val chosen = snap?.let { s -> runCatching { ProtonDefault.selectedId(this, s.protons.map { it.proton }) }.getOrNull() }
|
||||
uiHandler.post {
|
||||
drawerComponents = snap
|
||||
drawerDefaultProton = chosen
|
||||
if (applied.isNotEmpty()) android.widget.Toast.makeText(this, getString(R.string.main_applied, applied.joinToString(", ")), android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}, "drawer-components").start()
|
||||
@@ -1003,6 +1010,46 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}, "drawer-game-component").start()
|
||||
}
|
||||
|
||||
private fun chooseDrawerDefaultProton(protonId: String) {
|
||||
val proton = drawerComponents?.protons?.firstOrNull { it.proton.id == protonId }?.proton ?: return
|
||||
drawerDefaultProton = protonId
|
||||
Thread({
|
||||
val message = runCatching {
|
||||
when (ProtonDefault.request(this, proton)) {
|
||||
ProtonDefault.Outcome.LIVE -> getString(R.string.comp_default_live, proton.name)
|
||||
ProtonDefault.Outcome.NEXT_START -> getString(R.string.comp_default_next, proton.name)
|
||||
ProtonDefault.Outcome.NOT_RUNNABLE -> getString(R.string.comp_default_not_runnable, proton.name)
|
||||
}
|
||||
}.getOrElse { e -> getString(R.string.comp_default_failed, e.message ?: e.javaClass.simpleName) }
|
||||
uiHandler.post {
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
refreshDrawerComponents()
|
||||
refreshDrawerGameProfile()
|
||||
}
|
||||
}, "drawer-default-proton").start()
|
||||
}
|
||||
|
||||
private fun setDrawerGameProton(appId: Long, protonId: String?) {
|
||||
val name = drawerGameProfileRefresh.data?.profiles?.firstOrNull { it.first == appId }?.second ?: appId.toString()
|
||||
val proton = protonId?.let { id -> drawerComponents?.protons?.firstOrNull { it.proton.id == id }?.proton ?: return }
|
||||
Thread({
|
||||
val message = runCatching {
|
||||
getString(
|
||||
when (ProtonDefault.requestGame(this, appId.toString(), proton)) {
|
||||
ProtonDefault.Outcome.LIVE -> R.string.comp_game_proton_live
|
||||
ProtonDefault.Outcome.NEXT_START -> R.string.comp_game_proton_next
|
||||
ProtonDefault.Outcome.NOT_RUNNABLE -> R.string.comp_game_proton_not_runnable
|
||||
},
|
||||
name,
|
||||
)
|
||||
}.getOrElse { e -> getString(R.string.session_profile_save_failed, e.message ?: e.javaClass.simpleName) }
|
||||
uiHandler.post {
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
refreshDrawerGameProfile()
|
||||
}
|
||||
}, "drawer-game-proton").start()
|
||||
}
|
||||
|
||||
private fun setDrawerGameFexPreset(appId: Long, preset: String?) {
|
||||
if (drawerGameFexPresetSaving) return
|
||||
drawerGameFexPresetSaving = true
|
||||
@@ -1250,6 +1297,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
val turnip = TurnipDriver(this)
|
||||
com.droiddeck.launcher.gpu.DriverSetupLog.write(this, "surface.driver-setup")
|
||||
if (!CompositorHost.isStarted) CompositorHost.displayChoice = SessionPrefs.androidDriver(this)
|
||||
val driverId = if (CompositorHost.isStarted) null else turnip.install()
|
||||
|
||||
val previous = SessionState.outputSize
|
||||
@@ -1874,6 +1922,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
onSteamMenu = ::tapSteamButton,
|
||||
onQam = ::tapQam,
|
||||
onClose = { selectSecondScreenMode(SecondScreenMode.NONE) },
|
||||
pad = padBridge,
|
||||
)
|
||||
secondScreenPresentation = presentation
|
||||
}
|
||||
@@ -2383,6 +2432,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
private fun steamVrAction(action: String) {
|
||||
if (action == "start" && com.droiddeck.launcher.session.QuestExperimental.isSupported()) {
|
||||
startActivity(android.content.Intent(this, QuestXrActivity::class.java))
|
||||
return
|
||||
}
|
||||
if (action == "start") {
|
||||
switchPictureForSteamVr()
|
||||
steamVrSeenRunning = false
|
||||
|
||||
@@ -2,10 +2,15 @@ package com.droiddeck.launcher.core
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.PerformanceHintManager
|
||||
import android.os.PowerManager
|
||||
import android.os.StatFs
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.Display
|
||||
import com.droiddeck.launcher.gpu.LinuxVulkanDriver
|
||||
import com.droiddeck.launcher.gpu.LinuxVulkanDriverManager
|
||||
import com.droiddeck.launcher.gpu.TurnipDriver
|
||||
@@ -32,6 +37,57 @@ object DeviceReport {
|
||||
}
|
||||
}
|
||||
|
||||
/** Name of an ApplicationExitInfo reason code, so a report says "LOW_MEMORY" instead of "3". */
|
||||
internal fun exitReasonName(reason: Int): String = when (reason) {
|
||||
0 -> "UNKNOWN"
|
||||
1 -> "EXIT_SELF"
|
||||
2 -> "SIGNALED"
|
||||
3 -> "LOW_MEMORY"
|
||||
4 -> "CRASH"
|
||||
5 -> "CRASH_NATIVE"
|
||||
6 -> "ANR"
|
||||
7 -> "INITIALIZATION_FAILURE"
|
||||
8 -> "PERMISSION_CHANGE"
|
||||
9 -> "EXCESSIVE_RESOURCE_USAGE"
|
||||
10 -> "USER_REQUESTED"
|
||||
11 -> "USER_STOPPED"
|
||||
12 -> "DEPENDENCY_DIED"
|
||||
13 -> "OTHER"
|
||||
14 -> "FREEZER"
|
||||
15 -> "PACKAGE_STATE_CHANGE"
|
||||
16 -> "PACKAGE_UPDATED"
|
||||
else -> "REASON_$reason"
|
||||
}
|
||||
|
||||
internal fun thermalStatusName(status: Int): String = when (status) {
|
||||
0 -> "NONE"
|
||||
1 -> "LIGHT"
|
||||
2 -> "MODERATE"
|
||||
3 -> "SEVERE"
|
||||
4 -> "CRITICAL"
|
||||
5 -> "EMERGENCY"
|
||||
6 -> "SHUTDOWN"
|
||||
else -> "STATUS_$status"
|
||||
}
|
||||
|
||||
internal fun gameModeName(mode: Int): String = when (mode) {
|
||||
0 -> "UNSUPPORTED"
|
||||
1 -> "STANDARD"
|
||||
2 -> "PERFORMANCE"
|
||||
3 -> "BATTERY"
|
||||
4 -> "CUSTOM"
|
||||
else -> "MODE_$mode"
|
||||
}
|
||||
|
||||
internal fun refreshModesSummary(rates: List<Float>): String =
|
||||
rates.map { Math.round(it) }.distinct().sortedDescending().joinToString(", ") { "$it Hz" }
|
||||
|
||||
internal fun exitSummary(processName: String?, reason: Int, status: Int, description: String?): String =
|
||||
buildString {
|
||||
append(processName ?: "?").append(" ").append(exitReasonName(reason)).append(" status=").append(status)
|
||||
if (!description.isNullOrBlank()) append(" ").append(description.trim())
|
||||
}
|
||||
|
||||
fun build(context: Context, mode: String): String {
|
||||
val b = StringBuilder()
|
||||
fun h(title: String) {
|
||||
@@ -68,6 +124,20 @@ object DeviceReport {
|
||||
k("Fingerprint", Build.FINGERPRINT)
|
||||
k("Kernel", System.getProperty("os.version"))
|
||||
k("ABIs", Build.SUPPORTED_ABIS.joinToString(", "))
|
||||
k("Memory page size", runCatching { android.system.Os.sysconf(android.system.OsConstants._SC_PAGESIZE) }.getOrNull()?.let { "$it bytes" })
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
h("Recent process exits")
|
||||
val exits = runCatching {
|
||||
context.getSystemService(android.app.ActivityManager::class.java).getHistoricalProcessExitReasons(context.packageName, 0, 5)
|
||||
}.getOrNull().orEmpty()
|
||||
if (exits.isEmpty()) k("Exits", "none recorded") else {
|
||||
val stamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
|
||||
for (e in exits) {
|
||||
k(stamp.format(Date(e.timestamp)), exitSummary(e.processName, e.reason, e.status, e.description))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h("CPU and memory")
|
||||
k("Cores", CpuCores.all.size)
|
||||
@@ -82,6 +152,19 @@ object DeviceReport {
|
||||
k("RAM total", FileUtils.sizeToString(mi.totalMem))
|
||||
k("RAM available", FileUtils.sizeToString(mi.availMem))
|
||||
}
|
||||
runCatching {
|
||||
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) k("Thermal status", thermalStatusName(pm.currentThermalStatus))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
k("Thermal headroom (10 s)", pm.getThermalHeadroom(10).let { if (it.isNaN()) "not reported" else String.format(Locale.US, "%.2f", it) })
|
||||
}
|
||||
k("Power saver", pm.isPowerSaveMode)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val rate = runCatching { context.getSystemService(PerformanceHintManager::class.java)?.preferredUpdateRateNanos }.getOrNull()
|
||||
k("ADPF hint sessions", if (rate == null || rate <= 0) "not supported" else "supported (preferred update ${rate / 1000} us)")
|
||||
k("Game Mode", runCatching { context.getSystemService(android.app.GameManager::class.java)?.gameMode }.getOrNull()?.let { gameModeName(it) })
|
||||
}
|
||||
runCatching {
|
||||
val fs = StatFs(context.filesDir.path)
|
||||
k("App storage free", FileUtils.sizeToString(fs.availableBlocksLong * fs.blockSizeLong))
|
||||
@@ -99,6 +182,13 @@ object DeviceReport {
|
||||
h("Display")
|
||||
k("Session output", SessionState.outputSize?.let { "${it.first}x${it.second}" })
|
||||
k("Session refresh", String.format(Locale.US, "%.2f Hz", SessionState.refreshHz))
|
||||
runCatching {
|
||||
val display = context.getSystemService(DisplayManager::class.java).getDisplay(Display.DEFAULT_DISPLAY)
|
||||
k("Panel refresh now", String.format(Locale.US, "%.2f Hz", display.refreshRate))
|
||||
k("Panel refresh modes", refreshModesSummary(display.supportedModes.map { it.refreshRate }))
|
||||
}
|
||||
k("peak_refresh_rate", runCatching { Settings.System.getString(context.contentResolver, "peak_refresh_rate") }.getOrNull() ?: "not set")
|
||||
k("refresh_rate_mode", runCatching { Settings.Secure.getString(context.contentResolver, "refresh_rate_mode") }.getOrNull() ?: "not set")
|
||||
val panelSize = com.droiddeck.launcher.session.SessionDisplay.panelSize(context)
|
||||
for ((label, mode) in listOf("Resolution (Steam)" to SessionService.MODE_STEAM, "Resolution (Desktop)" to SessionService.MODE_DESKTOP)) {
|
||||
val choice = SessionPrefs.resolutionChoice(context, mode, panelSize)
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
object DownloadSource {
|
||||
const val MODE_AUTO = "auto"
|
||||
const val MODE_DIRECT = "direct"
|
||||
const val MODE_MIRROR = "mirror"
|
||||
const val MODE_CUSTOM = "custom"
|
||||
|
||||
private const val PREFS = "download-source"
|
||||
private const val KEY_MODE = "mode"
|
||||
private const val KEY_MIRROR = "mirror"
|
||||
private const val KEY_CUSTOM = "custom"
|
||||
private const val KEY_FASTEST = "fastest"
|
||||
|
||||
private const val PROBE = "https://raw.githubusercontent.com/Droid-Deck/DroidDeck/main/README.md"
|
||||
private const val PROBE_TIMEOUT_MS = 4_000
|
||||
private const val PROBE_COOLDOWN_MS = 30_000L
|
||||
private const val UA = "DroidDeck-Android"
|
||||
|
||||
private val MODES = listOf(MODE_AUTO, MODE_DIRECT, MODE_MIRROR, MODE_CUSTOM)
|
||||
|
||||
private val GITHUB_HOSTS = listOf(
|
||||
"github.com",
|
||||
"raw.githubusercontent.com",
|
||||
"api.github.com",
|
||||
)
|
||||
|
||||
private val CATALOGS = listOf(
|
||||
"https://raw.githubusercontent.com/Droid-Deck/DroidDeck-CI/",
|
||||
"https://raw.githubusercontent.com/Droid-Deck/DroidDeck-Components/",
|
||||
)
|
||||
|
||||
data class Mirror(val host: String, val api: Boolean)
|
||||
|
||||
val MIRRORS = listOf(
|
||||
Mirror("gh-proxy.com", true),
|
||||
Mirror("gh-proxy.org", true),
|
||||
Mirror("ghproxy.net", false),
|
||||
Mirror("ghfast.top", false),
|
||||
)
|
||||
|
||||
@Volatile
|
||||
private var app: Context? = null
|
||||
|
||||
@Volatile
|
||||
private var lastGood: String? = null
|
||||
|
||||
private val probing = AtomicBoolean(false)
|
||||
private val probedAt = AtomicLong(0L)
|
||||
|
||||
fun init(context: Context) {
|
||||
app = context.applicationContext
|
||||
lastGood = prefs()?.getString(KEY_FASTEST, null)
|
||||
}
|
||||
|
||||
fun probe() {
|
||||
if (app == null || prefs()?.getString(KEY_MODE, MODE_AUTO) == MODE_DIRECT) return
|
||||
if (!probing.compareAndSet(false, true)) return
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - probedAt.get() < PROBE_COOLDOWN_MS) {
|
||||
probing.set(false)
|
||||
return
|
||||
}
|
||||
probedAt.set(now)
|
||||
Thread {
|
||||
try {
|
||||
val times = ConcurrentHashMap<String, Long>()
|
||||
val sources = listOf<String?>(null) + MIRRORS.map { it.host }
|
||||
val latch = CountDownLatch(sources.size)
|
||||
for (mirror in sources) {
|
||||
Thread {
|
||||
try {
|
||||
val ms = latency(mirror?.let { "https://$it/$PROBE" } ?: PROBE)
|
||||
if (ms > 0) times[mirror ?: ""] = ms
|
||||
} finally {
|
||||
latch.countDown()
|
||||
}
|
||||
}.apply { isDaemon = true }.start()
|
||||
}
|
||||
latch.await(PROBE_TIMEOUT_MS * 3L, TimeUnit.MILLISECONDS)
|
||||
val best = times.minByOrNull { it.value }?.key ?: return@Thread
|
||||
lastGood = best
|
||||
prefs()?.edit()?.putString(KEY_FASTEST, best)?.apply()
|
||||
} finally {
|
||||
probing.set(false)
|
||||
}
|
||||
}.apply { isDaemon = true }.start()
|
||||
}
|
||||
|
||||
private fun latency(url: String): Long {
|
||||
val started = System.nanoTime()
|
||||
var connection: HttpURLConnection? = null
|
||||
return try {
|
||||
val c = (URL(url).openConnection() as HttpURLConnection).apply {
|
||||
connectTimeout = PROBE_TIMEOUT_MS
|
||||
readTimeout = PROBE_TIMEOUT_MS
|
||||
instanceFollowRedirects = true
|
||||
setRequestProperty("Range", "bytes=0-1023")
|
||||
setRequestProperty("User-Agent", UA)
|
||||
}
|
||||
connection = c
|
||||
if (c.responseCode / 100 != 2) 0L else {
|
||||
c.inputStream.use { it.read() }
|
||||
(System.nanoTime() - started) / 1_000_000
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
0L
|
||||
} finally {
|
||||
runCatching { connection?.disconnect() }
|
||||
}
|
||||
}
|
||||
|
||||
fun isGithub(url: String): Boolean = GITHUB_HOSTS.any { url.startsWith("https://$it/") }
|
||||
|
||||
/** Catalogs carry the digests everything else is checked against, so a mirror must never supply one. */
|
||||
fun isCatalog(url: String): Boolean = CATALOGS.any { url.startsWith(it) }
|
||||
|
||||
fun candidates(target: String): List<String> {
|
||||
// GitHub API metadata carries executable URLs and digests; never proxy it.
|
||||
if (isCatalog(target) || target.startsWith("https://api.github.com/")) return listOf(target)
|
||||
val prefs = prefs() ?: return listOf(target)
|
||||
if (!isGithub(target)) return listOf(target)
|
||||
val api = target.startsWith("https://api.github.com/")
|
||||
val proxies = MIRRORS.filter { !api || it.api }.map { it.proxy(target) }
|
||||
val ordered = when (prefs.getString(KEY_MODE, MODE_AUTO) ?: MODE_AUTO) {
|
||||
MODE_DIRECT -> listOf(target)
|
||||
MODE_MIRROR -> listOfNotNull(picked(prefs, api)?.proxy(target)) + listOf(target) + quickest(proxies)
|
||||
MODE_CUSTOM -> listOfNotNull(customProxy(prefs, target)) + listOf(target) + quickest(proxies)
|
||||
else -> if (api) listOf(target) else listOf(target) + quickest(proxies)
|
||||
}
|
||||
return ordered.distinct()
|
||||
}
|
||||
|
||||
private fun quickest(sources: List<String>): List<String> =
|
||||
sources.sortedBy { if (host(it) == lastGood) 0 else 1 }
|
||||
|
||||
fun succeeded(source: String) {
|
||||
lastGood = host(source)
|
||||
}
|
||||
|
||||
fun failed() {
|
||||
lastGood = null
|
||||
prefs()?.edit()?.remove(KEY_FASTEST)?.apply()
|
||||
}
|
||||
|
||||
fun mode(context: Context): String = prefs(context)?.getString(KEY_MODE, MODE_AUTO) ?: MODE_AUTO
|
||||
|
||||
fun setMode(context: Context, mode: String) {
|
||||
prefs(context)?.edit()?.putString(KEY_MODE, mode.takeIf { it in MODES } ?: MODE_AUTO)?.apply()
|
||||
}
|
||||
|
||||
fun mirror(context: Context): String {
|
||||
val host = prefs(context)?.getString(KEY_MIRROR, "") ?: ""
|
||||
return MIRRORS.firstOrNull { it.host == host }?.host ?: MIRRORS.first().host
|
||||
}
|
||||
|
||||
fun setMirror(context: Context, host: String) {
|
||||
prefs(context)?.edit()?.putString(KEY_MIRROR, host.takeIf { h -> MIRRORS.any { it.host == h } } ?: "")?.apply()
|
||||
}
|
||||
|
||||
fun custom(context: Context): String = prefs(context)?.getString(KEY_CUSTOM, "") ?: ""
|
||||
|
||||
fun setCustom(context: Context, prefix: String) {
|
||||
prefs(context)?.edit()?.putString(KEY_CUSTOM, normalizePrefix(prefix))?.apply()
|
||||
}
|
||||
|
||||
fun reset(context: Context) {
|
||||
lastGood = null
|
||||
prefs(context)?.edit()?.clear()?.apply()
|
||||
}
|
||||
|
||||
private fun prefs(): SharedPreferences? = app?.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
|
||||
private fun prefs(context: Context): SharedPreferences? =
|
||||
context.applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
|
||||
private fun Mirror.proxy(target: String): String = "https://$host/$target"
|
||||
|
||||
private fun host(url: String): String =
|
||||
MIRRORS.firstOrNull { url.startsWith("https://${it.host}/") }?.host ?: ""
|
||||
|
||||
private fun picked(prefs: SharedPreferences, api: Boolean): Mirror? {
|
||||
val chosen = MIRRORS.firstOrNull { it.host == (prefs.getString(KEY_MIRROR, "") ?: "") } ?: MIRRORS.firstOrNull()
|
||||
return if (api && chosen != null && !chosen.api) MIRRORS.firstOrNull { it.api } else chosen
|
||||
}
|
||||
|
||||
private fun custom(prefs: SharedPreferences): String = prefs.getString(KEY_CUSTOM, "") ?: ""
|
||||
|
||||
private fun customProxy(prefs: SharedPreferences, target: String): String? =
|
||||
normalizePrefix(custom(prefs)).takeIf { it.isNotEmpty() }?.let { "$it/$target" }
|
||||
|
||||
private fun normalizePrefix(value: String): String {
|
||||
val trimmed = value.trim().trimEnd('/')
|
||||
if (trimmed.isEmpty() || trimmed.startsWith("http://")) return ""
|
||||
return if (trimmed.startsWith("https://")) trimmed else "https://$trimmed"
|
||||
}
|
||||
}
|
||||
@@ -6,69 +6,201 @@ import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
public final class Downloader {
|
||||
private static final String TAG = "Downloader";
|
||||
private static final int TIMEOUT_MS = 30_000;
|
||||
private static final int CONNECT_TIMEOUT_MS = 10_000;
|
||||
private static final int STALL_TIMEOUT_MS = 15_000;
|
||||
private static final int HTTP_RANGE_NOT_SATISFIABLE = 416;
|
||||
|
||||
private Downloader() {}
|
||||
|
||||
public static String downloadString(String url) {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
connection = open(url);
|
||||
if (connection.getResponseCode() / 100 != 2) {
|
||||
Log.w(TAG, url + " -> HTTP " + connection.getResponseCode());
|
||||
return null;
|
||||
List<String> sources = DownloadSource.INSTANCE.candidates(url);
|
||||
for (int i = 0; i < sources.size(); i++) {
|
||||
if (i > 0) Log.i(TAG, "retrying " + url + " via " + sources.get(i));
|
||||
Fetch fetch = fetch(sources.get(i));
|
||||
if (fetch.body != null) {
|
||||
DownloadSource.INSTANCE.succeeded(sources.get(i));
|
||||
return fetch.body;
|
||||
}
|
||||
try (InputStream in = connection.getInputStream();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
FileUtils.copy(in, out);
|
||||
return new String(out.toByteArray(), StandardCharsets.UTF_8);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "GET " + url, e);
|
||||
return null;
|
||||
} finally {
|
||||
if (connection != null) connection.disconnect();
|
||||
if (fetch.fatal && sources.get(i).equals(url)) return null;
|
||||
}
|
||||
DownloadSource.INSTANCE.failed();
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Fetches without the mirror list, for a URL whose body is a digest the payload is checked against. */
|
||||
public static String downloadDirect(String url) {
|
||||
return fetch(url).body;
|
||||
}
|
||||
|
||||
public static boolean downloadFile(String url, File destination, boolean resume,
|
||||
java.util.function.Consumer<Float> progress) {
|
||||
return downloadFile(url, destination, resume, null, progress);
|
||||
}
|
||||
|
||||
/** Verify each candidate before accepting it, so a bad mirror can fall back. */
|
||||
public static boolean downloadFile(String url, File destination, boolean resume, String expectedSha256,
|
||||
java.util.function.Consumer<Float> progress) {
|
||||
if (expectedSha256 != null && !expectedSha256.matches("(?i)[0-9a-f]{64}"))
|
||||
throw new IllegalArgumentException("Expected SHA-256 must be 64 hex characters");
|
||||
return downloadCandidates(url, DownloadSource.INSTANCE.candidates(url), destination, resume, expectedSha256, progress);
|
||||
}
|
||||
|
||||
// Package-private for the local multi-source HTTP test.
|
||||
static boolean downloadCandidates(String original, List<String> sources, File destination, boolean resume,
|
||||
String expectedSha256, java.util.function.Consumer<Float> progress) {
|
||||
Identity identity = loadIdentity(destination, resume, expectedSha256);
|
||||
for (int i = 0; i < sources.size(); i++) {
|
||||
String source = sources.get(i);
|
||||
if (i > 0) Log.i(TAG, "retrying " + original + " via " + source);
|
||||
Outcome outcome = attempt(source, destination, resume, identity, expectedSha256, progress);
|
||||
if (outcome == Outcome.DONE) {
|
||||
if (expectedSha256 != null && !expectedSha256.equalsIgnoreCase(Hashes.sha256(destination))) {
|
||||
Log.w(TAG, "checksum mismatch from " + source + "; trying another source");
|
||||
if (!discardPartial(destination, identity)) return false;
|
||||
continue;
|
||||
}
|
||||
resumeFile(destination).delete();
|
||||
DownloadSource.INSTANCE.succeeded(source);
|
||||
if (progress != null) progress.accept(1f);
|
||||
return true;
|
||||
}
|
||||
if (outcome == Outcome.FATAL && source.equals(original)) return false;
|
||||
}
|
||||
DownloadSource.INSTANCE.failed();
|
||||
return false;
|
||||
}
|
||||
|
||||
private enum Outcome { DONE, RETRY, FATAL }
|
||||
|
||||
private static final class Identity {
|
||||
private String source;
|
||||
private String etag;
|
||||
}
|
||||
|
||||
private static File resumeFile(File dest) {
|
||||
return new File(dest.getPath() + ".resume.properties");
|
||||
}
|
||||
|
||||
private static boolean strongETag(String value) {
|
||||
return value != null && !value.isEmpty() && !value.startsWith("W/");
|
||||
}
|
||||
|
||||
/** Only reuse saved bytes when we persisted a strong validator and their source. */
|
||||
private static Identity loadIdentity(File dest, boolean resume, String expectedSha256) {
|
||||
Identity identity = new Identity();
|
||||
File marker = resumeFile(dest);
|
||||
if (!resume || !dest.isFile() || dest.length() == 0) {
|
||||
marker.delete();
|
||||
return identity;
|
||||
}
|
||||
try (FileInputStream in = new FileInputStream(marker)) {
|
||||
Properties p = new Properties();
|
||||
p.load(in);
|
||||
String source = p.getProperty("source");
|
||||
String etag = p.getProperty("etag");
|
||||
String expected = p.getProperty("sha256", "");
|
||||
if (source != null && source.startsWith("http") && strongETag(etag)
|
||||
&& expected.equals(expectedSha256 == null ? "" : expectedSha256.toLowerCase(java.util.Locale.ROOT))) {
|
||||
identity.source = source;
|
||||
identity.etag = etag;
|
||||
return identity;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Legacy partials or interrupted metadata writes have no trustworthy identity.
|
||||
}
|
||||
if (!discardPartial(dest, identity)) Log.w(TAG, "could not discard unidentified partial " + dest);
|
||||
return identity;
|
||||
}
|
||||
|
||||
private static boolean discardPartial(File dest, Identity identity) {
|
||||
identity.source = null;
|
||||
identity.etag = null;
|
||||
File marker = resumeFile(dest);
|
||||
return (!dest.exists() || dest.delete()) && (!marker.exists() || marker.delete());
|
||||
}
|
||||
|
||||
private static void saveIdentity(File dest, Identity identity, String expectedSha256) {
|
||||
File marker = resumeFile(dest);
|
||||
if (!strongETag(identity.etag)) {
|
||||
marker.delete();
|
||||
return;
|
||||
}
|
||||
Properties p = new Properties();
|
||||
p.setProperty("source", identity.source);
|
||||
p.setProperty("etag", identity.etag);
|
||||
p.setProperty("sha256", expectedSha256 == null ? "" : expectedSha256.toLowerCase(java.util.Locale.ROOT));
|
||||
try (FileOutputStream out = new FileOutputStream(marker)) {
|
||||
p.store(out, "DroidDeck partial download validator");
|
||||
} catch (IOException e) {
|
||||
marker.delete(); // No valid metadata means no future resume.
|
||||
Log.w(TAG, "cannot persist download validator", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Outcome attempt(String url, File destination, boolean resume, Identity identity,
|
||||
String expectedSha256, java.util.function.Consumer<Float> progress) {
|
||||
HttpURLConnection connection = null;
|
||||
long have = resume && destination.isFile() ? destination.length() : 0;
|
||||
try {
|
||||
connection = open(url);
|
||||
if (have > 0) connection.setRequestProperty("Range", "bytes=" + have + "-");
|
||||
if (have > 0 && (!url.equals(identity.source) || !strongETag(identity.etag))) {
|
||||
// A new source or unvalidated bytes must restart from byte zero.
|
||||
if (!discardPartial(destination, identity)) return Outcome.RETRY;
|
||||
have = 0;
|
||||
}
|
||||
connection = open(url, STALL_TIMEOUT_MS);
|
||||
if (have > 0) {
|
||||
connection.setRequestProperty("Range", "bytes=" + have + "-");
|
||||
connection.setRequestProperty("If-Range", identity.etag);
|
||||
}
|
||||
int code = connection.getResponseCode();
|
||||
boolean appending = code == HttpURLConnection.HTTP_PARTIAL;
|
||||
if (appending && !rangeStartsAt(connection.getHeaderField("Content-Range"), have)) {
|
||||
// A 206 that does not begin where the kept bytes end would corrupt the file if appended.
|
||||
if (have > 0 && code == HTTP_RANGE_NOT_SATISFIABLE) {
|
||||
connection.disconnect();
|
||||
destination.delete();
|
||||
return downloadFile(url, destination, false, progress);
|
||||
if (!discardPartial(destination, identity)) return Outcome.RETRY;
|
||||
return attempt(url, destination, false, identity, expectedSha256, progress);
|
||||
}
|
||||
if (code / 100 != 2) {
|
||||
if (have > 0) {
|
||||
connection.disconnect();
|
||||
destination.delete();
|
||||
return downloadFile(url, destination, false, progress);
|
||||
}
|
||||
Log.w(TAG, url + " -> HTTP " + code);
|
||||
return false;
|
||||
return code == HttpURLConnection.HTTP_NOT_FOUND || code == HttpURLConnection.HTTP_GONE
|
||||
? Outcome.FATAL : Outcome.RETRY;
|
||||
}
|
||||
boolean appending = code == HttpURLConnection.HTTP_PARTIAL;
|
||||
if (appending && !rangeStartsAt(connection.getHeaderField("Content-Range"), have)) {
|
||||
connection.disconnect();
|
||||
if (!discardPartial(destination, identity)) return Outcome.RETRY;
|
||||
return attempt(url, destination, false, identity, expectedSha256, progress);
|
||||
}
|
||||
String etag = connection.getHeaderField("ETag");
|
||||
if (have > 0 && appending && (!strongETag(etag) || !etag.equals(identity.etag))) {
|
||||
connection.disconnect();
|
||||
if (!discardPartial(destination, identity)) return Outcome.RETRY;
|
||||
return attempt(url, destination, false, identity, expectedSha256, progress);
|
||||
}
|
||||
if (!appending && have > 0) {
|
||||
// If-Range may legitimately return full 200 when representation changed.
|
||||
if (!discardPartial(destination, identity)) return Outcome.RETRY;
|
||||
have = 0;
|
||||
}
|
||||
if (!appending) have = 0;
|
||||
long length = connection.getContentLengthLong();
|
||||
long total = length < 0 ? -1 : length + have;
|
||||
long total = appending ? rangeTotal(connection.getHeaderField("Content-Range")) : -1;
|
||||
if (total < 0) total = length < 0 ? -1 : length + have;
|
||||
File parent = destination.getParentFile();
|
||||
if (parent != null && !parent.isDirectory())
|
||||
parent.mkdirs();
|
||||
if (parent != null && !parent.isDirectory()) parent.mkdirs();
|
||||
identity.source = url;
|
||||
identity.etag = strongETag(etag) ? etag : null;
|
||||
saveIdentity(destination, identity, expectedSha256);
|
||||
long written = have;
|
||||
try (InputStream in = new BufferedInputStream(connection.getInputStream(), 1 << 16);
|
||||
OutputStream out = new FileOutputStream(destination, appending)) {
|
||||
@@ -83,11 +215,11 @@ public final class Downloader {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (progress != null) progress.accept(1f);
|
||||
return total <= 0 || written >= total;
|
||||
if (total > 0 && written < total) return Outcome.RETRY;
|
||||
return Outcome.DONE;
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "download " + url, e);
|
||||
return false;
|
||||
return Outcome.RETRY;
|
||||
} finally {
|
||||
if (connection != null) connection.disconnect();
|
||||
}
|
||||
@@ -100,10 +232,49 @@ public final class Downloader {
|
||||
return m.matches() && Long.parseLong(m.group(1)) == start;
|
||||
}
|
||||
|
||||
private static HttpURLConnection open(String url) throws java.io.IOException {
|
||||
/** The complete length in a `Content-Range` header, or -1 when the header leaves it unknown. */
|
||||
static long rangeTotal(String contentRange) {
|
||||
if (contentRange == null) return -1;
|
||||
java.util.regex.Matcher m = java.util.regex.Pattern.compile("^\\s*bytes\\s+\\d+-\\d+/(\\d+)\\s*$").matcher(contentRange);
|
||||
return m.matches() ? Long.parseLong(m.group(1)) : -1;
|
||||
}
|
||||
|
||||
private static final class Fetch {
|
||||
final String body;
|
||||
final boolean fatal;
|
||||
|
||||
Fetch(String body, boolean fatal) {
|
||||
this.body = body;
|
||||
this.fatal = fatal;
|
||||
}
|
||||
}
|
||||
|
||||
private static Fetch fetch(String url) {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
connection = open(url, STALL_TIMEOUT_MS);
|
||||
int code = connection.getResponseCode();
|
||||
if (code / 100 != 2) {
|
||||
Log.w(TAG, url + " -> HTTP " + code);
|
||||
return new Fetch(null, code == HttpURLConnection.HTTP_NOT_FOUND || code == HttpURLConnection.HTTP_GONE);
|
||||
}
|
||||
try (InputStream in = connection.getInputStream();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
FileUtils.copy(in, out);
|
||||
return new Fetch(new String(out.toByteArray(), StandardCharsets.UTF_8), false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "GET " + url, e);
|
||||
return new Fetch(null, false);
|
||||
} finally {
|
||||
if (connection != null) connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpURLConnection open(String url, int readTimeoutMs) throws java.io.IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setConnectTimeout(TIMEOUT_MS);
|
||||
connection.setReadTimeout(TIMEOUT_MS);
|
||||
connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
||||
connection.setReadTimeout(readTimeoutMs);
|
||||
connection.setInstanceFollowRedirects(true);
|
||||
connection.setRequestProperty("User-Agent", "DroidDeck-Android");
|
||||
return connection;
|
||||
|
||||
@@ -26,10 +26,12 @@ object PhantomProcessLimit {
|
||||
usesDeviceConfig(sdk) && enabled -> listOf(
|
||||
"device_config delete activity_manager max_phantom_processes",
|
||||
"device_config set_sync_disabled_for_tests none",
|
||||
"settings put global $SETTING true",
|
||||
)
|
||||
usesDeviceConfig(sdk) -> listOf(
|
||||
"device_config set_sync_disabled_for_tests persistent",
|
||||
"device_config put activity_manager max_phantom_processes $MAX_PHANTOM",
|
||||
"settings put global $SETTING false",
|
||||
)
|
||||
else -> listOf("settings put global $SETTING ${if (enabled) "true" else "false"}")
|
||||
}
|
||||
@@ -57,7 +59,12 @@ object PhantomProcessLimit {
|
||||
if (sdk < Build.VERSION_CODES.S) return PhantomProcessStatus.NOT_APPLICABLE
|
||||
if (usesDeviceConfig(sdk)) {
|
||||
val off = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getBoolean(PREF_ANDROID_12_OFF, false)
|
||||
return if (off) PhantomProcessStatus.DISABLED else PhantomProcessStatus.UNREADABLE
|
||||
val global = try {
|
||||
Settings.Global.getString(context.contentResolver, SETTING)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
return android12Status(off, global, systemProperty(OVERRIDE_PROPERTY))
|
||||
}
|
||||
val global = try {
|
||||
Settings.Global.getString(context.contentResolver, SETTING)
|
||||
@@ -67,6 +74,10 @@ object PhantomProcessLimit {
|
||||
return status(global, systemProperty(OVERRIDE_PROPERTY))
|
||||
}
|
||||
|
||||
fun android12Status(rememberedOff: Boolean, global: String?, override: String?): PhantomProcessStatus =
|
||||
if (rememberedOff || status(global, override) == PhantomProcessStatus.DISABLED) PhantomProcessStatus.DISABLED
|
||||
else PhantomProcessStatus.UNREADABLE
|
||||
|
||||
fun status(global: String?, override: String?): PhantomProcessStatus {
|
||||
val value = global?.trim()?.lowercase()?.takeIf { it.isNotEmpty() }
|
||||
?: override?.trim()?.lowercase()?.takeIf { it.isNotEmpty() }
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.os.Process
|
||||
import android.util.Log
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
object SessionLogCapture {
|
||||
private const val TAG = "SessionLogCapture"
|
||||
@@ -11,6 +12,7 @@ object SessionLogCapture {
|
||||
private var pid = -1
|
||||
private var writer: BufferedWriter? = null
|
||||
private var target: File? = null
|
||||
private var syncer: Thread? = null
|
||||
|
||||
@Synchronized
|
||||
fun start(target: File) {
|
||||
@@ -37,6 +39,7 @@ object SessionLogCapture {
|
||||
out.write("logcat could not be started; this ROM may not hand an app its own entries.\n")
|
||||
out.flush()
|
||||
}
|
||||
target.parentFile?.let { syncer = startSyncing(it) }
|
||||
Log.i(TAG, "app log -> $target (logcat pid $pid)")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "could not start the app log", e)
|
||||
@@ -49,8 +52,19 @@ object SessionLogCapture {
|
||||
if (t.parentFile?.absolutePath == dir.absolutePath) stop()
|
||||
}
|
||||
|
||||
/** flush() only reaches the page cache, and a hard reset on f2fs lost the last minutes of every
|
||||
* log. Every 2 s the session folder's files (ours and the native parts') are synced to storage. */
|
||||
private fun startSyncing(dir: File): Thread = Thread({
|
||||
while (true) {
|
||||
try { Thread.sleep(2000) } catch (e: InterruptedException) { break }
|
||||
dir.listFiles()?.forEach { runCatching { FileOutputStream(it, true).use { s -> s.fd.sync() } } }
|
||||
}
|
||||
}, "session-log-sync").apply { isDaemon = true; start() }
|
||||
|
||||
@Synchronized
|
||||
fun stop() {
|
||||
syncer?.interrupt()
|
||||
syncer = null
|
||||
target = null
|
||||
if (pid != -1) {
|
||||
try {
|
||||
|
||||
@@ -22,7 +22,7 @@ object AddedGameArt {
|
||||
}
|
||||
|
||||
enum class Slot(val key: String, val cacheName: String, val assetType: Int?) {
|
||||
COVER("p", "p.jpg", 0), BACKGROUND("hero", "hero.jpg", 1), LOGO("logo", "logo.png", 2), ICON("icon", "icon.png", null),
|
||||
COVER("p", "p.jpg", 0), WIDE("header", "header.jpg", 3), BACKGROUND("hero", "hero.jpg", 1), LOGO("logo", "logo.png", 2), ICON("icon", "icon.png", null),
|
||||
}
|
||||
|
||||
const val FOLDER = "folder"
|
||||
@@ -40,10 +40,10 @@ object AddedGameArt {
|
||||
|
||||
class Found<T>(val items: List<T>, val status: SteamGridDb.Status)
|
||||
|
||||
private val CHOSEN = Regex("""chosen-(\w+)\.(\w+)""")
|
||||
private val CHOSEN = Regex("""chosen-(p|header|hero|logo|icon)(?:-\d+)?\.(\w+)""")
|
||||
|
||||
private fun chosen(cache: File, slot: Slot): File? =
|
||||
cache.listFiles { f -> CHOSEN.matchEntire(f.name)?.groupValues?.get(1) == slot.key }?.firstOrNull()
|
||||
internal fun chosen(cache: File, slot: Slot): File? =
|
||||
cache.listFiles { f -> CHOSEN.matchEntire(f.name)?.groupValues?.get(1) == slot.key }?.maxWithOrNull(compareBy({ it.lastModified() }, { it.name }))
|
||||
|
||||
private val IMAGE = listOf("png", "jpg", "jpeg")
|
||||
private val PORTRAIT = listOf("cover", "poster", "grid", "boxart", "capsule", "folder", "library_600x900", "portrait")
|
||||
@@ -79,7 +79,7 @@ object AddedGameArt {
|
||||
fun cached(name: String) = File(cache, name).takeIf { it.isFile }
|
||||
return Art(
|
||||
portrait = chosen(cache, Slot.COVER) ?: own.portrait ?: cached("p.jpg") ?: exeArt(context, game, cover = true),
|
||||
header = own.header ?: cached("header.jpg") ?: exeArt(context, game, cover = false),
|
||||
header = chosen(cache, Slot.WIDE) ?: own.header ?: cached("header.jpg") ?: exeArt(context, game, cover = false),
|
||||
hero = chosen(cache, Slot.BACKGROUND) ?: own.hero ?: cached("hero.jpg"),
|
||||
logo = chosen(cache, Slot.LOGO) ?: own.logo ?: cached("logo.png"),
|
||||
icon = chosen(cache, Slot.ICON) ?: own.icon ?: cached("icon-sgdb.png") ?: exeIcon(context, game),
|
||||
@@ -96,6 +96,7 @@ object AddedGameArt {
|
||||
chosen(cache, slot)?.let { return@map SlotState(slot, it, chosenSource(context, game, slot).first.ifEmpty { FILE }, true) }
|
||||
val auto: Pair<File, String>? = when (slot) {
|
||||
Slot.COVER -> own.portrait?.let { it to FOLDER } ?: cached("p.jpg") ?: exeArt(context, game, cover = true)?.let { it to EXE }
|
||||
Slot.WIDE -> own.header?.let { it to FOLDER } ?: cached("header.jpg") ?: exeArt(context, game, cover = false)?.let { it to EXE }
|
||||
Slot.BACKGROUND -> own.hero?.let { it to FOLDER } ?: cached("hero.jpg")
|
||||
Slot.LOGO -> own.logo?.let { it to FOLDER } ?: cached("logo.png")
|
||||
Slot.ICON -> own.icon?.let { it to FOLDER } ?: File(cache, "icon-sgdb.png").takeIf { it.isFile }?.let { it to SGDB }
|
||||
@@ -117,36 +118,37 @@ object AddedGameArt {
|
||||
folderImages(game.folder).forEach { out.add(Option(FOLDER, it.path, it.path)) }
|
||||
val cache = cacheDir(context, game)
|
||||
val steamId = SteamMatch.resolve(context, game.folder.path, game.exe, game.name, legacy = File(cache, "steam-appid")).appId?.toString()
|
||||
val remote = when (slot) { Slot.COVER -> "library_600x900.jpg"; Slot.BACKGROUND -> "library_hero.jpg"; Slot.LOGO -> "logo.png"; Slot.ICON -> null }
|
||||
if (steamId != null && remote != null) {
|
||||
val url = "$CDN$steamId/$remote".takeIf { exists(it) } ?: steamAssetUrl(steamId, remote)
|
||||
if (url != null) out.add(Option(STEAM, url, url))
|
||||
}
|
||||
var status = SteamGridDb.Status.OK
|
||||
val key = SteamGridDb.key(context)
|
||||
if (key.isEmpty()) {
|
||||
status = SteamGridDb.Status.NO_KEY
|
||||
} else {
|
||||
cache.mkdirs()
|
||||
try {
|
||||
val gridId = remembered(File(cache, "sgdb-id"), SteamGridDb.keyChangedAt(context), retryMiss = true) {
|
||||
SteamGridDb.gameId(key, steamId?.toIntOrNull(), game.name)?.toString()
|
||||
}?.toIntOrNull()
|
||||
if (gridId == null) status = SteamGridDb.Status.NO_GAME
|
||||
else SteamGridDb.images(key, gridId, slot, if (slot == Slot.ICON) 3 else 5)
|
||||
.forEach { (thumb, full) -> out.add(Option(SGDB, thumb, full)) }
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "${game.name}: SteamGridDB failed (${e.javaClass.simpleName})")
|
||||
status = (e as? SteamGridDb.Failed)?.status ?: SteamGridDb.Status.SERVER
|
||||
}
|
||||
}
|
||||
if (steamId != null) steamOption(steamId, slot)?.let { out.add(it) }
|
||||
val (found, status) = sgdbChoices(context, File(cache, "sgdb-id"), steamId?.toIntOrNull(), game.name, slot)
|
||||
out.addAll(found)
|
||||
return Choices(out, status)
|
||||
}
|
||||
|
||||
internal fun steamOption(steamId: String, slot: Slot): Option? {
|
||||
val remote = when (slot) { Slot.COVER -> "library_600x900.jpg"; Slot.WIDE -> "header.jpg"; Slot.BACKGROUND -> "library_hero.jpg"; Slot.LOGO -> "logo.png"; Slot.ICON -> null } ?: return null
|
||||
val url = "$CDN$steamId/$remote".takeIf { exists(it) } ?: steamAssetUrl(steamId, remote) ?: return null
|
||||
return Option(STEAM, url, url)
|
||||
}
|
||||
|
||||
internal fun sgdbChoices(context: Context, marker: File, steamId: Int?, name: String, slot: Slot): Pair<List<Option>, SteamGridDb.Status> {
|
||||
val key = SteamGridDb.key(context)
|
||||
if (key.isEmpty()) return emptyList<Option>() to SteamGridDb.Status.NO_KEY
|
||||
marker.parentFile?.mkdirs()
|
||||
return try {
|
||||
val gridId = remembered(marker, SteamGridDb.keyChangedAt(context), retryMiss = true) {
|
||||
SteamGridDb.gameId(key, steamId, name)?.toString()
|
||||
}?.toIntOrNull() ?: return emptyList<Option>() to SteamGridDb.Status.NO_GAME
|
||||
SteamGridDb.images(key, gridId, slot, if (slot == Slot.ICON) 3 else 5).map { (thumb, full) -> Option(SGDB, thumb, full) } to SteamGridDb.Status.OK
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "$name: SteamGridDB failed (${e.javaClass.simpleName})")
|
||||
emptyList<Option>() to ((e as? SteamGridDb.Failed)?.status ?: SteamGridDb.Status.SERVER)
|
||||
}
|
||||
}
|
||||
|
||||
fun searchGames(context: Context, term: String): Found<SteamGridDb.Game> = sgdb(context) { SteamGridDb.search(it, term) }
|
||||
|
||||
fun gameImages(context: Context, id: Int, slot: Slot, page: Int): Found<Option> = sgdb(context) { key ->
|
||||
SteamGridDb.page(key, id, slot, page).map { (thumb, full) -> Option(SGDB, thumb, full) }
|
||||
fun gameImages(context: Context, id: Int, slot: Slot): Found<Option> = sgdb(context) { key ->
|
||||
SteamGridDb.all(key, id, slot).map { (thumb, full) -> Option(SGDB, thumb, full) }
|
||||
}
|
||||
|
||||
private fun <T> sgdb(context: Context, call: (String) -> List<T>): Found<T> {
|
||||
@@ -161,19 +163,38 @@ object AddedGameArt {
|
||||
}
|
||||
|
||||
fun choose(context: Context, game: AddedGames.Game, slot: Slot, option: Option): Boolean {
|
||||
val bytes = if (option.full.startsWith("https://")) get(option.full) else runCatching { File(option.full).readBytes() }.getOrNull()
|
||||
bytes ?: return false
|
||||
val ext = option.full.substringAfterLast('.', "png").substringBefore('?').lowercase().takeIf { it in CHOOSABLE + "ico" } ?: "png"
|
||||
val cache = cacheDir(context, game).apply { mkdirs() }
|
||||
chosen(cache, slot)?.delete()
|
||||
val dst = File(cache, "chosen-${slot.key}.${if (ext == "jpeg") "jpg" else ext}")
|
||||
val tmp = File(cache, dst.name + ".tmp")
|
||||
tmp.writeBytes(bytes)
|
||||
if (!tmp.renameTo(dst)) return false
|
||||
if (!store(cacheDir(context, game), slot, option)) return false
|
||||
SessionPrefs.setAddedGameArtSource(context, game.folder.path, slot.key, option.group + "|" + option.full)
|
||||
return true
|
||||
}
|
||||
|
||||
internal fun store(cache: File, slot: Slot, option: Option): Boolean {
|
||||
val fresh = storeNew(cache, slot, option) ?: return false
|
||||
dropOlder(cache, slot, fresh)
|
||||
return true
|
||||
}
|
||||
|
||||
internal fun dropOlder(cache: File, slot: Slot, keep: File) {
|
||||
cache.listFiles { f -> f != keep && CHOSEN.matchEntire(f.name)?.groupValues?.get(1) == slot.key }?.forEach { it.delete() }
|
||||
}
|
||||
|
||||
private val lastStamp = java.util.concurrent.atomic.AtomicLong(0)
|
||||
|
||||
private fun nextStamp(): Long = lastStamp.updateAndGet { maxOf(it + 1, System.currentTimeMillis()) }
|
||||
|
||||
internal fun storeNew(cache: File, slot: Slot, option: Option): File? {
|
||||
val bytes = if (option.full.startsWith("https://")) get(option.full) else runCatching { File(option.full).readBytes() }.getOrNull()
|
||||
bytes ?: return null
|
||||
val ext = option.full.substringAfterLast('.', "png").substringBefore('?').lowercase().takeIf { it in CHOOSABLE + "ico" } ?: "png"
|
||||
cache.mkdirs()
|
||||
val suffix = if (ext == "jpeg") "jpg" else ext
|
||||
var dst = File(cache, "chosen-${slot.key}-" + "%013d".format(nextStamp()) + "." + suffix)
|
||||
while (dst.exists()) dst = File(cache, "chosen-${slot.key}-" + "%013d".format(nextStamp()) + "." + suffix)
|
||||
val tmp = File(cache, dst.name + ".tmp")
|
||||
tmp.writeBytes(bytes)
|
||||
return if (tmp.renameTo(dst)) dst else null
|
||||
}
|
||||
|
||||
fun chosenSource(context: Context, game: AddedGames.Game, slot: Slot): Pair<String, String> {
|
||||
val value = SessionPrefs.addedGameArtSource(context, game.folder.path, slot.key)
|
||||
return value.substringBefore('|') to value.substringAfter('|', "")
|
||||
|
||||
@@ -36,19 +36,34 @@ object AddedGameEdits {
|
||||
|
||||
fun setArt(context: Context, folder: String, slot: AddedGameArt.Slot, option: AddedGameArt.Option): Boolean {
|
||||
val game = game(context, folder) ?: return false
|
||||
if (!AddedGameArt.choose(context, game, slot, option)) return false
|
||||
applyArt(context, game, slot)
|
||||
return true
|
||||
return synchronized(SteamGrid.lock) {
|
||||
if (!AddedGameArt.choose(context, game, slot, option)) return@synchronized false
|
||||
applyArt(context, game, slot)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun resetArt(context: Context, folder: String, slot: AddedGameArt.Slot) {
|
||||
val game = game(context, folder) ?: return
|
||||
AddedGameArt.reset(context, game, slot)
|
||||
applyArt(context, game, slot)
|
||||
synchronized(SteamGrid.lock) {
|
||||
AddedGameArt.reset(context, game, slot)
|
||||
applyArt(context, game, slot)
|
||||
}
|
||||
}
|
||||
|
||||
fun logoPosition(context: Context, folder: String): LogoPosition.Value? =
|
||||
game(context, folder)?.let { SteamGrid.position(SteamGrid.target(it, context)) }
|
||||
|
||||
fun setLogoPosition(context: Context, folder: String, value: LogoPosition.Value?): Boolean {
|
||||
val game = game(context, folder) ?: return false
|
||||
if (!SteamGrid.writePosition(context, SteamGrid.target(game, context), value)) return false
|
||||
publish(context) { SteamLiveShortcuts.setLogoPosition(context, game.appId, value) }
|
||||
return true
|
||||
}
|
||||
|
||||
private fun applyArt(context: Context, game: AddedGames.Game, slot: AddedGameArt.Slot) {
|
||||
val file = AddedGameArt.describe(context, game).first { it.slot == slot }.file
|
||||
if (slot != AddedGameArt.Slot.ICON) SteamGrid.write(context, SteamGrid.target(game, context), slot, AddedGameArt.chosen(AddedGameArt.cacheDir(context, game), slot))
|
||||
publish(context) {
|
||||
if (slot == AddedGameArt.Slot.ICON) SteamLiveShortcuts.setArt(context, game.appId, null, null, iconPath = file?.absolutePath.orEmpty())
|
||||
else SteamLiveShortcuts.setArt(context, game.appId, slot.assetType, file)
|
||||
|
||||
@@ -330,7 +330,7 @@ object AddedGames {
|
||||
}
|
||||
}
|
||||
|
||||
private fun accountConfig(root: File): File? {
|
||||
internal fun accountConfig(root: File): File? {
|
||||
val users = runCatching { File(root, "config/loginusers.vdf").readText() }.getOrDefault("")
|
||||
val recent = Regex(""""(\d{5,})"\s*\{([^}]*)\}""").findAll(users)
|
||||
.lastOrNull { Regex(""""MostRecent"\s*"1"""").containsMatchIn(it.groupValues[2]) }
|
||||
|
||||
@@ -332,7 +332,7 @@ object AutoComponents {
|
||||
caps.hasCapability(android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
}
|
||||
|
||||
private fun constrained(context: Context): Boolean {
|
||||
fun constrained(context: Context): Boolean {
|
||||
val power = context.getSystemService(PowerManager::class.java)
|
||||
if (power?.isPowerSaveMode == true) return true
|
||||
if (Build.VERSION.SDK_INT >= 29 && (power?.currentThermalStatus ?: 0) >= PowerManager.THERMAL_STATUS_MODERATE) return true
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.droiddeck.launcher.frontend
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.core.DownloadSource
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
@@ -199,24 +200,62 @@ object CoverArt {
|
||||
setRequestProperty("User-Agent", "DroidDeck")
|
||||
}
|
||||
|
||||
private fun exists(url: String): Boolean = try {
|
||||
val c = open(url, "HEAD"); val ok = c.responseCode == 200; c.disconnect(); ok
|
||||
} catch (e: Exception) { false }
|
||||
private fun <T> through(url: String, attempt: (String, Boolean) -> T?): T? {
|
||||
for (source in DownloadSource.candidates(url)) {
|
||||
val result = attempt(source, source == url)
|
||||
if (result != null) {
|
||||
DownloadSource.succeeded(source)
|
||||
return result
|
||||
}
|
||||
}
|
||||
DownloadSource.failed()
|
||||
return null
|
||||
}
|
||||
|
||||
private fun get(url: String): String? = try {
|
||||
private fun exists(url: String): Boolean = through(url) { source, original -> head(source, original) } ?: false
|
||||
|
||||
private fun head(url: String, original: Boolean): Boolean? = try {
|
||||
val c = open(url, "HEAD")
|
||||
val code = c.responseCode
|
||||
c.disconnect()
|
||||
when {
|
||||
code == 200 -> true
|
||||
code == 404 || code == 410 -> if (original) false else null
|
||||
else -> null
|
||||
}
|
||||
} catch (e: Exception) { null }
|
||||
|
||||
private fun get(url: String): String? = through(url) { source, original -> text(source, original) }
|
||||
|
||||
private fun text(url: String, original: Boolean): String? = try {
|
||||
val c = open(url)
|
||||
if (c.responseCode != 200) { c.disconnect(); null } else c.inputStream.bufferedReader().use { it.readText() }
|
||||
val code = c.responseCode
|
||||
when {
|
||||
code == 200 -> c.inputStream.bufferedReader().use { it.readText() }.takeIf { it.isNotEmpty() }
|
||||
code == 404 || code == 410 -> { c.disconnect(); if (original) "" else null }
|
||||
else -> { c.disconnect(); null }
|
||||
}
|
||||
} catch (e: Exception) { Log.w(TAG, "$url: $e"); null }
|
||||
|
||||
private fun download(url: String, target: File): Boolean = try {
|
||||
private fun download(url: String, target: File): Boolean = through(url) { source, original -> fetch(source, target, original) } ?: false
|
||||
|
||||
private fun fetch(url: String, target: File, original: Boolean): Boolean? = try {
|
||||
val c = open(url)
|
||||
if (c.responseCode != 200) { c.disconnect(); false } else {
|
||||
target.parentFile?.mkdirs()
|
||||
val tmp = File(target.path + ".part")
|
||||
c.inputStream.use { input -> tmp.outputStream().use { input.copyTo(it) } }
|
||||
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeFile(tmp.path, bounds)
|
||||
if (bounds.outWidth > 0 && tmp.renameTo(target)) true else { tmp.delete(); false }
|
||||
when (val code = c.responseCode) {
|
||||
200 -> {
|
||||
target.parentFile?.mkdirs()
|
||||
val tmp = File(target.path + ".part")
|
||||
c.inputStream.use { input -> tmp.outputStream().use { input.copyTo(it) } }
|
||||
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeFile(tmp.path, bounds)
|
||||
when {
|
||||
bounds.outWidth > 0 && tmp.renameTo(target) -> true
|
||||
original -> { tmp.delete(); false }
|
||||
else -> { tmp.delete(); null }
|
||||
}
|
||||
}
|
||||
404, 410 -> { c.disconnect(); if (original) false else null }
|
||||
else -> { c.disconnect(); null }
|
||||
}
|
||||
} catch (e: Exception) { Log.w(TAG, "$url: $e"); false }
|
||||
} catch (e: Exception) { Log.w(TAG, "$url: $e"); null }
|
||||
}
|
||||
@@ -17,6 +17,9 @@ object Library {
|
||||
val source: String = SOURCE_STEAM,
|
||||
val storeId: String? = null,
|
||||
) {
|
||||
fun withArt(art: File?, hero: File?, icon: File?) =
|
||||
SteamGame(appId, name, art, library, gameId, hero, lastPlayed, gameFiles, protonPrefix, icon, source, storeId)
|
||||
|
||||
val gameIdString: String get() = java.lang.Long.toUnsignedString(gameId)
|
||||
val profileKey: String get() = if (library == ADDED) java.lang.Integer.toUnsignedString(appId) else appId.toString()
|
||||
}
|
||||
@@ -69,7 +72,7 @@ object Library {
|
||||
private val LAST_PLAYED = Regex("^\\s*\"LastPlayed\"\\s*\"(\\d+)\"", RegexOption.MULTILINE)
|
||||
private val INSTALL_DIR = Regex("^\\s*\"installdir\"\\s*\"([^\"]*)\"", RegexOption.MULTILINE)
|
||||
|
||||
private fun steamLibraries(context: Context): List<Pair<File, String>> {
|
||||
internal fun steamLibraries(context: Context): List<Pair<File, String>> {
|
||||
val root = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam")
|
||||
return listOfNotNull(
|
||||
root to "internal",
|
||||
@@ -116,13 +119,13 @@ object Library {
|
||||
val installDir = INSTALL_DIR.find(text)?.groupValues?.get(1)?.trim()
|
||||
?.takeIf { it.isNotEmpty() && it != "." && it != ".." && '/' !in it && '\\' !in it }
|
||||
val gameFiles = installDir?.let { File(steamapps, "common/$it").takeIf(File::isDirectory) }
|
||||
out[appId] = SteamGame(
|
||||
out[appId] = SteamGameArt.withOverrides(context, SteamGame(
|
||||
appId, name, art, label, hero = hero, lastPlayed = lastPlayed,
|
||||
gameFiles = gameFiles,
|
||||
protonPrefix = protonPrefix(context, appId.toLong(), library, roots),
|
||||
icon = steamCacheImage(cache, appId, listOf("icon.jpg", "icon.png"))
|
||||
?: File(cache, appId.toString()).listFiles()?.firstOrNull { it.isFile && STEAM_ICON.matches(it.name) },
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
return out.values.toList()
|
||||
@@ -138,7 +141,11 @@ object Library {
|
||||
|
||||
fun launchableGames(context: Context, strictRead: Boolean = false, added: List<AddedGames.Game>? = null): List<SteamGame> {
|
||||
val roots = steamLibraries(context).map { it.first }
|
||||
return (steamGames(context, strictRead) + (added ?: AddedGames.scan(context)).map { g -> addedGame(context, g, roots) }).distinctBy { it.gameId }
|
||||
val list = added ?: AddedGames.scan(context)
|
||||
var steam = steamGames(context, strictRead)
|
||||
val targets = steam.filter { it.library != ADDED && it.source == SOURCE_STEAM }.map { SteamGrid.target(it, context) } + list.map { SteamGrid.target(it, context) }
|
||||
if (runCatching { SteamGrid.sync(context, targets) }.getOrDefault(false)) steam = steamGames(context, strictRead)
|
||||
return (steam + list.map { g -> addedGame(context, g, roots) }).distinctBy { it.gameId }
|
||||
}
|
||||
|
||||
fun addedGame(context: Context, g: AddedGames.Game, roots: List<File> = steamLibraries(context).map { it.first }): SteamGame =
|
||||
@@ -152,7 +159,7 @@ object Library {
|
||||
)
|
||||
}
|
||||
|
||||
private fun steamCacheImage(cache: File, appId: Int, names: List<String>): File? {
|
||||
internal fun steamCacheImage(cache: File, appId: Int, names: List<String>): File? {
|
||||
val appDir = File(cache, appId.toString())
|
||||
val dirs = listOf(appDir) + appDir.listFiles()
|
||||
.orEmpty().filter { it.isDirectory }.sortedBy { it.name }
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
/** Where Steam pins a game's logo on its hero image and how big it draws it; the same JSON Steam keeps in `<appid>.json`. */
|
||||
object LogoPosition {
|
||||
enum class Pin(val steam: String) {
|
||||
UPPER_LEFT("UpperLeft"), UPPER_CENTER("UpperCenter"), CENTER("CenterCenter"), BOTTOM_LEFT("BottomLeft"), BOTTOM_CENTER("BottomCenter");
|
||||
|
||||
companion object {
|
||||
fun of(steam: String): Pin? = values().firstOrNull { it.steam == steam }
|
||||
}
|
||||
}
|
||||
|
||||
data class Value(val pin: Pin, val widthPct: Int, val heightPct: Int)
|
||||
|
||||
const val MIN_PCT = 10
|
||||
const val MAX_PCT = 100
|
||||
|
||||
/** What Steam draws when a game has no custom position. */
|
||||
val DEFAULT = Value(Pin.BOTTOM_LEFT, 50, 50)
|
||||
|
||||
private const val VERSION = 1
|
||||
private const val KEY = "logoPosition"
|
||||
|
||||
fun sized(pin: Pin, size: Int): Value = size.coerceIn(MIN_PCT, MAX_PCT).let { Value(pin, it, it) }
|
||||
|
||||
/** `{"nVersion":1,"logoPosition":{...}}`, with whatever else [into] already held kept as it was. */
|
||||
fun toJson(value: Value, into: JSONObject = JSONObject()): JSONObject = into
|
||||
.put("nVersion", VERSION)
|
||||
.put(KEY, JSONObject().put("pinnedPosition", value.pin.steam).put("nWidthPct", value.widthPct).put("nHeightPct", value.heightPct))
|
||||
|
||||
fun parse(text: String): Value? = runCatching { parse(JSONObject(text)) }.getOrNull()
|
||||
|
||||
fun parse(json: JSONObject): Value? {
|
||||
val inner = json.optJSONObject(KEY) ?: return null
|
||||
val pin = Pin.of(inner.optString("pinnedPosition")) ?: return null
|
||||
if (!inner.has("nWidthPct") || !inner.has("nHeightPct")) return null
|
||||
return Value(pin, inner.optInt("nWidthPct").coerceIn(MIN_PCT, MAX_PCT), inner.optInt("nHeightPct").coerceIn(MIN_PCT, MAX_PCT))
|
||||
}
|
||||
|
||||
/** [existing] without the logo position; null when nothing but the version number would be left. */
|
||||
fun without(existing: JSONObject): JSONObject? {
|
||||
existing.remove(KEY)
|
||||
val rest = existing.keys().asSequence().filter { it != "nVersion" }.toList()
|
||||
return if (rest.isEmpty()) null else existing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import android.content.Context
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.droiddeck.launcher.stores.SteamLiveShortcuts
|
||||
import java.io.File
|
||||
|
||||
object SteamGameArt {
|
||||
internal fun dir(context: Context, appId: Int) = File(context.filesDir, "steam-art/$appId")
|
||||
|
||||
private fun sourceFile(context: Context, appId: Int, slot: AddedGameArt.Slot) = File(dir(context, appId), "source-${slot.key}")
|
||||
|
||||
internal fun setSource(context: Context, appId: Int, slot: AddedGameArt.Slot, value: String) {
|
||||
val file = sourceFile(context, appId, slot)
|
||||
if (value.isEmpty()) file.delete() else file.apply { parentFile?.mkdirs() }.writeText(value)
|
||||
}
|
||||
|
||||
fun chosenFile(context: Context, appId: Int, slot: AddedGameArt.Slot): File? = AddedGameArt.chosen(dir(context, appId), slot)
|
||||
|
||||
fun withOverrides(context: Context, game: Library.SteamGame): Library.SteamGame {
|
||||
if (game.library == Library.ADDED || !dir(context, game.appId).isDirectory) return game
|
||||
val cover = chosenFile(context, game.appId, AddedGameArt.Slot.COVER)
|
||||
val hero = chosenFile(context, game.appId, AddedGameArt.Slot.BACKGROUND)
|
||||
val icon = chosenFile(context, game.appId, AddedGameArt.Slot.ICON)
|
||||
if (cover == null && hero == null && icon == null) return game
|
||||
return Library.SteamGame(
|
||||
game.appId, game.name, cover ?: game.art, game.library, game.gameId,
|
||||
hero ?: game.hero, game.lastPlayed, game.gameFiles, game.protonPrefix, icon ?: game.icon, game.source, game.storeId,
|
||||
)
|
||||
}
|
||||
|
||||
fun chosenSource(context: Context, appId: Int, slot: AddedGameArt.Slot): Pair<String, String> {
|
||||
val value = sourceFile(context, appId, slot).takeIf { it.isFile }?.readText()?.trim().orEmpty()
|
||||
return value.substringBefore('|') to value.substringAfter('|', "")
|
||||
}
|
||||
|
||||
fun describe(context: Context, game: Library.SteamGame): List<AddedGameArt.SlotState> {
|
||||
val cache = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/appcache/librarycache")
|
||||
val own = withOverrides(context, game)
|
||||
return AddedGameArt.Slot.values().map { slot ->
|
||||
chosenFile(context, game.appId, slot)?.let {
|
||||
return@map AddedGameArt.SlotState(slot, it, chosenSource(context, game.appId, slot).first.ifEmpty { AddedGameArt.FILE }, true)
|
||||
}
|
||||
val auto = when (slot) {
|
||||
AddedGameArt.Slot.COVER -> own.art
|
||||
AddedGameArt.Slot.WIDE -> Library.steamCacheImage(cache, game.appId, listOf("library_header.jpg", "header.jpg"))
|
||||
AddedGameArt.Slot.BACKGROUND -> own.hero
|
||||
AddedGameArt.Slot.ICON -> own.icon
|
||||
AddedGameArt.Slot.LOGO -> Library.steamCacheImage(cache, game.appId, listOf("logo.png"))
|
||||
}
|
||||
AddedGameArt.SlotState(slot, auto, if (auto != null) AddedGameArt.STEAM else AddedGameArt.AUTO, false)
|
||||
}
|
||||
}
|
||||
|
||||
fun choices(context: Context, game: Library.SteamGame, slot: AddedGameArt.Slot): AddedGameArt.Choices {
|
||||
val out = ArrayList<AddedGameArt.Option>()
|
||||
AddedGameArt.steamOption(game.appId.toString(), slot)?.let { out.add(it) }
|
||||
val (found, status) = AddedGameArt.sgdbChoices(context, File(dir(context, game.appId), "sgdb-id"), game.appId, game.name, slot)
|
||||
out.addAll(found)
|
||||
return AddedGameArt.Choices(out, status)
|
||||
}
|
||||
|
||||
fun choose(context: Context, game: Library.SteamGame, slot: AddedGameArt.Slot, option: AddedGameArt.Option): Boolean {
|
||||
return synchronized(SteamGrid.lock) {
|
||||
val folder = dir(context, game.appId)
|
||||
val fresh = AddedGameArt.storeNew(folder, slot, option) ?: return@synchronized false
|
||||
if (!push(context, game, slot, fresh)) {
|
||||
fresh.delete()
|
||||
return@synchronized false
|
||||
}
|
||||
AddedGameArt.dropOlder(folder, slot, fresh)
|
||||
setSource(context, game.appId, slot, option.group + "|" + option.full)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun reset(context: Context, game: Library.SteamGame, slot: AddedGameArt.Slot) {
|
||||
synchronized(SteamGrid.lock) {
|
||||
if (!push(context, game, slot, null)) return@synchronized
|
||||
AddedGameArt.chosen(dir(context, game.appId), slot)?.delete()
|
||||
setSource(context, game.appId, slot, "")
|
||||
}
|
||||
}
|
||||
|
||||
fun position(context: Context, game: Library.SteamGame): LogoPosition.Value? = SteamGrid.position(SteamGrid.target(game, context))
|
||||
|
||||
fun setPosition(context: Context, game: Library.SteamGame, value: LogoPosition.Value?): Boolean {
|
||||
if (!SteamGrid.writePosition(context, SteamGrid.target(game, context), value)) return false
|
||||
AddedGamesPublisher.later(context) { SteamLiveShortcuts.setLogoPosition(context, game.appId.toLong(), value) }
|
||||
return true
|
||||
}
|
||||
|
||||
private fun push(context: Context, game: Library.SteamGame, slot: AddedGameArt.Slot, file: File?): Boolean {
|
||||
val type = slot.assetType ?: return true
|
||||
if (!SteamGrid.write(context, SteamGrid.target(game, context), slot, file)) return false
|
||||
AddedGamesPublisher.later(context) { SteamLiveShortcuts.setArt(context, game.appId.toLong(), type, file) }
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
/** Steam keeps a game's custom art in `userdata/<account>/config/grid`; this is where DroidDeck's choices and Steam's meet. */
|
||||
object SteamGrid {
|
||||
private const val TAG = "SteamGrid"
|
||||
|
||||
/** Every change to a game's art, here or in a scan, takes this first so two of them never interleave. */
|
||||
internal val lock = Any()
|
||||
private const val MANIFEST = ".droiddeck-art.json"
|
||||
private val SUFFIX = mapOf(AddedGameArt.Slot.COVER to "p", AddedGameArt.Slot.WIDE to "", AddedGameArt.Slot.BACKGROUND to "_hero", AddedGameArt.Slot.LOGO to "_logo")
|
||||
private val EXTS = listOf("png", "jpg", "jpeg", "webp")
|
||||
|
||||
class Target(val artDir: File, val id: Long, val setSource: (AddedGameArt.Slot, String) -> Unit)
|
||||
|
||||
fun target(game: Library.SteamGame, context: Context) = Target(SteamGameArt.dir(context, game.appId), game.appId.toLong()) { slot, value ->
|
||||
SteamGameArt.setSource(context, game.appId, slot, value)
|
||||
}
|
||||
|
||||
fun target(game: AddedGames.Game, context: Context) = Target(AddedGameArt.cacheDir(context, game), game.appId and 0xFFFFFFFFL) { slot, value ->
|
||||
SessionPrefs.setAddedGameArtSource(context, game.folder.path, slot.key, value)
|
||||
}
|
||||
|
||||
internal fun dir(context: Context): File? {
|
||||
val root = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam")
|
||||
return AddedGames.accountConfig(root)?.let { File(it, "grid") }
|
||||
}
|
||||
|
||||
private fun files(grid: File, id: Long, slot: AddedGameArt.Slot): List<File> =
|
||||
EXTS.map { File(grid, "$id${SUFFIX[slot]}.$it") }.filter { it.isFile }
|
||||
|
||||
private fun signature(file: File?) = if (file == null) "" else "${file.length()}:${file.lastModified()}"
|
||||
|
||||
private fun sigFile(target: Target, slot: AddedGameArt.Slot) = File(target.artDir, "grid-${slot.key}")
|
||||
|
||||
private fun recorded(target: Target, slot: AddedGameArt.Slot): String? = sigFile(target, slot).takeIf { it.isFile }?.readText()
|
||||
|
||||
private fun record(target: Target, slot: AddedGameArt.Slot, value: String) {
|
||||
target.artDir.mkdirs()
|
||||
sigFile(target, slot).writeText(value)
|
||||
}
|
||||
|
||||
private fun writtenByDroidDeck(grid: File): Set<String> = runCatching {
|
||||
val manifest = JSONObject(File(grid, MANIFEST).readText())
|
||||
manifest.keys().asSequence().filter { name -> File(grid, name).let { it.isFile && it.length() == manifest.optLong(name, -1L) } }.toSet()
|
||||
}.getOrDefault(emptySet())
|
||||
|
||||
/** Puts a choice (or, with null, the lack of one) where Steam reads it, whether or not Steam is running. */
|
||||
fun write(context: Context, target: Target, slot: AddedGameArt.Slot, file: File?): Boolean = synchronized(lock) { place(context, target, slot, file) }
|
||||
|
||||
private fun place(context: Context, target: Target, slot: AddedGameArt.Slot, file: File?): Boolean {
|
||||
val suffix = SUFFIX[slot] ?: return true
|
||||
val grid = dir(context) ?: return true
|
||||
if (file == null) {
|
||||
if (files(grid, target.id, slot).map { it.delete() }.any { !it }) return false
|
||||
record(target, slot, "")
|
||||
return true
|
||||
}
|
||||
val ext = file.extension.lowercase().let { if (it == "jpeg") "jpg" else it }
|
||||
if (ext !in EXTS) return false
|
||||
grid.mkdirs()
|
||||
val dst = File(grid, "${target.id}$suffix.$ext")
|
||||
val tmp = File(grid, dst.name + ".tmp")
|
||||
val placed = runCatching { file.copyTo(tmp, overwrite = true); if (tmp.renameTo(dst)) dst else null }.getOrNull()
|
||||
if (placed == null) {
|
||||
tmp.delete()
|
||||
return false
|
||||
}
|
||||
if (files(grid, target.id, slot).filter { it != dst }.map { it.delete() }.any { !it }) return false
|
||||
record(target, slot, signature(placed))
|
||||
return true
|
||||
}
|
||||
|
||||
private const val POSITION = "logo-position"
|
||||
private const val POSITION_SIG = "grid-json"
|
||||
|
||||
private fun positionFile(grid: File, id: Long) = File(grid, "$id.json")
|
||||
|
||||
private fun positionIn(file: File): LogoPosition.Value? = file.takeIf { it.isFile }?.let { runCatching { it.readText() }.getOrNull() }?.let(LogoPosition::parse)
|
||||
|
||||
/** The logo position chosen here (or taken over from Steam) for a game; null when Steam's own default applies. */
|
||||
fun position(target: Target): LogoPosition.Value? = positionIn(File(target.artDir, POSITION))
|
||||
|
||||
private fun positionRecorded(target: Target): String? = File(target.artDir, POSITION_SIG).takeIf { it.isFile }?.readText()
|
||||
|
||||
private fun recordPosition(target: Target, value: String) {
|
||||
target.artDir.mkdirs()
|
||||
File(target.artDir, POSITION_SIG).writeText(value)
|
||||
}
|
||||
|
||||
private fun storePosition(target: Target, value: LogoPosition.Value?): Boolean {
|
||||
val file = File(target.artDir, POSITION)
|
||||
if (value == null) return !file.exists() || file.delete()
|
||||
target.artDir.mkdirs()
|
||||
return runCatching { file.writeText(LogoPosition.toJson(value).toString()) }.isSuccess
|
||||
}
|
||||
|
||||
/** Sets (or with null, clears) a game's logo position here and in the client's grid folder, whether or not Steam is running. */
|
||||
fun writePosition(context: Context, target: Target, value: LogoPosition.Value?): Boolean = synchronized(lock) {
|
||||
placePosition(context, target, value) && storePosition(target, value)
|
||||
}
|
||||
|
||||
/** Steam keeps the position in `<id>.json`; anything else that file holds is left as it was. */
|
||||
private fun placePosition(context: Context, target: Target, value: LogoPosition.Value?): Boolean {
|
||||
val grid = dir(context) ?: return true
|
||||
val file = positionFile(grid, target.id)
|
||||
val existing = if (file.isFile) runCatching { JSONObject(file.readText()) }.getOrNull() else null
|
||||
if (file.isFile && existing == null) return value == null
|
||||
val next = if (value == null) existing?.let(LogoPosition::without) else LogoPosition.toJson(value, existing ?: JSONObject())
|
||||
if (next == null) {
|
||||
if (file.isFile && !file.delete()) return false
|
||||
recordPosition(target, "")
|
||||
return true
|
||||
}
|
||||
grid.mkdirs()
|
||||
val tmp = File(grid, file.name + ".tmp")
|
||||
val placed = runCatching { tmp.writeText(next.toString()); tmp.renameTo(file) }.getOrDefault(false)
|
||||
if (!placed) {
|
||||
tmp.delete()
|
||||
return false
|
||||
}
|
||||
recordPosition(target, signature(file))
|
||||
return true
|
||||
}
|
||||
|
||||
/** A `logoPosition` this version cannot read (a pin it does not know, say) is left alone rather than read as "removed". */
|
||||
private fun hasUnknownPosition(file: File): Boolean {
|
||||
val json = runCatching { JSONObject(file.readText()) }.getOrNull() ?: return false
|
||||
return json.has("logoPosition") && LogoPosition.parse(json) == null
|
||||
}
|
||||
|
||||
private fun reconcilePosition(context: Context, grid: File, target: Target) {
|
||||
val file = positionFile(grid, target.id)
|
||||
if (hasUnknownPosition(file)) return
|
||||
val theirs = positionIn(file)
|
||||
val now = if (theirs != null) signature(file) else ""
|
||||
val before = positionRecorded(target)
|
||||
val own = position(target)
|
||||
when {
|
||||
before == null && own != null && theirs == null -> placePosition(context, target, own)
|
||||
before == null || now != before -> {
|
||||
// Recorded only once the local side agrees, so a failed take-over is tried again on the next scan.
|
||||
if (theirs != null) {
|
||||
if (own == theirs || storePosition(target, theirs)) recordPosition(target, now)
|
||||
} else {
|
||||
if (own == null || storePosition(target, null)) recordPosition(target, "")
|
||||
}
|
||||
}
|
||||
own != null && own != theirs -> placePosition(context, target, own)
|
||||
}
|
||||
}
|
||||
|
||||
/** Takes over what changed in Steam since the last look; returns true when a choice was added, replaced or dropped. */
|
||||
fun sync(context: Context, targets: List<Target>): Boolean = synchronized(lock) { reconcile(context, targets) }
|
||||
|
||||
private fun reconcile(context: Context, targets: List<Target>): Boolean {
|
||||
val grid = dir(context) ?: return false
|
||||
val ours = writtenByDroidDeck(grid)
|
||||
var changed = false
|
||||
for (target in targets) for (slot in SUFFIX.keys) {
|
||||
try {
|
||||
val own = AddedGameArt.chosen(target.artDir, slot)
|
||||
val present = files(grid, target.id, slot)
|
||||
val theirs = present.filter { it.name !in ours }.maxByOrNull { it.lastModified() }
|
||||
val now = signature(theirs)
|
||||
val before = recorded(target, slot)
|
||||
when {
|
||||
before == null && own != null && theirs == null -> write(context, target, slot, own)
|
||||
before == null || now != before -> {
|
||||
if (theirs != null) {
|
||||
if (own != null && own.length() == theirs.length() && own.readBytes().contentEquals(theirs.readBytes())) record(target, slot, now)
|
||||
else if (AddedGameArt.store(target.artDir, slot, AddedGameArt.Option(AddedGameArt.STEAM, theirs.path, theirs.path))) {
|
||||
target.setSource(slot, AddedGameArt.STEAM + "|steam-client")
|
||||
record(target, slot, now)
|
||||
changed = true
|
||||
}
|
||||
} else {
|
||||
if (own != null && present.isEmpty()) { own.delete(); target.setSource(slot, ""); changed = true }
|
||||
record(target, slot, "")
|
||||
}
|
||||
}
|
||||
own != null && theirs != null && now == before && own.length() != theirs.length() -> write(context, target, slot, own)
|
||||
own != null && present.isEmpty() -> write(context, target, slot, own)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "${target.id} ${slot.key}: ${e.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
for (target in targets) {
|
||||
try {
|
||||
reconcilePosition(context, grid, target)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "${target.id} logo position: ${e.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
if (changed) Log.i(TAG, "took over art changed in Steam")
|
||||
return changed
|
||||
}
|
||||
}
|
||||
@@ -118,23 +118,42 @@ object SteamGridDb {
|
||||
.filter { it.optInt("id") > 0 && it.optString("name").isNotEmpty() }
|
||||
.map { Game(it.optInt("id"), it.optString("name"), it.optBoolean("verified")) }
|
||||
|
||||
internal fun path(id: Int, slot: AddedGameArt.Slot, page: Int): String {
|
||||
val base = when (slot) {
|
||||
internal fun path(id: Int, slot: AddedGameArt.Slot): String =
|
||||
when (slot) {
|
||||
AddedGameArt.Slot.COVER -> "grids/game/$id?dimensions=600x900&$QUERY"
|
||||
AddedGameArt.Slot.WIDE -> "grids/game/$id?dimensions=460x215,920x430&$QUERY"
|
||||
AddedGameArt.Slot.BACKGROUND -> "heroes/game/$id?$QUERY"
|
||||
AddedGameArt.Slot.LOGO -> "logos/game/$id?$QUERY"
|
||||
AddedGameArt.Slot.ICON -> "icons/game/$id?$QUERY"
|
||||
}
|
||||
return if (page > 0) "$base&page=$page" else base
|
||||
}
|
||||
|
||||
fun page(key: String, id: Int, slot: AddedGameArt.Slot, page: Int): List<Pair<String, String>> {
|
||||
val items = json(key, "$API/${path(id, slot, page)}")?.optJSONArray("data") ?: return emptyList()
|
||||
fun page(key: String, id: Int, slot: AddedGameArt.Slot): List<Pair<String, String>> {
|
||||
val items = json(key, "$API/${path(id, slot)}")?.optJSONArray("data") ?: return emptyList()
|
||||
return ranked(items, pngOnly = slot == AddedGameArt.Slot.LOGO || slot == AddedGameArt.Slot.ICON)
|
||||
}
|
||||
|
||||
private const val PAGE_SIZE = 50
|
||||
private const val MAX_PAGES = 4
|
||||
|
||||
fun all(key: String, id: Int, slot: AddedGameArt.Slot): List<Pair<String, String>> {
|
||||
val raw = JSONArray()
|
||||
for (n in 0 until MAX_PAGES) {
|
||||
val url = "$API/${path(id, slot)}" + if (n > 0) "&page=$n" else ""
|
||||
val data: JSONArray? = try {
|
||||
json(key, url)?.optJSONArray("data")
|
||||
} catch (e: Failed) {
|
||||
if (n == 0) throw e
|
||||
null
|
||||
}
|
||||
val items = data ?: break
|
||||
for (i in 0 until items.length()) raw.put(items.get(i))
|
||||
if (items.length() < PAGE_SIZE) break
|
||||
}
|
||||
return ranked(raw, pngOnly = slot == AddedGameArt.Slot.LOGO || slot == AddedGameArt.Slot.ICON)
|
||||
}
|
||||
|
||||
fun images(key: String, id: Int, slot: AddedGameArt.Slot, limit: Int): List<Pair<String, String>> =
|
||||
page(key, id, slot, 0).take(limit)
|
||||
page(key, id, slot).take(limit)
|
||||
|
||||
internal fun ranked(items: JSONArray, pngOnly: Boolean): List<Pair<String, String>> =
|
||||
(0 until items.length()).map { items.getJSONObject(it) }
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
|
||||
object AutoDriverTrial {
|
||||
private const val TAG = "AutoDriverTrial"
|
||||
private const val PREFS = "gpu_auto_trial"
|
||||
const val UNREADY_STARTS_BEFORE_ROLLBACK = 2
|
||||
|
||||
class Trial(
|
||||
val release: String, val name: String,
|
||||
val display: String, val linux: String,
|
||||
val previousDisplay: String, val previousLinux: String,
|
||||
val starts: Int,
|
||||
)
|
||||
|
||||
@Volatile private var app: Context? = null
|
||||
@Volatile private var watching: Trial? = null
|
||||
|
||||
fun release(pair: DriverPairs.DriverPair) = "${pair.key}@${pair.version}"
|
||||
|
||||
fun current(context: Context): Trial? {
|
||||
val p = prefs(context)
|
||||
val release = p.getString("release", null) ?: return null
|
||||
return Trial(
|
||||
release, p.getString("name", "").orEmpty(),
|
||||
p.getString("display", "").orEmpty(), p.getString("linux", "").orEmpty(),
|
||||
p.getString("previousDisplay", "").orEmpty(), p.getString("previousLinux", "").orEmpty(),
|
||||
p.getInt("starts", 0),
|
||||
)
|
||||
}
|
||||
|
||||
fun begin(context: Context, release: String, name: String, display: String, linux: String, previousDisplay: String, previousLinux: String) {
|
||||
prefs(context).edit()
|
||||
.putString("release", release).putString("name", name)
|
||||
.putString("display", display).putString("linux", linux)
|
||||
.putString("previousDisplay", previousDisplay).putString("previousLinux", previousLinux)
|
||||
.putInt("starts", 0)
|
||||
.commit()
|
||||
}
|
||||
|
||||
fun clear(context: Context) {
|
||||
val bad = bad(context)
|
||||
prefs(context).edit().clear().putStringSet("bad", bad).commit()
|
||||
}
|
||||
|
||||
fun bad(context: Context): Set<String> = prefs(context).getStringSet("bad", emptySet()).orEmpty()
|
||||
|
||||
fun sessionStarting(context: Context, display: String, linux: String) {
|
||||
val trial = current(context)
|
||||
watching = null
|
||||
if (trial == null || trial.display != display || trial.linux != linux) return
|
||||
app = context.applicationContext
|
||||
watching = trial
|
||||
prefs(context).edit().putInt("starts", trial.starts + 1).commit()
|
||||
}
|
||||
|
||||
fun sessionReady() {
|
||||
val trial = watching ?: return
|
||||
val context = app ?: return
|
||||
watching = null
|
||||
if (current(context)?.release != trial.release) return
|
||||
Log.i(TAG, "${trial.release} reached a ready session; keeping it")
|
||||
clear(context)
|
||||
prune(context, setOf(trial.display, trial.linux))
|
||||
}
|
||||
|
||||
fun rollbackDue(context: Context): Trial? = current(context)?.takeIf { it.starts >= UNREADY_STARTS_BEFORE_ROLLBACK }
|
||||
|
||||
fun rollBack(context: Context, trial: Trial) {
|
||||
Log.w(TAG, "${trial.release} never reached a ready session in ${trial.starts} starts; restoring the previous drivers")
|
||||
if (SessionPrefs.androidDriver(context) == trial.display) SessionPrefs.setAndroidDriver(context, trial.previousDisplay)
|
||||
if (SessionPrefs.linuxDriver(context) == trial.linux) SessionPrefs.setLinuxDriver(context, trial.previousLinux)
|
||||
val bad = bad(context) + trial.release
|
||||
prefs(context).edit().clear().putStringSet("bad", bad).commit()
|
||||
prune(context, setOf(trial.previousDisplay, trial.previousLinux, SessionPrefs.androidDriver(context), SessionPrefs.linuxDriver(context)))
|
||||
}
|
||||
|
||||
fun prune(context: Context, keep: Set<String>) {
|
||||
val lm = LinuxVulkanDriverManager(context)
|
||||
val td = TurnipDriver(context)
|
||||
val owned = SessionPrefs.gpuAutoInstalled(context)
|
||||
for (id in owned - keep) {
|
||||
val bundle = DriverBundle.containing(context, id, lm.isInstalled(id))
|
||||
when {
|
||||
bundle != null -> {
|
||||
if (bundle.androidId in keep || bundle.linuxId in keep) continue
|
||||
DriverBundle.remove(context, bundle); TurnipReleases.forget(context, bundle.id)
|
||||
}
|
||||
lm.isInstalled(id) -> lm.removeDriver(id)
|
||||
else -> td.remove(id)
|
||||
}
|
||||
TurnipReleases.forget(context, id)
|
||||
}
|
||||
SessionPrefs.setGpuAutoInstalled(context, owned.intersect(keep))
|
||||
}
|
||||
|
||||
private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import android.util.Log
|
||||
|
||||
object TpUbwcHint {
|
||||
const val ENV = "FD_DEV_FEATURES"
|
||||
private const val TAG = "TpUbwcHint"
|
||||
|
||||
@Volatile private var resolved = false
|
||||
@Volatile private var cached: String? = null
|
||||
|
||||
// On Adreno 740, TPL1_DBG_ECO_CNTL1.TP_UBWC_FLAG_HINT is shared by every driver on the GPU and must
|
||||
// match the system driver: blob v6xx leaves it off, newer blobs set it. A mismatch corrupts Android UI.
|
||||
internal fun features(model: Int, systemDriverVersion: String?): String? {
|
||||
if (model != 740) return null
|
||||
val blob = systemDriverVersion?.split('.')?.getOrNull(1)?.toIntOrNull() ?: return null
|
||||
return "enable_tp_ubwc_flag_hint=" + if (blob >= 700) "1" else "0"
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun forThisDevice(): String? {
|
||||
if (resolved) return cached
|
||||
val model = GpuInfo.detect().model
|
||||
val version = if (model == 740) VulkanInfo.query()["driver_version"] else null
|
||||
cached = features(model, version)
|
||||
resolved = true
|
||||
Log.i(TAG, "Adreno $model, system driver ${version ?: "n/a"}: ${cached ?: "driver default"}")
|
||||
return cached
|
||||
}
|
||||
|
||||
fun applyToProcess() {
|
||||
val value = forThisDevice() ?: return
|
||||
runCatching { android.system.Os.setenv(ENV, value, true) }
|
||||
.onFailure { Log.w(TAG, "setenv $ENV failed: ${it.javaClass.simpleName}") }
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import com.droiddeck.launcher.core.DownloadSource
|
||||
import com.droiddeck.launcher.core.Downloader
|
||||
import com.droiddeck.launcher.core.Hashes
|
||||
import com.droiddeck.launcher.R
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
object TurnipReleases {
|
||||
private const val TAG = "TurnipReleases"
|
||||
private const val PREFS = "turnip_releases"
|
||||
private const val KEY_RELEASE = "latest"
|
||||
private const val KEY_DOWNLOADS = "downloads"
|
||||
@@ -104,7 +107,24 @@ object TurnipReleases {
|
||||
}
|
||||
|
||||
private fun get(context: Context, url: String): String {
|
||||
val c = URL(url).openConnection() as HttpURLConnection
|
||||
val sources = DownloadSource.candidates(url)
|
||||
var last: IOException? = null
|
||||
for ((index, source) in sources.withIndex()) {
|
||||
try {
|
||||
val body = get(context, source, url)
|
||||
DownloadSource.succeeded(source)
|
||||
return body
|
||||
} catch (e: IOException) {
|
||||
last = e
|
||||
if (index < sources.lastIndex) Log.i(TAG, "retrying $url via $source")
|
||||
}
|
||||
}
|
||||
DownloadSource.failed()
|
||||
throw last ?: IOException(context.getString(R.string.turnip_github_http, 0))
|
||||
}
|
||||
|
||||
private fun get(context: Context, source: String, url: String): String {
|
||||
val c = URL(source).openConnection() as HttpURLConnection
|
||||
c.connectTimeout = 15_000
|
||||
c.readTimeout = 20_000
|
||||
c.setRequestProperty("Accept", "application/vnd.github+json")
|
||||
@@ -160,39 +180,13 @@ object TurnipReleases {
|
||||
fun download(context: Context, asset: Asset, progress: (Int) -> Unit): File {
|
||||
if (asset.sha256.isEmpty()) throw IOException(context.getString(R.string.turnip_list_predates_checksums))
|
||||
val target = File(context.cacheDir, asset.name)
|
||||
val c = URL(asset.url).openConnection() as HttpURLConnection
|
||||
c.connectTimeout = 15_000
|
||||
c.readTimeout = 60_000
|
||||
c.setRequestProperty("User-Agent", "DroidDeck-app")
|
||||
c.instanceFollowRedirects = true
|
||||
try {
|
||||
if (c.responseCode != 200) throw IOException(context.getString(R.string.turnip_download_http, c.responseCode))
|
||||
val total = c.contentLengthLong.takeIf { it > 0 } ?: asset.size
|
||||
c.inputStream.use { input ->
|
||||
FileOutputStream(target).use { out ->
|
||||
val buf = ByteArray(1 shl 16)
|
||||
var done = 0L
|
||||
var last = -1
|
||||
while (true) {
|
||||
val r = input.read(buf)
|
||||
if (r <= 0) break
|
||||
out.write(buf, 0, r)
|
||||
done += r
|
||||
val pct = if (total > 0) (done * 100 / total).toInt() else 0
|
||||
if (pct != last) { last = pct; progress(pct) }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!asset.sha256.equals(Hashes.sha256(target), ignoreCase = true)) {
|
||||
throw IOException(context.getString(R.string.turnip_checksum_mismatch))
|
||||
}
|
||||
return target
|
||||
} catch (e: Exception) {
|
||||
val ok = Downloader.downloadFile(asset.url, target, true, asset.sha256) { f -> progress(if (f < 0) -1 else (f * 100).toInt()) }
|
||||
if (!ok) throw IOException(context.getString(R.string.turnip_download_failed))
|
||||
if (!asset.sha256.equals(Hashes.sha256(target), ignoreCase = true)) {
|
||||
FileUtils.delete(target)
|
||||
throw e
|
||||
} finally {
|
||||
c.disconnect()
|
||||
throw IOException(context.getString(R.string.turnip_checksum_mismatch))
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
private fun downloads(context: Context): JSONObject =
|
||||
|
||||
@@ -108,11 +108,11 @@ object DeckyManager {
|
||||
target.parentFile?.mkdirs()
|
||||
val temp = File(target.parentFile, "PluginLoader.download")
|
||||
val downloading = context.getString(R.string.user_apps_downloading, release.tag)
|
||||
val ok = Downloader.downloadFile(release.url, temp, false) { f -> progress(downloading, if (f < 0) -1 else (f * 100).toInt().coerceIn(0, 100)) }
|
||||
val ok = Downloader.downloadFile(release.url, temp, true, release.digest?.substringAfter(':')) { f -> progress(downloading, if (f < 0) -1 else (f * 100).toInt().coerceIn(0, 100)) }
|
||||
if (!ok) { temp.delete(); return context.getString(R.string.deckymgr_download_failed) }
|
||||
if (temp.length() != release.size) { temp.delete(); return context.getString(R.string.deckymgr_size_mismatch) }
|
||||
val expected = release.digest?.substringAfter(':') ?: release.shaUrl?.let { checksumUrl ->
|
||||
val checksumBody = Downloader.downloadString(checksumUrl) ?: run { temp.delete(); return context.getString(R.string.deckymgr_checksum_download_failed) }
|
||||
val checksumBody = Downloader.downloadDirect(checksumUrl) ?: run { temp.delete(); return context.getString(R.string.deckymgr_checksum_download_failed) }
|
||||
Regex("(?i)\\b[0-9a-f]{64}\\b").find(checksumBody)?.value
|
||||
} ?: run { temp.delete(); return context.getString(R.string.deckymgr_checksum_missing) }
|
||||
if (!expected.matches(Regex("(?i)[0-9a-f]{64}"))) { temp.delete(); return context.getString(R.string.deckymgr_checksum_invalid) }
|
||||
@@ -236,7 +236,7 @@ object DeckyManager {
|
||||
downloadingBinary = true
|
||||
progress(downloading, -1)
|
||||
val downloaded = try {
|
||||
Downloader.downloadFile(url, temp, false) { fraction ->
|
||||
Downloader.downloadFile(url, temp, true, hash) { fraction ->
|
||||
progress(downloading, if (fraction < 0) -1 else (fraction * 100).toInt().coerceIn(0, 100))
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -90,7 +90,7 @@ object DesktopCatalog {
|
||||
try {
|
||||
val downloading = context.getString(R.string.user_apps_downloading, entry.name)
|
||||
listener?.onProgress(LinuxRuntimeInstaller.Step.DOWNLOADING, downloading, 0)
|
||||
val ok = Downloader.downloadFile(entry.url, download, true) { f ->
|
||||
val ok = Downloader.downloadFile(entry.url, download, true, entry.sha256) { f ->
|
||||
listener?.onProgress(LinuxRuntimeInstaller.Step.DOWNLOADING, downloading, if (f < 0) -1 else Math.round(f * 100f))
|
||||
}
|
||||
if (!ok) return context.getString(R.string.user_apps_download_failed)
|
||||
|
||||
@@ -176,6 +176,7 @@ object FlatpakManager {
|
||||
return id to problem
|
||||
}
|
||||
fun update(context: Context, id: String?, onProgress: (String, Int) -> Unit) = transaction(context, "update", id, onProgress)
|
||||
fun prune(context: Context, onProgress: (String, Int) -> Unit) = transaction(context, "prune", null, onProgress)
|
||||
|
||||
fun updates(context: Context): Set<String>? {
|
||||
if (!ready(context)) return emptySet()
|
||||
|
||||
@@ -261,7 +261,7 @@ public final class LinuxRuntimeInstaller {
|
||||
emptyTrash(LinuxRuntime.rootDir(context).getParentFile());
|
||||
String downloading = context.getString(R.string.rtinst_downloading);
|
||||
if (listener != null) listener.onProgress(Step.DOWNLOADING, downloading, 0);
|
||||
boolean ok = Downloader.downloadFile(release.url, archive, true, (fraction) -> {
|
||||
boolean ok = Downloader.downloadFile(release.url, archive, true, release.sha256, (fraction) -> {
|
||||
if (listener != null) {
|
||||
listener.onProgress(Step.DOWNLOADING, downloading,
|
||||
fraction < 0 ? -1 : Math.round(fraction * 100f));
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaCodec
|
||||
import android.media.MediaExtractor
|
||||
import android.media.MediaFormat
|
||||
import android.media.MediaMuxer
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
object BootMovie {
|
||||
private const val TAG = "BootMovie"
|
||||
const val DROIDDECK_30 = "dd30"
|
||||
const val DROIDDECK_60 = "dd60"
|
||||
const val CUSTOM = "custom"
|
||||
const val MAX_BYTES = 50L shl 20
|
||||
const val MAX_CONVERT_SECONDS = 60
|
||||
val PICKER_EXTENSIONS = listOf("webm", "mkv", "mp4", "mov", "m4v", "3gp")
|
||||
|
||||
private val ASSETS = mapOf(
|
||||
DROIDDECK_30 to "droiddeck-startup.webm",
|
||||
DROIDDECK_60 to "droiddeck-startup-60.webm",
|
||||
)
|
||||
|
||||
// Steam's CEF player only takes WebM codecs; MediaMuxer's WebM writer cannot take AV1.
|
||||
internal val VIDEO_CODECS = setOf(MediaFormat.MIMETYPE_VIDEO_VP8, MediaFormat.MIMETYPE_VIDEO_VP9, MediaFormat.MIMETYPE_VIDEO_AV1)
|
||||
private val REMUX_CODECS = setOf(MediaFormat.MIMETYPE_VIDEO_VP8, MediaFormat.MIMETYPE_VIDEO_VP9)
|
||||
internal val AUDIO_CODECS = setOf(MediaFormat.MIMETYPE_AUDIO_OPUS, MediaFormat.MIMETYPE_AUDIO_VORBIS)
|
||||
|
||||
sealed class Import {
|
||||
data class Done(val name: String, val audioDropped: Boolean) : Import()
|
||||
object TooLong : Import()
|
||||
data class Unsupported(val codec: String?) : Import()
|
||||
object Failed : Import()
|
||||
}
|
||||
|
||||
private fun dir(context: Context) = File(context.filesDir, "boot-movie")
|
||||
private fun customFile(context: Context) = File(dir(context), "custom.webm")
|
||||
|
||||
fun customName(context: Context): String? =
|
||||
if (customFile(context).isFile) SessionPrefs.bootMovieCustomName(context) ?: "custom.webm" else null
|
||||
|
||||
fun choice(context: Context): String {
|
||||
val chosen = SessionPrefs.bootMovie(context)
|
||||
return when {
|
||||
chosen == CUSTOM && customFile(context).isFile -> CUSTOM
|
||||
chosen in ASSETS -> chosen
|
||||
else -> DROIDDECK_30
|
||||
}
|
||||
}
|
||||
|
||||
fun prepare(context: Context): File? {
|
||||
val choice = choice(context)
|
||||
val source = if (choice == CUSTOM) customFile(context) else extractAsset(context, ASSETS.getValue(choice)) ?: return null
|
||||
if (SessionPrefs.bootMovieSound(context)) return source
|
||||
val muted = File(source.parentFile, source.nameWithoutExtension + "-muted.webm")
|
||||
if (muted.isFile && muted.lastModified() >= source.lastModified()) return muted
|
||||
if (remux(source, muted, audio = false)) return muted
|
||||
if (BootMovieTranscoder.transcode(source, muted, keepAudio = false) {} != null) return muted
|
||||
Log.e(TAG, "could not strip audio from ${source.name}; not staging it")
|
||||
return null
|
||||
}
|
||||
|
||||
private fun extractAsset(context: Context, name: String): File? {
|
||||
val target = File(dir(context), name)
|
||||
val updated = runCatching { context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime }.getOrDefault(Long.MAX_VALUE)
|
||||
if (target.isFile && target.lastModified() >= updated) return target
|
||||
val staged = File(target.parentFile, "$name.staged")
|
||||
return try {
|
||||
target.parentFile?.mkdirs()
|
||||
context.assets.open("steam-startup/$name").use { input ->
|
||||
staged.outputStream().use { output -> FileUtils.copy(input, output) }
|
||||
}
|
||||
if (staged.renameTo(target)) target else null
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "could not extract $name", e)
|
||||
null
|
||||
} finally {
|
||||
staged.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun import(context: Context, file: File, onStart: () -> Unit = {}, onProgress: (Int) -> Unit = {}): Import {
|
||||
val (format, audio) = probe(file) ?: return Import.Unsupported(null)
|
||||
val video = format.getString(MediaFormat.KEY_MIME)
|
||||
val audioPlayable = audio == null || audio in AUDIO_CODECS
|
||||
val playable = video in VIDEO_CODECS && file.length() <= MAX_BYTES
|
||||
val copy = playable && audioPlayable && isWebm(file)
|
||||
val remux = !copy && playable && video in REMUX_CODECS
|
||||
if (!copy && !remux) {
|
||||
if (!BootMovieTranscoder.canDecode(format)) return Import.Unsupported(video)
|
||||
val durationUs = if (format.containsKey(MediaFormat.KEY_DURATION)) format.getLong(MediaFormat.KEY_DURATION) else 0L
|
||||
if (durationUs > MAX_CONVERT_SECONDS * 1_000_000L) return Import.TooLong
|
||||
onStart()
|
||||
}
|
||||
val target = customFile(context)
|
||||
val staged = File(dir(context), "custom.webm.staged")
|
||||
var audioDropped = remux && !audioPlayable
|
||||
return try {
|
||||
target.parentFile?.mkdirs()
|
||||
val ok = when {
|
||||
copy -> {
|
||||
file.inputStream().use { input -> staged.outputStream().use { output -> FileUtils.copy(input, output) } }
|
||||
true
|
||||
}
|
||||
remux -> remux(file, staged, audio = true)
|
||||
else -> BootMovieTranscoder.transcode(file, staged, onProgress = onProgress)?.also { audioDropped = it.audioDropped } != null
|
||||
}
|
||||
if (!ok || !staged.renameTo(target)) return Import.Failed
|
||||
File(target.parentFile, "custom-muted.webm").delete()
|
||||
SessionPrefs.setBootMovieCustomName(context, file.name)
|
||||
SessionPrefs.setBootMovie(context, CUSTOM)
|
||||
Import.Done(file.name, audioDropped)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "could not import ${file.name}", e)
|
||||
Import.Failed
|
||||
} finally {
|
||||
staged.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeCustom(context: Context) {
|
||||
customFile(context).delete()
|
||||
File(dir(context), "custom-muted.webm").delete()
|
||||
SessionPrefs.setBootMovieCustomName(context, null)
|
||||
if (SessionPrefs.bootMovie(context) == CUSTOM) SessionPrefs.setBootMovie(context, DROIDDECK_30)
|
||||
}
|
||||
|
||||
private fun isWebm(file: File): Boolean {
|
||||
if (!file.name.endsWith(".webm", ignoreCase = true)) return false
|
||||
val magic = ByteArray(4)
|
||||
val read = runCatching { file.inputStream().use { it.read(magic) } }.getOrDefault(0)
|
||||
return read == 4 && magic.contentEquals(byteArrayOf(0x1A, 0x45, 0xDF.toByte(), 0xA3.toByte()))
|
||||
}
|
||||
|
||||
private fun probe(file: File): Pair<MediaFormat, String?>? {
|
||||
val extractor = MediaExtractor()
|
||||
return try {
|
||||
extractor.setDataSource(file.path)
|
||||
var video: MediaFormat? = null
|
||||
var audio: String? = null
|
||||
for (i in 0 until extractor.trackCount) {
|
||||
val format = extractor.getTrackFormat(i)
|
||||
val mime = format.getString(MediaFormat.KEY_MIME) ?: continue
|
||||
if (video == null && mime.startsWith("video/")) video = format
|
||||
if (audio == null && mime.startsWith("audio/")) audio = mime
|
||||
}
|
||||
video?.let { it to audio }
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "could not read ${file.name}", e)
|
||||
null
|
||||
} finally {
|
||||
extractor.release()
|
||||
}
|
||||
}
|
||||
|
||||
private fun remux(source: File, target: File, audio: Boolean): Boolean {
|
||||
val staged = File(target.parentFile, target.name + ".remux")
|
||||
val extractor = MediaExtractor()
|
||||
var muxer: MediaMuxer? = null
|
||||
return try {
|
||||
extractor.setDataSource(source.path)
|
||||
val output = MediaMuxer(staged.path, MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM).also { muxer = it }
|
||||
val tracks = HashMap<Int, Int>()
|
||||
var hasVideo = false
|
||||
var hasAudio = false
|
||||
var bufferSize = 8 shl 20
|
||||
for (i in 0 until extractor.trackCount) {
|
||||
val format = extractor.getTrackFormat(i)
|
||||
val mime = format.getString(MediaFormat.KEY_MIME) ?: continue
|
||||
val keep = mime in VIDEO_CODECS && !hasVideo || audio && mime in AUDIO_CODECS && !hasAudio
|
||||
if (!keep) continue
|
||||
if (mime in VIDEO_CODECS) hasVideo = true else hasAudio = true
|
||||
extractor.selectTrack(i)
|
||||
tracks[i] = output.addTrack(format)
|
||||
if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) bufferSize = maxOf(bufferSize, format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE))
|
||||
}
|
||||
if (!hasVideo) return false
|
||||
output.start()
|
||||
val buffer = ByteBuffer.allocate(bufferSize)
|
||||
val info = MediaCodec.BufferInfo()
|
||||
while (true) {
|
||||
val size = extractor.readSampleData(buffer, 0)
|
||||
if (size < 0) break
|
||||
val track = tracks[extractor.sampleTrackIndex]
|
||||
if (track != null) {
|
||||
val key = extractor.sampleFlags and MediaExtractor.SAMPLE_FLAG_SYNC != 0
|
||||
info.set(0, size, extractor.sampleTime, if (key) MediaCodec.BUFFER_FLAG_KEY_FRAME else 0)
|
||||
output.writeSampleData(track, buffer, info)
|
||||
}
|
||||
extractor.advance()
|
||||
}
|
||||
output.stop()
|
||||
output.release()
|
||||
muxer = null
|
||||
staged.renameTo(target)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "could not remux ${source.name}", e)
|
||||
false
|
||||
} finally {
|
||||
runCatching { muxer?.release() }
|
||||
extractor.release()
|
||||
staged.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.media.MediaCodec
|
||||
import android.media.MediaCodecInfo
|
||||
import android.media.MediaCodecList
|
||||
import android.media.MediaExtractor
|
||||
import android.media.MediaFormat
|
||||
import android.media.MediaMuxer
|
||||
import android.opengl.EGL14
|
||||
import android.opengl.EGLConfig
|
||||
import android.opengl.EGLExt
|
||||
import android.opengl.GLES11Ext
|
||||
import android.opengl.GLES20
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.nio.FloatBuffer
|
||||
|
||||
internal object BootMovieTranscoder {
|
||||
private const val TAG = "BootMovieTranscoder"
|
||||
private const val TIMEOUT_US = 10_000L
|
||||
private const val MAX_FPS = 60
|
||||
private const val OPUS_RATE = 48_000
|
||||
|
||||
class Result(val audioDropped: Boolean)
|
||||
|
||||
private class Sample(val data: ByteArray, val timeUs: Long)
|
||||
private class Audio(val format: MediaFormat, val samples: List<Sample>)
|
||||
|
||||
fun canDecode(format: MediaFormat): Boolean = runCatching {
|
||||
MediaCodecList(MediaCodecList.REGULAR_CODECS).findDecoderForFormat(format) != null
|
||||
}.getOrDefault(false)
|
||||
|
||||
fun transcode(source: File, target: File, keepAudio: Boolean = true, onProgress: (Int) -> Unit): Result? {
|
||||
val hadAudio = keepAudio && trackFormat(source, "audio/") != null
|
||||
val audio = if (hadAudio) runCatching { encodeAudio(source) }
|
||||
.onFailure { Log.w(TAG, "could not convert the audio of ${source.name}", it) }.getOrNull() else null
|
||||
val muxedAudio = runCatching { encodeVideo(source, target, audio, onProgress) }
|
||||
.onFailure { Log.w(TAG, "could not convert ${source.name}", it) }.getOrNull() ?: return null
|
||||
return Result(audioDropped = hadAudio && !muxedAudio)
|
||||
}
|
||||
|
||||
private fun trackFormat(source: File, prefix: String): Pair<Int, MediaFormat>? {
|
||||
val extractor = MediaExtractor()
|
||||
return try {
|
||||
extractor.setDataSource(source.path)
|
||||
(0 until extractor.trackCount).map { it to extractor.getTrackFormat(it) }
|
||||
.firstOrNull { it.second.getString(MediaFormat.KEY_MIME)?.startsWith(prefix) == true }
|
||||
} finally {
|
||||
extractor.release()
|
||||
}
|
||||
}
|
||||
|
||||
internal class Layout(val width: Int, val height: Int, val x: Int, val y: Int, val contentWidth: Int, val contentHeight: Int)
|
||||
|
||||
// Steam fills the screen with its startup movie, so portrait clips are pillarboxed into 16:9 instead of cropped.
|
||||
internal fun layout(width: Int, height: Int, rotation: Int): Layout {
|
||||
val (w, h) = if (rotation % 180 != 0) height to width else width to height
|
||||
fun even(v: Double) = maxOf(2, (v.toInt() / 2) * 2)
|
||||
if (w >= h) {
|
||||
val scale = minOf(1.0, 1280.0 / w, 720.0 / h)
|
||||
val cw = even(w * scale)
|
||||
val ch = even(h * scale)
|
||||
return Layout(cw, ch, 0, 0, cw, ch)
|
||||
}
|
||||
val ch = even(minOf(h, 720).toDouble())
|
||||
val cw = even(w * ch.toDouble() / h)
|
||||
val canvas = even(ch * 16 / 9.0)
|
||||
return Layout(canvas, ch, (canvas - cw) / 2, 0, cw, ch)
|
||||
}
|
||||
|
||||
internal fun resample(pcm: ShortArray, channels: Int, rate: Int): ShortArray {
|
||||
if (rate == OPUS_RATE) return pcm
|
||||
val frames = pcm.size / channels
|
||||
val outFrames = (frames.toLong() * OPUS_RATE / rate).toInt()
|
||||
val out = ShortArray(outFrames * channels)
|
||||
for (i in 0 until outFrames) {
|
||||
val pos = i.toDouble() * rate / OPUS_RATE
|
||||
val i0 = pos.toInt().coerceAtMost(frames - 1)
|
||||
val i1 = (i0 + 1).coerceAtMost(frames - 1)
|
||||
val frac = pos - i0
|
||||
for (c in 0 until channels) {
|
||||
val a = pcm[i0 * channels + c]
|
||||
val b = pcm[i1 * channels + c]
|
||||
out[i * channels + c] = (a + (b - a) * frac).toInt().toShort()
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private fun encodeAudio(source: File): Audio? {
|
||||
val (track, inFormat) = trackFormat(source, "audio/") ?: return null
|
||||
val extractor = MediaExtractor()
|
||||
var decoder: MediaCodec? = null
|
||||
var encoder: MediaCodec? = null
|
||||
try {
|
||||
extractor.setDataSource(source.path)
|
||||
extractor.selectTrack(track)
|
||||
var rate = inFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
|
||||
var channels = inFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
|
||||
val dec = MediaCodec.createDecoderByType(inFormat.getString(MediaFormat.KEY_MIME)!!).also { decoder = it }
|
||||
dec.configure(inFormat, null, null, 0)
|
||||
dec.start()
|
||||
val pcm = ByteArrayOutputStream()
|
||||
val info = MediaCodec.BufferInfo()
|
||||
var inputDone = false
|
||||
while (true) {
|
||||
if (!inputDone) {
|
||||
val index = dec.dequeueInputBuffer(TIMEOUT_US)
|
||||
if (index >= 0) {
|
||||
val size = extractor.readSampleData(dec.getInputBuffer(index)!!, 0)
|
||||
if (size < 0) {
|
||||
dec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
|
||||
inputDone = true
|
||||
} else {
|
||||
dec.queueInputBuffer(index, 0, size, extractor.sampleTime, 0)
|
||||
extractor.advance()
|
||||
}
|
||||
}
|
||||
}
|
||||
val index = dec.dequeueOutputBuffer(info, TIMEOUT_US)
|
||||
if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||
val format = dec.outputFormat
|
||||
rate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
|
||||
channels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
|
||||
if (format.containsKey(MediaFormat.KEY_PCM_ENCODING) &&
|
||||
format.getInteger(MediaFormat.KEY_PCM_ENCODING) != android.media.AudioFormat.ENCODING_PCM_16BIT) return null
|
||||
} else if (index >= 0) {
|
||||
if (info.size > 0) {
|
||||
val bytes = ByteArray(info.size)
|
||||
dec.getOutputBuffer(index)!!.apply { position(info.offset); get(bytes) }
|
||||
pcm.write(bytes)
|
||||
}
|
||||
dec.releaseOutputBuffer(index, false)
|
||||
if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) break
|
||||
}
|
||||
}
|
||||
if (channels !in 1..2) return null
|
||||
val raw = pcm.toByteArray()
|
||||
val shorts = ShortArray(raw.size / 2)
|
||||
ByteBuffer.wrap(raw).order(ByteOrder.nativeOrder()).asShortBuffer().get(shorts)
|
||||
val samples = resample(shorts, channels, rate)
|
||||
|
||||
val format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_OPUS, OPUS_RATE, channels)
|
||||
format.setInteger(MediaFormat.KEY_BIT_RATE, 128_000)
|
||||
val name = MediaCodecList(MediaCodecList.REGULAR_CODECS).findEncoderForFormat(format) ?: return null
|
||||
val enc = MediaCodec.createByCodecName(name).also { encoder = it }
|
||||
enc.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
||||
enc.start()
|
||||
val packets = ArrayList<Sample>()
|
||||
var outFormat: MediaFormat? = null
|
||||
var offset = 0
|
||||
var encodeInputDone = false
|
||||
while (true) {
|
||||
if (!encodeInputDone) {
|
||||
val index = enc.dequeueInputBuffer(TIMEOUT_US)
|
||||
if (index >= 0) {
|
||||
val buffer = enc.getInputBuffer(index)!!
|
||||
buffer.clear()
|
||||
val count = minOf(buffer.remaining() / 2, samples.size - offset) / channels * channels
|
||||
val timeUs = offset.toLong() / channels * 1_000_000L / OPUS_RATE
|
||||
if (count == 0) {
|
||||
enc.queueInputBuffer(index, 0, 0, timeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
|
||||
encodeInputDone = true
|
||||
} else {
|
||||
buffer.order(ByteOrder.nativeOrder()).asShortBuffer().put(samples, offset, count)
|
||||
enc.queueInputBuffer(index, 0, count * 2, timeUs, 0)
|
||||
offset += count
|
||||
}
|
||||
}
|
||||
}
|
||||
val index = enc.dequeueOutputBuffer(info, TIMEOUT_US)
|
||||
if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||
outFormat = enc.outputFormat
|
||||
} else if (index >= 0) {
|
||||
if (info.size > 0 && info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG == 0) {
|
||||
val bytes = ByteArray(info.size)
|
||||
enc.getOutputBuffer(index)!!.apply { position(info.offset); get(bytes) }
|
||||
packets.add(Sample(bytes, info.presentationTimeUs))
|
||||
}
|
||||
enc.releaseOutputBuffer(index, false)
|
||||
if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) break
|
||||
}
|
||||
}
|
||||
return outFormat?.let { Audio(it, packets) }
|
||||
} finally {
|
||||
runCatching { decoder?.stop() }
|
||||
decoder?.release()
|
||||
runCatching { encoder?.stop() }
|
||||
encoder?.release()
|
||||
extractor.release()
|
||||
}
|
||||
}
|
||||
|
||||
private fun MediaFormat.intOrNull(key: String): Int? = if (!containsKey(key)) null else
|
||||
runCatching { getInteger(key) }.getOrElse { runCatching { getFloat(key).toInt() }.getOrNull() }
|
||||
|
||||
private fun encodeVideo(source: File, target: File, audio: Audio?, onProgress: (Int) -> Unit): Boolean {
|
||||
val (track, inFormat) = trackFormat(source, "video/") ?: throw IllegalStateException("no video track")
|
||||
val rotation = (inFormat.intOrNull(MediaFormat.KEY_ROTATION) ?: 0).mod(360)
|
||||
val layout = layout(inFormat.getInteger(MediaFormat.KEY_WIDTH), inFormat.getInteger(MediaFormat.KEY_HEIGHT), rotation)
|
||||
val width = layout.width
|
||||
val height = layout.height
|
||||
val fps = (inFormat.intOrNull(MediaFormat.KEY_FRAME_RATE) ?: 30).coerceIn(1, MAX_FPS)
|
||||
val durationUs = if (inFormat.containsKey(MediaFormat.KEY_DURATION)) inFormat.getLong(MediaFormat.KEY_DURATION) else 0L
|
||||
val minIntervalUs = 1_000_000L / MAX_FPS - 2_000
|
||||
|
||||
val encFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_VP8, width, height).apply {
|
||||
setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
|
||||
setInteger(MediaFormat.KEY_BIT_RATE, (width.toLong() * height * fps / 10).coerceIn(1_000_000, 8_000_000).toInt())
|
||||
setInteger(MediaFormat.KEY_FRAME_RATE, fps)
|
||||
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2)
|
||||
}
|
||||
val encoderName = MediaCodecList(MediaCodecList.REGULAR_CODECS).findEncoderForFormat(encFormat)
|
||||
?: throw IllegalStateException("no VP8 encoder")
|
||||
|
||||
val extractor = MediaExtractor()
|
||||
var encoder: MediaCodec? = null
|
||||
var decoder: MediaCodec? = null
|
||||
var egl: EglWindow? = null
|
||||
var renderer: FrameRenderer? = null
|
||||
var muxer: MediaMuxer? = null
|
||||
val staged = File(target.parentFile, target.name + ".convert")
|
||||
try {
|
||||
extractor.setDataSource(source.path)
|
||||
extractor.selectTrack(track)
|
||||
val enc = MediaCodec.createByCodecName(encoderName).also { encoder = it }
|
||||
enc.configure(encFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
||||
val window = EglWindow(enc.createInputSurface()).also { egl = it }
|
||||
val frames = FrameRenderer(rotation).also { renderer = it }
|
||||
enc.start()
|
||||
inFormat.setInteger(MediaFormat.KEY_ROTATION, 0)
|
||||
val dec = MediaCodec.createDecoderByType(inFormat.getString(MediaFormat.KEY_MIME)!!).also { decoder = it }
|
||||
dec.configure(inFormat, frames.surface, null, 0)
|
||||
dec.start()
|
||||
val mux = MediaMuxer(staged.path, MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM).also { muxer = it }
|
||||
|
||||
var videoTrack = -1
|
||||
var audioTrack = -1
|
||||
var audioIndex = 0
|
||||
fun writeAudioUpTo(timeUs: Long) {
|
||||
if (audioTrack < 0 || audio == null) return
|
||||
val info = MediaCodec.BufferInfo()
|
||||
while (audioIndex < audio.samples.size && audio.samples[audioIndex].timeUs <= timeUs) {
|
||||
val sample = audio.samples[audioIndex++]
|
||||
info.set(0, sample.data.size, sample.timeUs, 0)
|
||||
mux.writeSampleData(audioTrack, ByteBuffer.wrap(sample.data), info)
|
||||
}
|
||||
}
|
||||
|
||||
val decInfo = MediaCodec.BufferInfo()
|
||||
val encInfo = MediaCodec.BufferInfo()
|
||||
var inputDone = false
|
||||
var decoderDone = false
|
||||
var lastKept = Long.MIN_VALUE
|
||||
var lastProgress = -1
|
||||
while (true) {
|
||||
if (!inputDone) {
|
||||
val index = dec.dequeueInputBuffer(TIMEOUT_US)
|
||||
if (index >= 0) {
|
||||
val size = extractor.readSampleData(dec.getInputBuffer(index)!!, 0)
|
||||
if (size < 0) {
|
||||
dec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
|
||||
inputDone = true
|
||||
} else {
|
||||
dec.queueInputBuffer(index, 0, size, extractor.sampleTime, 0)
|
||||
extractor.advance()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!decoderDone) {
|
||||
val index = dec.dequeueOutputBuffer(decInfo, TIMEOUT_US)
|
||||
if (index >= 0) {
|
||||
val timeUs = decInfo.presentationTimeUs
|
||||
val keep = decInfo.size > 0 && (lastKept == Long.MIN_VALUE || timeUs - lastKept >= minIntervalUs)
|
||||
dec.releaseOutputBuffer(index, keep)
|
||||
if (keep) {
|
||||
if (!frames.awaitFrame()) throw IllegalStateException("decoder frame timed out")
|
||||
frames.draw(layout)
|
||||
window.swap(timeUs * 1000)
|
||||
lastKept = timeUs
|
||||
if (durationUs > 0) {
|
||||
val percent = (timeUs * 100 / durationUs).toInt().coerceIn(0, 99)
|
||||
if (percent != lastProgress) { lastProgress = percent; onProgress(percent) }
|
||||
}
|
||||
}
|
||||
if (decInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
|
||||
enc.signalEndOfInputStream()
|
||||
decoderDone = true
|
||||
}
|
||||
}
|
||||
}
|
||||
var encoderDone = false
|
||||
while (true) {
|
||||
val index = enc.dequeueOutputBuffer(encInfo, if (decoderDone) TIMEOUT_US else 0)
|
||||
if (index == MediaCodec.INFO_TRY_AGAIN_LATER) break
|
||||
if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||
videoTrack = mux.addTrack(enc.outputFormat)
|
||||
if (audio != null) audioTrack = runCatching { mux.addTrack(audio.format) }
|
||||
.onFailure { Log.w(TAG, "WebM muxer refused the Opus track", it) }.getOrDefault(-1)
|
||||
mux.start()
|
||||
} else if (index >= 0) {
|
||||
if (encInfo.size > 0 && encInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG == 0) {
|
||||
check(videoTrack >= 0) { "encoder produced data before its format" }
|
||||
writeAudioUpTo(encInfo.presentationTimeUs)
|
||||
mux.writeSampleData(videoTrack, enc.getOutputBuffer(index)!!, encInfo)
|
||||
}
|
||||
enc.releaseOutputBuffer(index, false)
|
||||
if (encInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { encoderDone = true; break }
|
||||
}
|
||||
}
|
||||
if (encoderDone) break
|
||||
}
|
||||
writeAudioUpTo(Long.MAX_VALUE)
|
||||
mux.stop()
|
||||
mux.release()
|
||||
muxer = null
|
||||
if (!staged.renameTo(target)) throw IllegalStateException("could not move the converted movie")
|
||||
onProgress(100)
|
||||
return audioTrack >= 0
|
||||
} finally {
|
||||
runCatching { muxer?.release() }
|
||||
runCatching { decoder?.stop() }
|
||||
decoder?.release()
|
||||
runCatching { encoder?.stop() }
|
||||
encoder?.release()
|
||||
renderer?.release()
|
||||
egl?.release()
|
||||
extractor.release()
|
||||
staged.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private class EglWindow(private val surface: Surface) {
|
||||
private val display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
|
||||
private val context: android.opengl.EGLContext
|
||||
private val window: android.opengl.EGLSurface
|
||||
|
||||
init {
|
||||
val version = IntArray(2)
|
||||
check(EGL14.eglInitialize(display, version, 0, version, 1)) { "eglInitialize failed" }
|
||||
val attributes = intArrayOf(
|
||||
EGL14.EGL_RED_SIZE, 8, EGL14.EGL_GREEN_SIZE, 8, EGL14.EGL_BLUE_SIZE, 8,
|
||||
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, EGL_RECORDABLE_ANDROID, 1, EGL14.EGL_NONE,
|
||||
)
|
||||
val configs = arrayOfNulls<EGLConfig>(1)
|
||||
val count = IntArray(1)
|
||||
check(EGL14.eglChooseConfig(display, attributes, 0, configs, 0, 1, count, 0) && count[0] > 0) { "no recordable EGL config" }
|
||||
context = EGL14.eglCreateContext(display, configs[0], EGL14.EGL_NO_CONTEXT,
|
||||
intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE), 0)
|
||||
window = EGL14.eglCreateWindowSurface(display, configs[0], surface, intArrayOf(EGL14.EGL_NONE), 0)
|
||||
check(EGL14.eglMakeCurrent(display, window, window, context)) { "eglMakeCurrent failed" }
|
||||
}
|
||||
|
||||
fun swap(timeNs: Long) {
|
||||
EGLExt.eglPresentationTimeANDROID(display, window, timeNs)
|
||||
EGL14.eglSwapBuffers(display, window)
|
||||
}
|
||||
|
||||
fun release() {
|
||||
EGL14.eglMakeCurrent(display, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT)
|
||||
EGL14.eglDestroySurface(display, window)
|
||||
EGL14.eglDestroyContext(display, context)
|
||||
EGL14.eglReleaseThread()
|
||||
EGL14.eglTerminate(display)
|
||||
surface.release()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EGL_RECORDABLE_ANDROID = 0x3142
|
||||
}
|
||||
}
|
||||
|
||||
private class FrameRenderer(rotation: Int) {
|
||||
private val thread = HandlerThread("boot-movie-frames").apply { start() }
|
||||
private val lock = Object()
|
||||
private var available = false
|
||||
private val texture = IntArray(1).also { GLES20.glGenTextures(1, it, 0) }[0]
|
||||
private val surfaceTexture: SurfaceTexture
|
||||
val surface: Surface
|
||||
private val program: Int
|
||||
private val transform = FloatArray(16)
|
||||
private val positions: FloatBuffer
|
||||
private val coords: FloatBuffer = floats(0f, 0f, 1f, 0f, 0f, 1f, 1f, 1f)
|
||||
|
||||
init {
|
||||
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture)
|
||||
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
|
||||
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
|
||||
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
|
||||
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
|
||||
surfaceTexture = SurfaceTexture(texture)
|
||||
surfaceTexture.setOnFrameAvailableListener({
|
||||
synchronized(lock) { available = true; lock.notifyAll() }
|
||||
}, Handler(thread.looper))
|
||||
surface = Surface(surfaceTexture)
|
||||
program = link(VERTEX, FRAGMENT)
|
||||
val radians = Math.toRadians(rotation.toDouble())
|
||||
val cos = Math.cos(radians).toFloat()
|
||||
val sin = Math.sin(radians).toFloat()
|
||||
val corners = floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f)
|
||||
positions = floats(*FloatArray(8) { i ->
|
||||
val x = corners[i - i % 2]
|
||||
val y = corners[i - i % 2 + 1]
|
||||
if (i % 2 == 0) x * cos + y * sin else -x * sin + y * cos
|
||||
})
|
||||
}
|
||||
|
||||
fun awaitFrame(): Boolean {
|
||||
synchronized(lock) {
|
||||
val deadline = System.currentTimeMillis() + 2_500
|
||||
while (!available) {
|
||||
val wait = deadline - System.currentTimeMillis()
|
||||
if (wait <= 0) return false
|
||||
lock.wait(wait)
|
||||
}
|
||||
available = false
|
||||
}
|
||||
surfaceTexture.updateTexImage()
|
||||
return true
|
||||
}
|
||||
|
||||
fun draw(layout: Layout) {
|
||||
surfaceTexture.getTransformMatrix(transform)
|
||||
GLES20.glViewport(0, 0, layout.width, layout.height)
|
||||
GLES20.glClearColor(0f, 0f, 0f, 1f)
|
||||
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
|
||||
GLES20.glViewport(layout.x, layout.y, layout.contentWidth, layout.contentHeight)
|
||||
GLES20.glUseProgram(program)
|
||||
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
|
||||
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture)
|
||||
val position = GLES20.glGetAttribLocation(program, "aPosition")
|
||||
val coord = GLES20.glGetAttribLocation(program, "aCoord")
|
||||
GLES20.glEnableVertexAttribArray(position)
|
||||
GLES20.glVertexAttribPointer(position, 2, GLES20.GL_FLOAT, false, 0, positions)
|
||||
GLES20.glEnableVertexAttribArray(coord)
|
||||
GLES20.glVertexAttribPointer(coord, 2, GLES20.GL_FLOAT, false, 0, coords)
|
||||
GLES20.glUniformMatrix4fv(GLES20.glGetUniformLocation(program, "uTransform"), 1, false, transform, 0)
|
||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
|
||||
}
|
||||
|
||||
fun release() {
|
||||
surface.release()
|
||||
surfaceTexture.release()
|
||||
GLES20.glDeleteProgram(program)
|
||||
GLES20.glDeleteTextures(1, intArrayOf(texture), 0)
|
||||
thread.quitSafely()
|
||||
}
|
||||
|
||||
private fun floats(vararg values: Float): FloatBuffer =
|
||||
ByteBuffer.allocateDirect(values.size * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().apply { put(values); position(0) }
|
||||
|
||||
private fun link(vertex: String, fragment: String): Int {
|
||||
fun compile(type: Int, code: String): Int = GLES20.glCreateShader(type).also {
|
||||
GLES20.glShaderSource(it, code)
|
||||
GLES20.glCompileShader(it)
|
||||
val ok = IntArray(1)
|
||||
GLES20.glGetShaderiv(it, GLES20.GL_COMPILE_STATUS, ok, 0)
|
||||
check(ok[0] != 0) { GLES20.glGetShaderInfoLog(it) }
|
||||
}
|
||||
return GLES20.glCreateProgram().also {
|
||||
GLES20.glAttachShader(it, compile(GLES20.GL_VERTEX_SHADER, vertex))
|
||||
GLES20.glAttachShader(it, compile(GLES20.GL_FRAGMENT_SHADER, fragment))
|
||||
GLES20.glLinkProgram(it)
|
||||
val ok = IntArray(1)
|
||||
GLES20.glGetProgramiv(it, GLES20.GL_LINK_STATUS, ok, 0)
|
||||
check(ok[0] != 0) { GLES20.glGetProgramInfoLog(it) }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val VERTEX = """
|
||||
attribute vec4 aPosition;
|
||||
attribute vec4 aCoord;
|
||||
uniform mat4 uTransform;
|
||||
varying vec2 vCoord;
|
||||
void main() {
|
||||
gl_Position = aPosition;
|
||||
vCoord = (uTransform * aCoord).xy;
|
||||
}
|
||||
""".trimIndent()
|
||||
private val FRAGMENT = """
|
||||
#extension GL_OES_EGL_image_external : require
|
||||
precision mediump float;
|
||||
varying vec2 vCoord;
|
||||
uniform samplerExternalOES sTexture;
|
||||
void main() {
|
||||
gl_FragColor = texture2D(sTexture, vCoord);
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -711,9 +711,71 @@ object ComponentsManager {
|
||||
saveState(context, state)
|
||||
}
|
||||
|
||||
class StoredPackage(val pkg: Package, val inUse: Boolean)
|
||||
class StoredOriginal(val protonId: String, val protonName: String?, val comp: String, val protonVersion: String, val size: Long, val kept: Boolean)
|
||||
class Stored(val packages: List<StoredPackage>, val originals: List<StoredOriginal>)
|
||||
|
||||
private fun packagesInUse(state: JSONObject): Set<String> {
|
||||
val names = HashSet<String>()
|
||||
for (key in listOf("active", "queued")) {
|
||||
val byProton = state.optJSONObject(key) ?: continue
|
||||
for (pid in byProton.keys()) {
|
||||
val comps = byProton.optJSONObject(pid) ?: continue
|
||||
for (comp in comps.keys()) comps.optJSONObject(comp)?.optString("file")?.takeIf { it.isNotEmpty() }?.let(names::add)
|
||||
}
|
||||
}
|
||||
val profiles = state.optJSONObject("profiles")
|
||||
for (scope in profiles?.keys() ?: emptyList<String>().iterator()) {
|
||||
val profile = profiles?.optJSONObject(scope) ?: continue
|
||||
COMPONENTS.forEach { comp -> profile.optString(comp).takeIf { it.isNotEmpty() }?.let(names::add) }
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
private fun originalKept(state: JSONObject, installed: Map<String, Proton>, protonId: String, comp: String, version: String): Boolean {
|
||||
val current = installed[protonId]?.let { safeName(it.version) == version } == true
|
||||
val queued = state.optJSONObject("queued")?.optJSONObject(protonId)?.optJSONObject(comp)
|
||||
?.takeIf { it.optString("kind") == "original" }?.let { safeName(it.optString("protonVersion")) == version } == true
|
||||
return current || queued
|
||||
}
|
||||
|
||||
fun stored(context: Context): Stored = synchronized(lock) {
|
||||
val state = loadState(context)
|
||||
val used = packagesInUse(state)
|
||||
val installed = protons(context).associateBy { it.id }
|
||||
val originals = originalsDir(context).listFiles()?.filter { it.isDirectory }?.flatMap { proton ->
|
||||
proton.listFiles()?.filter { it.isDirectory }?.flatMap { version ->
|
||||
COMPONENTS.mapNotNull { comp ->
|
||||
val f = File(version, "$comp.wcp")
|
||||
if (!f.isFile) null else StoredOriginal(
|
||||
proton.name, installed[proton.name]?.name, comp, version.name, f.length(),
|
||||
originalKept(state, installed, proton.name, comp, version.name),
|
||||
)
|
||||
}
|
||||
}.orEmpty()
|
||||
}.orEmpty()
|
||||
Stored(packages(context).map { StoredPackage(it, it.file in used) }, originals)
|
||||
}
|
||||
|
||||
fun deleteUnusedOriginal(context: Context, protonId: String, comp: String, protonVersion: String): Boolean = synchronized(lock) {
|
||||
val state = loadState(context)
|
||||
if (originalKept(state, protons(context).associateBy { it.id }, protonId, comp, protonVersion)) return false
|
||||
if (comp !in COMPONENTS || listOf(protonId, protonVersion).any { it.isEmpty() || '/' in it || it == "." || it == ".." }) return false
|
||||
val f = File(originalsDir(context), "$protonId/$protonVersion/$comp.wcp")
|
||||
if (!f.isFile || !f.delete()) return false
|
||||
f.parentFile?.takeIf { it.listFiles().isNullOrEmpty() }?.delete()
|
||||
f.parentFile?.parentFile?.takeIf { it.listFiles().isNullOrEmpty() }?.delete()
|
||||
true
|
||||
}
|
||||
|
||||
fun deletePackage(context: Context, file: String): String = synchronized(lock) {
|
||||
val name = safeName(file)
|
||||
val state = loadState(context)
|
||||
val queued = state.sub("queued")
|
||||
for (pid in queued.keys()) {
|
||||
val comps = queued.optJSONObject(pid) ?: continue
|
||||
for (comp in comps.keys()) check(comps.optJSONObject(comp)?.optString("file") != name) { context.getString(R.string.cmgr_package_in_use, name, pid) }
|
||||
}
|
||||
val active = state.sub("active")
|
||||
for (pid in active.keys()) {
|
||||
val comps = active.optJSONObject(pid) ?: continue
|
||||
@@ -867,8 +929,7 @@ object ComponentsManager {
|
||||
require(Hashes.isGithubSha256(item.digest)) { context.getString(R.string.cmgr_list_predates_checksums) }
|
||||
val dest = File(packagesDir(context), safeName(item.file))
|
||||
val part = File(dest.parentFile, dest.name + ".part")
|
||||
part.delete()
|
||||
check(Downloader.downloadFile(item.url, part, false) { f -> progress(if (f < 0) -1 else (f * 100).toInt().coerceIn(0, 100)) }) { context.getString(R.string.user_apps_download_failed) }
|
||||
check(Downloader.downloadFile(item.url, part, true, item.digest.substringAfter(':')) { f -> progress(if (f < 0) -1 else (f * 100).toInt().coerceIn(0, 100)) }) { context.getString(R.string.user_apps_download_failed) }
|
||||
if (!Hashes.sha256(part).equals(item.digest.substringAfter(':'), true)) { part.delete(); error(context.getString(R.string.cmgr_checksum_mismatch)) }
|
||||
val info = runCatching { packageInfo(context, part) }.getOrElse { part.delete(); throw it }
|
||||
synchronized(lock) { part.renameTo(dest) }
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.core.DownloadSource
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import com.droiddeck.launcher.core.Hashes
|
||||
import com.github.luben.zstd.ZstdInputStream
|
||||
@@ -620,16 +621,27 @@ object EsyncPacks {
|
||||
val label = context.getString(com.droiddeck.launcher.R.string.esync_fetching)
|
||||
onProgress?.invoke(label, -1)
|
||||
var reported = -1
|
||||
val fetched = FileOutputStream(part).use { output ->
|
||||
fetch(entry.asset.url, entry.asset.size, output) { done ->
|
||||
val percent = (done * 100 / entry.asset.size).toInt().coerceIn(0, 100)
|
||||
if (percent != reported) {
|
||||
reported = percent
|
||||
onProgress?.invoke(label, percent)
|
||||
val sources = DownloadSource.candidates(entry.asset.url)
|
||||
var fetched = false
|
||||
for ((index, source) in sources.withIndex()) {
|
||||
if (index > 0) Log.i(TAG, "retrying ${entry.asset.url} via $source")
|
||||
reported = -1
|
||||
fetched = FileOutputStream(part).use { output ->
|
||||
fetch(source, entry.asset.size, output) { done ->
|
||||
val percent = (done * 100 / entry.asset.size).toInt().coerceIn(0, 100)
|
||||
if (percent != reported) {
|
||||
reported = percent
|
||||
onProgress?.invoke(label, percent)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fetched) {
|
||||
DownloadSource.succeeded(source)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!fetched) {
|
||||
DownloadSource.failed()
|
||||
SessionEvents.record("sync.pack", mapOf("id" to entry.id, "source" to "download", "result" to "failed", "reason" to "download"))
|
||||
return false
|
||||
}
|
||||
@@ -836,8 +848,17 @@ object EsyncPacks {
|
||||
}
|
||||
|
||||
private fun fetchBytes(url: String, limit: Long): ByteArray? {
|
||||
val out = ByteArrayOutputStream()
|
||||
return if (fetch(url, limit, out, null)) out.toByteArray() else null
|
||||
val sources = DownloadSource.candidates(url)
|
||||
for ((index, source) in sources.withIndex()) {
|
||||
if (index > 0) Log.i(TAG, "retrying $url via $source")
|
||||
val out = ByteArrayOutputStream()
|
||||
if (fetch(source, limit, out, null)) {
|
||||
DownloadSource.succeeded(source)
|
||||
return out.toByteArray()
|
||||
}
|
||||
}
|
||||
DownloadSource.failed()
|
||||
return null
|
||||
}
|
||||
|
||||
private fun fetch(url: String, limit: Long, out: OutputStream, progress: ((Long) -> Unit)?): Boolean =
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.PerformanceHintManager
|
||||
import android.os.PowerManager
|
||||
import android.os.Process
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.wayland.WaylandCompositor
|
||||
import java.io.File
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* The game's busiest threads named to Android's performance hint service (ADPF) for a session.
|
||||
*
|
||||
* Qualcomm's WALT moves a thread to the prime cores only once it fills a mid core (the Odin 3's
|
||||
* sched_upmigrate is 95), and a game thread that waits on its workers or the GPU for part of each
|
||||
* frame never does: Factorio's update thread sat at ~68% on cores 0-5 with 6-7 idle, and ran 60%
|
||||
* faster pinned to 6-7. An app may not raise a thread's uclamp (EPERM without CAP_SYS_NICE), but a
|
||||
* hint session may name any thread of its UID - the guest's are - and one reported behind its
|
||||
* target is boosted by the power HAL onto the prime cores: on the Odin 3 a 70%-busy thread of a
|
||||
* child process went from cores 0-5 to 6-7 and ran 1.7x faster within a second.
|
||||
*
|
||||
* Once a second the busiest threads of this UID are read from /proc; one busy more than [ENTER] of
|
||||
* the time (averaged with the second before, so a loading spike does not count) joins the session,
|
||||
* and leaves it under [LEAVE] - well below, since it runs faster where it was moved. While the
|
||||
* session has threads it reports them behind target, which is what asks for the boost: the reports
|
||||
* carry no frame timing, the busy share is the signal (a capped game's frame interval looks the same
|
||||
* as a slow one's). Nothing is boosted while the device is critically hot or saves power.
|
||||
*
|
||||
* Nor while the game holds the panel's rate or a fraction of it ([holdsPace]): there is nothing to
|
||||
* gain, and it cost frame pacing - Serious Sam capped at 120 fps ran its boosted main thread on prime
|
||||
* cores the governor kept at 2.2 GHz, slower than a 3.5 GHz mid core, and 23-25% of its frames fell
|
||||
* under 90 fps against 2-7% unboosted. A game with no frames (Factorio's benchmark, a loading screen)
|
||||
* or one falling behind the panel is boosted; one holding pace keeps what it has for [PACE_LEAVE_S]
|
||||
* seconds and is then let go.
|
||||
*/
|
||||
object GameThreadHints {
|
||||
private const val TAG = "GameThreadHints"
|
||||
private const val TARGET_NS = 10_000_000L
|
||||
private const val REPORT_MS = 50L
|
||||
private const val SCAN_EVERY = 20 // reports, so once a second
|
||||
private const val ENTER = 0.6
|
||||
private const val LEAVE = 0.15 // a capped game's main thread idles to ~30% on a prime core; under 0.3 it hopped back and forth
|
||||
private const val MAX_THREADS = 2 // one per prime core; a third would be sped up in place instead
|
||||
private const val TOP_PROCESSES = 3
|
||||
private const val PACE_WINDOW_S = 3
|
||||
private const val PACE_LEAVE_S = 10
|
||||
private const val PACE_TOLERANCE = 0.08
|
||||
|
||||
/** Busy, but not the game: the app itself and the Steam client, whose UI would otherwise hold the prime cores. */
|
||||
private val NOT_GAMES = setOf("steam", "steamwebhelper")
|
||||
|
||||
private var worker: Thread? = null
|
||||
|
||||
/** At a session's start. The line is for the log. */
|
||||
@Synchronized
|
||||
fun start(context: Context): String {
|
||||
if (!SessionPrefs.gameThreadHints(context)) return "game thread hints: off"
|
||||
// Measured on the Odin 3 (CQ8725S) only; another HAL may boost differently, or not at all.
|
||||
if (Build.VERSION.SDK_INT < 31 || Build.SOC_MODEL != "CQ8725S") return "game thread hints: Odin 3 only"
|
||||
val hm = context.getSystemService(PerformanceHintManager::class.java)
|
||||
?: return "game thread hints: no PerformanceHintManager"
|
||||
val pm = context.getSystemService(PowerManager::class.java)
|
||||
worker?.interrupt()
|
||||
worker = Thread({ run(hm, pm) }, "game-thread-hints").apply { isDaemon = true; start() }
|
||||
return "game thread hints: watching"
|
||||
}
|
||||
|
||||
/** At a session's end. */
|
||||
@Synchronized
|
||||
fun stop() {
|
||||
worker?.interrupt()
|
||||
worker = null
|
||||
}
|
||||
|
||||
private fun run(hm: PerformanceHintManager, pm: PowerManager) {
|
||||
val scanner = Scanner()
|
||||
var session: PerformanceHintManager.Session? = null
|
||||
var boosted = emptySet<Int>()
|
||||
val frames = ArrayDeque<Long>()
|
||||
var pacedFor = 0
|
||||
var tick = 0
|
||||
try {
|
||||
while (!Thread.currentThread().isInterrupted) {
|
||||
if (tick++ % SCAN_EVERY == 0) {
|
||||
val busy = scanner.sample()
|
||||
frames.addLast(WaylandCompositor.gameFrameCount())
|
||||
if (frames.size > PACE_WINDOW_S + 1) frames.removeFirst()
|
||||
val fps = (frames.last() - frames.first()).toDouble() / (frames.size - 1).coerceAtLeast(1)
|
||||
val paced = frames.size > PACE_WINDOW_S && holdsPace(fps, SessionState.refreshHz.toDouble())
|
||||
pacedFor = if (paced) pacedFor + 1 else 0
|
||||
// Not SEVERE: the Odin 3 reports it once its skin reaches 50 C, a couple of minutes into any game.
|
||||
val spare = pm.isPowerSaveMode || pm.currentThermalStatus >= PowerManager.THERMAL_STATUS_CRITICAL
|
||||
val next = when {
|
||||
spare || pacedFor >= PACE_LEAVE_S -> emptySet()
|
||||
paced -> pick(boosted, busy.filterKeys { it in boosted })
|
||||
else -> pick(boosted, busy)
|
||||
}
|
||||
if (next != boosted) {
|
||||
session = update(hm, session, next)
|
||||
// No session means a thread exited in between: tried again next second.
|
||||
boosted = if (session == null) emptySet() else next
|
||||
Log.i(TAG, (if (session == null && next.isNotEmpty()) "no session for " else "boosting ") +
|
||||
next.joinToString { "${readName("/proc/$it/comm")}/$it ${((busy[it] ?: 0.0) * 100).toInt()}%" }.ifEmpty { "nothing" } +
|
||||
" (game ${fps.toInt()} fps${if (paced) ", holding pace" else ""})")
|
||||
}
|
||||
}
|
||||
session?.reportActualWorkDuration(TARGET_NS * 3 / 2)
|
||||
Thread.sleep(REPORT_MS)
|
||||
}
|
||||
} catch (_: InterruptedException) {
|
||||
} catch (t: Throwable) {
|
||||
Log.w(TAG, "stopped", t)
|
||||
} finally {
|
||||
session?.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** A new session over [tids] in place of [session]; null for none. The set changes seldom, so no setThreads. */
|
||||
private fun update(hm: PerformanceHintManager, session: PerformanceHintManager.Session?, tids: Set<Int>): PerformanceHintManager.Session? {
|
||||
session?.close()
|
||||
if (tids.isEmpty()) return null
|
||||
return try {
|
||||
hm.createHintSession(tids.toIntArray(), TARGET_NS)
|
||||
} catch (t: RuntimeException) {
|
||||
Log.w(TAG, "createHintSession", t)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a game presenting [fps] frames a second keeps up with a [refreshHz] panel: within
|
||||
* [PACE_TOLERANCE] of the rate or of a half, a third or a quarter of it (a 60 fps limiter on 120 Hz).
|
||||
* A game with next to no frames (a headless benchmark, a loading screen) does not.
|
||||
*/
|
||||
// ponytail: a CPU-bound game that happens to run at ~60 fps on a 120 Hz panel reads as limited; narrow the band if that shows up
|
||||
internal fun holdsPace(fps: Double, refreshHz: Double): Boolean =
|
||||
fps >= 10 && refreshHz > 0 && (1..4).any { k -> abs(fps - refreshHz / k) < PACE_TOLERANCE * refreshHz / k }
|
||||
|
||||
/**
|
||||
* The threads to boost next: those already boosted that are still above [LEAVE], then new ones
|
||||
* above [ENTER], busiest first, at most [MAX_THREADS].
|
||||
*/
|
||||
internal fun pick(boosted: Set<Int>, busy: Map<Int, Double>): Set<Int> =
|
||||
busy.entries
|
||||
.filter { (tid, share) -> share >= if (tid in boosted) LEAVE else ENTER }
|
||||
.sortedWith(compareByDescending<Map.Entry<Int, Double>> { it.key in boosted }.thenByDescending { it.value })
|
||||
.take(MAX_THREADS)
|
||||
.map { it.key }
|
||||
.toSet()
|
||||
|
||||
private fun readName(path: String) = try { File(path).readText().trim() } catch (_: Exception) { "?" }
|
||||
|
||||
/** Busy shares of this UID's threads since the last sample: /proc shows an app only its own processes. */
|
||||
private class Scanner {
|
||||
private val self = Process.myPid()
|
||||
private val hz = Os.sysconf(OsConstants._SC_CLK_TCK).toDouble()
|
||||
private var lastNs = 0L
|
||||
private var lastProcess = HashMap<Int, Long>()
|
||||
private var lastThread = HashMap<Int, Long>()
|
||||
private var lastBusy = HashMap<Int, Double>()
|
||||
|
||||
/** Only the busiest few processes have their threads read, so a second costs a few dozen small reads. */
|
||||
fun sample(): Map<Int, Double> {
|
||||
val now = System.nanoTime()
|
||||
val seconds = (now - lastNs) / 1e9
|
||||
lastNs = now
|
||||
val process = HashMap<Int, Long>()
|
||||
File("/proc").list()?.forEach { entry ->
|
||||
val pid = entry.toIntOrNull() ?: return@forEach
|
||||
cpuTicks("/proc/$pid/stat")?.let { process[pid] = it }
|
||||
}
|
||||
val top = process.entries
|
||||
.map { (pid, ticks) -> pid to ticks - (lastProcess[pid] ?: ticks) }
|
||||
.filter { (pid, delta) -> delta > 0 && pid != self }
|
||||
.sortedByDescending { it.second }
|
||||
.asSequence()
|
||||
.filter { (pid, _) -> readName("/proc/$pid/comm") !in NOT_GAMES }
|
||||
.take(TOP_PROCESSES)
|
||||
.toList()
|
||||
lastProcess = process
|
||||
val thread = HashMap<Int, Long>()
|
||||
val busy = HashMap<Int, Double>()
|
||||
for ((pid, _) in top) {
|
||||
File("/proc/$pid/task").list()?.forEach { entry ->
|
||||
val tid = entry.toIntOrNull() ?: return@forEach
|
||||
val ticks = cpuTicks("/proc/$pid/task/$tid/stat") ?: return@forEach
|
||||
thread[tid] = ticks
|
||||
lastThread[tid]?.let { before -> busy[tid] = ((ticks - before) / hz / seconds + (lastBusy[tid] ?: 0.0)) / 2 }
|
||||
}
|
||||
}
|
||||
lastThread = thread
|
||||
lastBusy = busy
|
||||
return busy
|
||||
}
|
||||
|
||||
/** utime + stime of a /proc stat line; fields counted after the command's closing parenthesis. */
|
||||
private fun cpuTicks(path: String): Long? = try {
|
||||
val line = File(path).readText()
|
||||
val fields = line.substring(line.lastIndexOf(')') + 2).split(' ')
|
||||
fields[11].toLong() + fields[12].toLong()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,7 +167,7 @@ object ProtonExtras {
|
||||
onProgress(downloading, 0)
|
||||
var downloaded = false
|
||||
for (attempt in 0 until 5) {
|
||||
downloaded = Downloader.downloadFile(asset.url, archive, true) { fraction ->
|
||||
downloaded = Downloader.downloadFile(asset.url, archive, true, asset.sha256) { fraction ->
|
||||
onProgress(downloading, if (fraction < 0) -1 else (fraction * 100f).toInt().coerceIn(0, 100))
|
||||
}
|
||||
if (downloaded) break
|
||||
@@ -192,7 +192,7 @@ object ProtonExtras {
|
||||
val verified = when {
|
||||
asset.sha256 != null -> asset.sha256.equals(Hashes.sha256(archive), ignoreCase = true)
|
||||
asset.sha512 != null -> {
|
||||
val expected = Downloader.downloadString(asset.sha512)?.let { Regex("(?i)\\b[0-9a-f]{128}\\b").find(it)?.value }
|
||||
val expected = Downloader.downloadDirect(asset.sha512)?.let { Regex("(?i)\\b[0-9a-f]{128}\\b").find(it)?.value }
|
||||
?: return context.getString(R.string.pextra_checksum_unreadable)
|
||||
expected.equals(Hashes.sha512(archive), ignoreCase = true)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import android.net.LocalServerSocket
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.os.CombinedVibration
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
@@ -21,15 +24,18 @@ class RumbleComponent : SessionPart() {
|
||||
@Volatile private var server: LocalServerSocket? = null
|
||||
private var phone: Motors? = null
|
||||
private val playing = HashMap<Int, Motors>()
|
||||
private val sent = HashMap<Int, Sent>()
|
||||
private val effects = HashMap<Int, Effect>()
|
||||
private val controllers = HashMap<Int, Motors?>()
|
||||
private var preferences: SharedPreferences? = null
|
||||
private var enabled = false
|
||||
private var motorThread: HandlerThread? = null
|
||||
private var handler: Handler? = null
|
||||
private val preferenceListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == null || key == "rumble") refreshEnabled()
|
||||
}
|
||||
|
||||
internal var controllerMotors: (Int) -> Motors? = ::lookupController
|
||||
internal var motorLooper: Looper? = null
|
||||
|
||||
@Synchronized private fun refreshEnabled() {
|
||||
enabled = app()?.let { ControllerPrefs.rumbleEnabled(it) } == true
|
||||
@@ -42,6 +48,7 @@ class RumbleComponent : SessionPart() {
|
||||
val vibrator = if (Build.VERSION.SDK_INT >= 31) ctx.getSystemService(VibratorManager::class.java)?.defaultVibrator
|
||||
else @Suppress("DEPRECATION") ctx.getSystemService(Vibrator::class.java)
|
||||
phone = vibrator?.takeIf { it.hasVibrator() }?.let { PhoneMotors(it) }
|
||||
handler = Handler(motorLooper ?: HandlerThread("rumble-motors").also { motorThread = it; it.start() }.looper)
|
||||
preferences = ControllerPrefs.prefs(ctx).also { it.registerOnSharedPreferenceChangeListener(preferenceListener) }
|
||||
refreshEnabled()
|
||||
server = s
|
||||
@@ -66,6 +73,9 @@ class RumbleComponent : SessionPart() {
|
||||
preferences = null
|
||||
enabled = false
|
||||
cancelAll()
|
||||
handler = null
|
||||
motorThread?.quitSafely()
|
||||
motorThread = null
|
||||
phone = null
|
||||
controllers.clear()
|
||||
val s = server
|
||||
@@ -81,24 +91,39 @@ class RumbleComponent : SessionPart() {
|
||||
val previous = playing[slot]
|
||||
if (target !== previous) {
|
||||
previous?.cancel()
|
||||
sent.remove(slot)
|
||||
forget(slot)
|
||||
if (target == null) playing.remove(slot) else playing[slot] = target
|
||||
target?.let { Log.i(TAG, "rumble: slot $slot playing on ${it.name}") }
|
||||
}
|
||||
if (target == null) return
|
||||
if ((strong == 0 && weak == 0) || ms == 0) { target.cancel(); sent.remove(slot); return }
|
||||
if ((strong == 0 && weak == 0) || ms == 0) { target.cancel(); forget(slot); return }
|
||||
val handler = handler ?: return
|
||||
val effect = effects.getOrPut(slot) { Effect(slot) }
|
||||
effect.want(strong, weak, ms)
|
||||
val now = SystemClock.uptimeMillis()
|
||||
val last = sent[slot]
|
||||
if (last != null && (now - last.at < MIN_GAP_MS || (last.strong == strong && last.weak == weak && now < last.until))) return
|
||||
val duration = ms.toLong().coerceIn(1L, 5000L)
|
||||
if (effect.playing(now)) return
|
||||
handler.removeCallbacks(effect)
|
||||
val wait = effect.at + MIN_GAP_MS - now
|
||||
if (wait > 0) handler.postDelayed(effect, wait) else play(effect)
|
||||
}
|
||||
|
||||
@Synchronized private fun play(effect: Effect) {
|
||||
val target = playing[effect.slot]
|
||||
if (target == null || effects[effect.slot] !== effect) return
|
||||
val duration = if (effect.hold) HOLD_EFFECT_MS else effect.ms.toLong().coerceIn(1L, 5000L)
|
||||
try {
|
||||
target.play(strong, weak, duration)
|
||||
sent[slot] = Sent(strong, weak, now, now + duration)
|
||||
target.play(effect.strong, effect.weak, duration)
|
||||
effect.played(SystemClock.uptimeMillis(), duration)
|
||||
if (effect.hold) handler?.postDelayed(effect, KEEPALIVE_MS)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "rumble: ${target.name}: $e")
|
||||
}
|
||||
}
|
||||
|
||||
private fun forget(slot: Int) {
|
||||
effects.remove(slot)?.let { handler?.removeCallbacks(it) }
|
||||
}
|
||||
|
||||
private fun target(slot: Int): Motors? {
|
||||
val id = PadBridge.controllerForSlot(slot)
|
||||
if (id == PadBridge.NO_CONTROLLER) return phone
|
||||
@@ -109,10 +134,36 @@ class RumbleComponent : SessionPart() {
|
||||
private fun cancelAll() {
|
||||
playing.values.toSet().forEach { it.cancel() }
|
||||
playing.clear()
|
||||
sent.clear()
|
||||
effects.values.forEach { handler?.removeCallbacks(it) }
|
||||
effects.clear()
|
||||
}
|
||||
|
||||
private data class Sent(val strong: Int, val weak: Int, val at: Long, val until: Long)
|
||||
private inner class Effect(val slot: Int) : Runnable {
|
||||
var strong = 0
|
||||
var weak = 0
|
||||
var ms = 0
|
||||
var at = -MIN_GAP_MS
|
||||
private var playedStrong = 0
|
||||
private var playedWeak = 0
|
||||
private var playedMs = 0
|
||||
private var until = 0L
|
||||
val hold get() = ms == HOLD_MS
|
||||
|
||||
fun want(strong: Int, weak: Int, ms: Int) { this.strong = strong; this.weak = weak; this.ms = ms }
|
||||
|
||||
fun playing(now: Long) = playedMs != 0 && playedStrong == strong && playedWeak == weak && playedMs == ms &&
|
||||
(hold || now < until)
|
||||
|
||||
fun played(now: Long, duration: Long) {
|
||||
playedStrong = strong
|
||||
playedWeak = weak
|
||||
playedMs = ms
|
||||
at = now
|
||||
until = now + duration
|
||||
}
|
||||
|
||||
override fun run() = play(this)
|
||||
}
|
||||
|
||||
internal interface Motors {
|
||||
val name: String
|
||||
@@ -137,20 +188,35 @@ class RumbleComponent : SessionPart() {
|
||||
any = true
|
||||
}
|
||||
if (ids.size >= 2) { add(ids[0], strong); add(ids[1], weak) } else add(ids[0], max(strong, weak))
|
||||
if (any) manager.vibrate(combined.combine()) else manager.cancel()
|
||||
if (any) manager.vibrate(combined.combine()) else cancel()
|
||||
}
|
||||
|
||||
// Some vendors' input-device vibrator HALs (Samsung's) don't halt a Bluetooth pad's running
|
||||
// effect on cancel(), but a new effect always replaces it.
|
||||
override fun cancel() {
|
||||
val supersede = CombinedVibration.startParallel()
|
||||
ids.forEach { supersede.addVibrator(it, VibrationEffect.createOneShot(1L, 1)) }
|
||||
manager.vibrate(supersede.combine())
|
||||
manager.cancel()
|
||||
}
|
||||
override fun cancel() = manager.cancel()
|
||||
}
|
||||
|
||||
private class LegacyPadMotors(override val name: String, private val vibrator: Vibrator) : Motors {
|
||||
override fun play(strong: Int, weak: Int, ms: Long) = vibrator.vibrate(oneShot(vibrator, max(strong, weak), ms))
|
||||
override fun cancel() = vibrator.cancel()
|
||||
override fun cancel() {
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(1L, 1))
|
||||
vibrator.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SessionService"
|
||||
const val NAME = "droiddeck-rumble"
|
||||
const val MIN_GAP_MS = 50L
|
||||
// Must match RUMBLE_HOLD_MS in fakeinput_steam.cpp.
|
||||
const val HOLD_MS = 0xFFFF
|
||||
const val HOLD_EFFECT_MS = 2000L
|
||||
const val KEEPALIVE_MS = 1500L
|
||||
|
||||
private fun oneShot(vibrator: Vibrator, strength: Int, ms: Long): VibrationEffect {
|
||||
val amplitude = if (vibrator.hasAmplitudeControl()) (strength * 255L / 65535L).toInt().coerceIn(1, 255)
|
||||
|
||||
@@ -103,6 +103,7 @@ object SessionEvents {
|
||||
if (!SessionState.running || SessionState.guestPid <= 1 || !SessionState.firstFrameSeen || SessionState.suspended) return
|
||||
if (SessionState.phase != SessionPhase.READY) {
|
||||
transition(SessionPhase.READY, "session.ready")
|
||||
Thread({ com.droiddeck.launcher.gpu.AutoDriverTrial.sessionReady() }, "gpu-driver-trial").start()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@ object SessionFiles {
|
||||
"usr/local/bin/droiddeck-steam-games" to "usr/local/bin/droiddeck-steam-games",
|
||||
"usr/local/bin/droiddeck-pad-defaults" to "usr/local/bin/droiddeck-pad-defaults",
|
||||
"usr/local/bin/droiddeck-steamvr" to "usr/local/bin/droiddeck-steamvr",
|
||||
"usr/local/bin/droiddeck-steamvr-settings" to "usr/local/bin/droiddeck-steamvr-settings",
|
||||
"usr/local/bin/droiddeck-steamvr-game" to "usr/local/bin/droiddeck-steamvr-game",
|
||||
"usr/local/bin/droiddeck-bwrap" to "usr/local/bin/droiddeck-bwrap",
|
||||
"usr/local/bin/droiddeck-flatpak" to "usr/local/bin/droiddeck-flatpak",
|
||||
"usr/local/bin/droiddeck-flatpak-setup" to "usr/local/bin/droiddeck-flatpak-setup",
|
||||
@@ -104,6 +106,7 @@ object SessionFiles {
|
||||
listOf("NOTICE", "GPL-2", "GPL-3", "LGPL-2.1", "7zip-License", "7zip-unRarLicense").map { "usr/local/share/licenses/droiddeck-msitools/$it" }
|
||||
val vr = listOf(
|
||||
"droiddeck/bin/linuxarm64/driver_droiddeck.so",
|
||||
"droiddeck/bin/linuxarm64/xr-display-mode",
|
||||
"droiddeck/driver.vrdrivermanifest",
|
||||
"droiddeck/resources/settings/default.vrsettings",
|
||||
"layer/libVkLayer_droiddeck_vr.so",
|
||||
@@ -185,27 +188,29 @@ object SessionFiles {
|
||||
}
|
||||
|
||||
val startupMovieDir = File(root, "root/.local/share/Steam/config/uioverrides/movies")
|
||||
val bigPictureMovieInstalled = stageStartupMovie(
|
||||
context,
|
||||
startupMovieDir,
|
||||
"bigpicture_startup.webm",
|
||||
)
|
||||
stageStartupMovie(context, startupMovieDir, "steam_os_startup.webm")
|
||||
val startupMovie = BootMovie.prepare(context)
|
||||
val bigPictureMovieInstalled = startupMovie != null &&
|
||||
stageStartupMovie(startupMovie, startupMovieDir, "bigpicture_startup.webm")
|
||||
if (startupMovie != null) stageStartupMovie(startupMovie, startupMovieDir, "steam_os_startup.webm")
|
||||
if (bigPictureMovieInstalled) {
|
||||
ensureStartupMovieDefault(File(root, "root/.local/share/Steam/config/config.vdf"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun stageStartupMovie(context: Context, directory: File, name: String): Boolean {
|
||||
private fun stageStartupMovie(source: File, directory: File, name: String): Boolean {
|
||||
val movie = File(directory, name)
|
||||
val stamp = File(directory, ".$name.source")
|
||||
val identity = "${source.absolutePath}\n${source.length()}\n${source.lastModified()}"
|
||||
if (movie.length() == source.length() && runCatching { stamp.readText() }.getOrNull() == identity) return true
|
||||
val staged = File(directory, "$name.staged")
|
||||
var installed = false
|
||||
try {
|
||||
directory.mkdirs()
|
||||
context.assets.open("steam-startup/droiddeck-startup.webm").use { input ->
|
||||
source.inputStream().use { input ->
|
||||
staged.outputStream().use { output -> FileUtils.copy(input, output) }
|
||||
}
|
||||
installed = staged.setReadable(true, false) && staged.renameTo(movie)
|
||||
if (installed) FileUtils.writeString(stamp, identity)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "could not stage Steam startup movie $name", e)
|
||||
} finally {
|
||||
|
||||
@@ -201,6 +201,7 @@ object SessionPrefs {
|
||||
|
||||
const val GPU_DRIVERS_AUTO = "auto"
|
||||
const val GPU_DRIVERS_MANUAL = "manual"
|
||||
const val GPU_DRIVERS_MIX = "mix"
|
||||
|
||||
fun gpuDriverMode(context: Context): String =
|
||||
prefs(context).getString("gpuDriverMode", GPU_DRIVERS_AUTO) ?: GPU_DRIVERS_AUTO
|
||||
@@ -358,6 +359,9 @@ object SessionPrefs {
|
||||
prefs(context).edit().putBoolean("gpuClockPin", on).apply()
|
||||
}
|
||||
|
||||
/** Whether the game's busiest threads are named to the power HAL for a boost (GameThreadHints). */
|
||||
fun gameThreadHints(context: Context): Boolean = prefs(context).getBoolean("gameThreadHints", true)
|
||||
|
||||
fun prootNoSeccomp(context: Context): Boolean = prefs(context).getBoolean("prootNoSeccomp", false)
|
||||
|
||||
fun setProotNoSeccomp(context: Context, on: Boolean) {
|
||||
@@ -495,6 +499,16 @@ object SessionPrefs {
|
||||
.onFailure { android.util.Log.e("GameEnvironment", "Could not update game environment", it) }
|
||||
}
|
||||
|
||||
fun bootMovie(context: Context): String =
|
||||
prefs(context).getString("bootMovie", BootMovie.DROIDDECK_30) ?: BootMovie.DROIDDECK_30
|
||||
fun setBootMovie(context: Context, id: String) { prefs(context).edit().putString("bootMovie", id).apply() }
|
||||
fun bootMovieSound(context: Context): Boolean = prefs(context).getBoolean("bootMovieSound", true)
|
||||
fun setBootMovieSound(context: Context, on: Boolean) { prefs(context).edit().putBoolean("bootMovieSound", on).apply() }
|
||||
fun bootMovieCustomName(context: Context): String? = prefs(context).getString("bootMovieCustomName", null)
|
||||
fun setBootMovieCustomName(context: Context, name: String?) {
|
||||
prefs(context).edit().apply { if (name == null) remove("bootMovieCustomName") else putString("bootMovieCustomName", name) }.apply()
|
||||
}
|
||||
|
||||
fun runSteamAtStartup(context: Context): Boolean = prefs(context).getBoolean("runSteamAtStartup", false)
|
||||
|
||||
fun setRunSteamAtStartup(context: Context, on: Boolean) {
|
||||
|
||||
@@ -71,6 +71,8 @@ class SessionService : Service() {
|
||||
private var suspendPolicy = SessionPrefs.SUSPEND_MANUAL
|
||||
private var steamDownloadsInBackground = false
|
||||
private var activityVisible = true
|
||||
private var xrActivityVisible = false
|
||||
private val hasVisibleSurface get() = sessionHasVisibleSurface(activityVisible, screenOn, xrActivityVisible)
|
||||
private var screenOn = true
|
||||
private var manualPauseRequested = false
|
||||
private var steamSleepToken: String? = null
|
||||
@@ -170,6 +172,17 @@ class SessionService : Service() {
|
||||
updateSuspendPolicy()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
ACTION_XR_ACTIVITY_VISIBLE -> {
|
||||
xrActivityVisible = true
|
||||
resumeSession(panelVisible = false)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
ACTION_XR_ACTIVITY_HIDDEN -> {
|
||||
xrActivityVisible = false
|
||||
suspendAttemptFailed = false
|
||||
updateSuspendPolicy()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
ACTION_SUSPEND_POLICY_CHANGED -> {
|
||||
if (!SessionState.running) return START_NOT_STICKY
|
||||
suspendPolicy = SessionPrefs.suspendPolicy(this, SessionState.mode)
|
||||
@@ -205,6 +218,7 @@ class SessionService : Service() {
|
||||
suspendPolicy = SessionPrefs.suspendPolicy(this, SessionState.mode)
|
||||
steamDownloadsInBackground = SessionPrefs.steamDownloadsInBackground(this)
|
||||
activityVisible = true
|
||||
xrActivityVisible = false
|
||||
screenOn = (getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isInteractive ?: true
|
||||
manualPauseRequested = false
|
||||
suspendDownloadNow = false
|
||||
@@ -278,6 +292,7 @@ class SessionService : Service() {
|
||||
|
||||
private fun runSession(gen: Int) {
|
||||
SessionTerminal.clear()
|
||||
com.droiddeck.launcher.gpu.TpUbwcHint.forThisDevice()
|
||||
try {
|
||||
LinuxRuntime.writeAccounts(this)
|
||||
} catch (e: Exception) {
|
||||
@@ -299,6 +314,7 @@ class SessionService : Service() {
|
||||
val sessionLog = File(sessionDir, "session.log")
|
||||
|
||||
Log.i(TAG, GpuClockPin.start(this))
|
||||
Log.i(TAG, GameThreadHints.start(this))
|
||||
|
||||
val size = SessionState.outputSize
|
||||
val guest = ArrayList<String>()
|
||||
@@ -522,9 +538,17 @@ class SessionService : Service() {
|
||||
guest.add("LIBGL_KOPPER_DRI2=true")
|
||||
LinuxRuntime.vulkanIcd(this)?.let { guest.add("VK_ICD_FILENAMES=" + it.path) }
|
||||
val linuxDriverId = SessionPrefs.linuxDriver(this)
|
||||
SessionState.runtimeDriver = linuxDriverId
|
||||
com.droiddeck.launcher.gpu.AutoDriverTrial.sessionStarting(
|
||||
this,
|
||||
com.droiddeck.launcher.wayland.CompositorHost.displayChoice.takeIf { com.droiddeck.launcher.wayland.CompositorHost.isStarted }
|
||||
?: SessionPrefs.androidDriver(this),
|
||||
linuxDriverId,
|
||||
)
|
||||
LinuxVulkanDriver.resolveIcdPath(this, linuxDriverId)
|
||||
?.let { guest.add(LinuxVulkanDriver.ENV + "=" + it) }
|
||||
tuDebug(linuxDriverId)?.let { guest.add("TU_DEBUG=$it") }
|
||||
com.droiddeck.launcher.gpu.TpUbwcHint.forThisDevice()?.let { guest.add("${com.droiddeck.launcher.gpu.TpUbwcHint.ENV}=$it") }
|
||||
if (SessionPrefs.zinkLazy(this)) {
|
||||
guest.add("ZINK_DESCRIPTORS=lazy")
|
||||
guest.add("ZINK_DEBUG=compact")
|
||||
@@ -919,8 +943,8 @@ class SessionService : Service() {
|
||||
Log.i(TAG, "queued Steam game $gameId for the running client")
|
||||
}
|
||||
|
||||
private fun resumeSession() {
|
||||
activityVisible = true
|
||||
private fun resumeSession(panelVisible: Boolean = true) {
|
||||
if (panelVisible) activityVisible = true
|
||||
screenOn = (getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isInteractive ?: screenOn
|
||||
manualPauseRequested = false
|
||||
suspendDownloadNow = false
|
||||
@@ -1111,11 +1135,11 @@ class SessionService : Service() {
|
||||
|
||||
private fun updateSuspendPolicy() {
|
||||
if (!SessionState.running) return
|
||||
if (suspendPolicy == SessionPrefs.SUSPEND_MANUAL && (!activityVisible || !screenOn)) {
|
||||
if (suspendPolicy == SessionPrefs.SUSPEND_MANUAL && !hasVisibleSurface) {
|
||||
manualPauseRequested = true
|
||||
}
|
||||
val controller = suspendController ?: return
|
||||
val hidden = !activityVisible || !screenOn
|
||||
val hidden = !hasVisibleSurface
|
||||
val downloadsBlockingSuspend = steamDownloadsInBackground && SessionState.mode == MODE_STEAM &&
|
||||
suspendPolicy != SessionPrefs.SUSPEND_NEVER && hidden && downloadActive && !suspendDownloadNow
|
||||
if (downloadsBlockingSuspend) acquireLocks()
|
||||
@@ -1137,6 +1161,7 @@ class SessionService : Service() {
|
||||
SessionEvents.record(if (shouldSuspend) "session.suspend_requested" else "session.resume_requested", mapOf(
|
||||
"policy" to suspendPolicy, "activityVisible" to activityVisible,
|
||||
"screenOn" to screenOn, "steamSleep" to (steamSleepToken != null),
|
||||
"xrActivityVisible" to xrActivityVisible,
|
||||
"nativeSleep" to (nativeSleepToken != null),
|
||||
))
|
||||
if (shouldSuspend) {
|
||||
@@ -1194,7 +1219,7 @@ class SessionService : Service() {
|
||||
}
|
||||
|
||||
private fun resumeSteamSleepOnReturn() {
|
||||
if (suspendPolicy in setOf(SessionPrefs.SUSPEND_AUTO, SessionPrefs.SUSPEND_NATIVE) && activityVisible && screenOn) {
|
||||
if (suspendPolicy in setOf(SessionPrefs.SUSPEND_AUTO, SessionPrefs.SUSPEND_NATIVE) && hasVisibleSurface) {
|
||||
if (steamSleepToken == null && (nativeSleepToken == null || nativeSleepFallback)) requestSteamWake()
|
||||
completeNativeSleep()
|
||||
completeSteamSleep()
|
||||
@@ -1305,12 +1330,14 @@ class SessionService : Service() {
|
||||
if (!SessionState.running) return
|
||||
SessionState.running = false
|
||||
}
|
||||
xrActivityVisible = false
|
||||
updateDownloadMonitoring()
|
||||
val stoppedGen = sessionGen
|
||||
val stoppedSession = SessionState.sessionId
|
||||
SessionState.stopRequested = false
|
||||
SessionEvents.record("guest.exited", mapOf("status" to status))
|
||||
GpuClockPin.stop(this)
|
||||
GameThreadHints.stop()
|
||||
SessionEvents.transition(SessionPhase.STOPPING, "session.stopping", mapOf("status" to status))
|
||||
suspendOperationPending = false
|
||||
launchWatcher?.stopWatching()
|
||||
@@ -1357,8 +1384,8 @@ class SessionService : Service() {
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
if (pipTask) {
|
||||
Log.i(TAG, "PiP task dismissed - retaining the session under its background policy")
|
||||
if (pipTask || xrActivityVisible) {
|
||||
Log.i(TAG, "panel task dismissed - retaining the session under its active surface policy")
|
||||
activityVisible = false
|
||||
updateSuspendPolicy()
|
||||
return
|
||||
@@ -1370,6 +1397,7 @@ class SessionService : Service() {
|
||||
|
||||
override fun onDestroy() {
|
||||
stopSession(0)
|
||||
GameThreadHints.stop() // also when the session never got as far as running
|
||||
if (screenReceiverRegistered) {
|
||||
unregisterReceiver(screenReceiver)
|
||||
screenReceiverRegistered = false
|
||||
@@ -1559,6 +1587,8 @@ class SessionService : Service() {
|
||||
private const val ACTION_SUSPEND_POLICY_CHANGED = "com.droiddeck.launcher.SUSPEND_POLICY_CHANGED"
|
||||
private const val ACTION_ACTIVITY_VISIBLE = "com.droiddeck.launcher.ACTIVITY_VISIBLE"
|
||||
private const val ACTION_ACTIVITY_HIDDEN = "com.droiddeck.launcher.ACTIVITY_HIDDEN"
|
||||
private const val ACTION_XR_ACTIVITY_VISIBLE = "com.droiddeck.launcher.XR_ACTIVITY_VISIBLE"
|
||||
private const val ACTION_XR_ACTIVITY_HIDDEN = "com.droiddeck.launcher.XR_ACTIVITY_HIDDEN"
|
||||
private const val ACTION_TRACK_AUXILIARY = "com.droiddeck.launcher.TRACK_AUXILIARY"
|
||||
private const val ACTION_STOP_AUXILIARY = "com.droiddeck.launcher.STOP_AUXILIARY"
|
||||
private const val ACTION_AUXILIARY_EXITED = "com.droiddeck.launcher.AUXILIARY_EXITED"
|
||||
@@ -1618,6 +1648,12 @@ class SessionService : Service() {
|
||||
context.startService(Intent(context, SessionService::class.java).setAction(action))
|
||||
}
|
||||
|
||||
fun setXrActivityVisible(context: Context, visible: Boolean) {
|
||||
if (!SessionState.running) return
|
||||
val action = if (visible) ACTION_XR_ACTIVITY_VISIBLE else ACTION_XR_ACTIVITY_HIDDEN
|
||||
context.startService(Intent(context, SessionService::class.java).setAction(action))
|
||||
}
|
||||
|
||||
fun resume(context: Context) {
|
||||
if (!SessionState.running) return
|
||||
context.startService(Intent(context, SessionService::class.java).setAction(ACTION_RESUME))
|
||||
|
||||
@@ -41,6 +41,8 @@ object SessionState {
|
||||
|
||||
@Volatile
|
||||
var running = false
|
||||
@Volatile
|
||||
var runtimeDriver: String? = null
|
||||
var suspended by mutableStateOf(false)
|
||||
@Volatile var pipActive = false
|
||||
@Volatile
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
internal fun sessionHasVisibleSurface(panelVisible: Boolean, screenOn: Boolean, xrVisible: Boolean): Boolean =
|
||||
xrVisible || (panelVisible && screenOn)
|
||||
@@ -2,6 +2,7 @@ package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.core.DownloadSource
|
||||
import com.droiddeck.launcher.core.GameEnvironment
|
||||
import com.droiddeck.launcher.input.ControllerPrefs
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
@@ -20,7 +21,7 @@ object SettingsReset {
|
||||
"noXalia", "fastSync", "fsyncFirst", "syncFallback", "prootNoSeccomp", "prootFastPath",
|
||||
"guestHostname", "fexPreset", "forceSsbs", "logs", "storageDiagnostics",
|
||||
"upscaler", "upscaleSharpness", "screenEffects", "textureAnisotropy", "textureLodBias",
|
||||
"steamDownloadsInBackground", "steam_offline",
|
||||
"steamDownloadsInBackground", "steam_offline", "bootMovie", "bootMovieSound",
|
||||
"questDefaultsV1",
|
||||
)
|
||||
|
||||
@@ -41,6 +42,7 @@ object SettingsReset {
|
||||
context.getSharedPreferences("frame_gen", Context.MODE_PRIVATE).edit().clear().commit()
|
||||
context.getSharedPreferences("steam_touch", Context.MODE_PRIVATE).edit()
|
||||
.apply { STEAM_TOUCH_KEYS.forEach { remove(it) } }.commit()
|
||||
DownloadSource.reset(context)
|
||||
com.droiddeck.launcher.agent.AgentAccess.setCommandsEnabled(context, false)
|
||||
|
||||
if (gameProfiles) {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.droiddeck.launcher.storage
|
||||
|
||||
import java.io.File
|
||||
|
||||
object SteamStorage {
|
||||
const val SCOUT = 1070560
|
||||
const val SOLDIER = 1391110
|
||||
const val SNIPER = 1628350
|
||||
const val ARM_RUNTIME = 4185400
|
||||
const val PROTON_ARM = 4427310
|
||||
const val PROTON_11_ARM = 4628740
|
||||
const val REDISTRIBUTABLES = 228980
|
||||
const val FEX = 3127680
|
||||
|
||||
const val ARM_TOOL = "droiddeck-proton-arm64"
|
||||
const val ARM_11_TOOL = "droiddeck-proton-11-arm64"
|
||||
|
||||
private val VALVE_TOOLS = setOf(
|
||||
858280, 961940, 1054830, 1113280, 1245040, 1420170, 1493710, 1580130, 1887720, 2180100, 2348590,
|
||||
2805730, 3029110, 3658110, 4183110, 4628710, 4690330,
|
||||
SCOUT, SOLDIER, SNIPER, ARM_RUNTIME, PROTON_ARM, PROTON_11_ARM, REDISTRIBUTABLES, FEX,
|
||||
)
|
||||
|
||||
private val APP_ID = Regex("^\\s*\"appid\"\\s*\"(\\d+)\"", RegexOption.MULTILINE)
|
||||
private val NAME = Regex("^\\s*\"name\"\\s*\"([^\"]*)\"", RegexOption.MULTILINE)
|
||||
private val INSTALL_DIR = Regex("^\\s*\"installdir\"\\s*\"([^\"]*)\"", RegexOption.MULTILINE)
|
||||
private val STATE = Regex("^\\s*\"StateFlags\"\\s*\"(\\d+)\"", RegexOption.MULTILINE)
|
||||
private val LAST_PLAYED = Regex("^\\s*\"LastPlayed\"\\s*\"(\\d+)\"", RegexOption.MULTILINE)
|
||||
private val MAPPING = Regex("\"(\\d+)\"\\s*\\{\\s*\"name\"\\s*\"([^\"]*)\"")
|
||||
private val COMPAT_NAME = Regex("\"compat_tools\"\\s*\\{\\s*\"([^\"]+)\"")
|
||||
|
||||
data class Manifest(
|
||||
val file: File, val appId: Int, val name: String, val installDir: String?,
|
||||
val installed: Boolean, val lastPlayed: Long,
|
||||
)
|
||||
|
||||
fun parseManifest(file: File, text: String): Manifest? {
|
||||
val id = APP_ID.find(text)?.groupValues?.get(1)?.toIntOrNull()
|
||||
?: file.name.removePrefix("appmanifest_").removeSuffix(".acf").toIntOrNull() ?: return null
|
||||
val dir = INSTALL_DIR.find(text)?.groupValues?.get(1)?.trim()
|
||||
?.takeIf { it.isNotEmpty() && it != "." && it != ".." && '/' !in it && '\\' !in it }
|
||||
return Manifest(
|
||||
file, id, NAME.find(text)?.groupValues?.get(1)?.trim().orEmpty(), dir,
|
||||
(STATE.find(text)?.groupValues?.get(1)?.toIntOrNull() ?: 0) and 4 != 0,
|
||||
LAST_PLAYED.find(text)?.groupValues?.get(1)?.toLongOrNull() ?: 0L,
|
||||
)
|
||||
}
|
||||
|
||||
class Listing(val manifests: List<Manifest>, val otherIds: Set<Int>, val complete: Boolean) {
|
||||
val ids: Set<Int> get() = manifests.map { it.appId }.toSet() + otherIds
|
||||
}
|
||||
|
||||
private val PENDING_MANIFEST = Regex("appmanifest_(\\d+)\\.acf\\..+")
|
||||
|
||||
// A partial download has no manifest yet, only a temporary one and its staging folder.
|
||||
fun manifests(library: File): Listing? {
|
||||
val apps = File(library, "steamapps")
|
||||
val names = apps.list() ?: return null
|
||||
var complete = true
|
||||
val other = HashSet<Int>()
|
||||
val read = ArrayList<Manifest>()
|
||||
for (name in names.sorted()) {
|
||||
val pending = PENDING_MANIFEST.matchEntire(name)
|
||||
if (pending != null) {
|
||||
pending.groupValues[1].toIntOrNull()?.let(other::add)
|
||||
continue
|
||||
}
|
||||
if (!name.startsWith("appmanifest_") || !name.endsWith(".acf")) continue
|
||||
val f = File(apps, name)
|
||||
if (!f.isFile) continue
|
||||
val named = name.removePrefix("appmanifest_").removeSuffix(".acf").toIntOrNull()
|
||||
val text = runCatching { f.readText() }.getOrNull()
|
||||
if (text == null) {
|
||||
if (named != null) other.add(named) else complete = false
|
||||
continue
|
||||
}
|
||||
parseManifest(f, text)?.let(read::add)
|
||||
}
|
||||
File(apps, "downloading").list()?.forEach { it.toIntOrNull()?.let(other::add) }
|
||||
return Listing(read, other, complete)
|
||||
}
|
||||
|
||||
fun isTool(m: Manifest): Boolean = m.appId in VALVE_TOOLS ||
|
||||
m.installDir?.let { it.startsWith("Proton ") || it.startsWith("SteamLinuxRuntime") } == true
|
||||
|
||||
enum class Kind { ARM_PROTON, REQUIRED, OLD_RUNTIME, X86_PROTON }
|
||||
|
||||
// DroidDeck's ARM64 Steam client cannot run Valve's x86 Protons, and its own Proton and FEX tools
|
||||
// replace the scout and soldier container runtimes.
|
||||
fun kind(m: Manifest): Kind {
|
||||
val dir = m.installDir.orEmpty()
|
||||
return when {
|
||||
m.appId == SCOUT || m.appId == SOLDIER || dir == "SteamLinuxRuntime" || dir == "SteamLinuxRuntime_soldier" -> Kind.OLD_RUNTIME
|
||||
dir.startsWith("Proton") && "ARM64" in dir -> Kind.ARM_PROTON
|
||||
m.appId == PROTON_ARM || m.appId == PROTON_11_ARM -> Kind.ARM_PROTON
|
||||
dir.startsWith("Proton ") -> Kind.X86_PROTON
|
||||
else -> Kind.REQUIRED
|
||||
}
|
||||
}
|
||||
|
||||
fun compatMappings(configVdf: String): Map<String, String> {
|
||||
val start = configVdf.indexOf("\"CompatToolMapping\"")
|
||||
if (start < 0) return emptyMap()
|
||||
val open = configVdf.indexOf('{', start)
|
||||
if (open < 0) return emptyMap()
|
||||
var depth = 0
|
||||
var end = configVdf.length
|
||||
for (i in open until configVdf.length) {
|
||||
when (configVdf[i]) {
|
||||
'{' -> depth++
|
||||
'}' -> if (--depth == 0) { end = i; break }
|
||||
}
|
||||
}
|
||||
return MAPPING.findAll(configVdf.substring(open, end)).associate { it.groupValues[1] to it.groupValues[2] }
|
||||
}
|
||||
|
||||
fun compatToolName(compatibilityToolVdf: String): String? = COMPAT_NAME.find(compatibilityToolVdf)?.groupValues?.get(1)
|
||||
|
||||
fun leftoverId(name: String): Int? = name.toLongOrNull()?.takeIf { it in 1 until (1L shl 31) }?.toInt()
|
||||
|
||||
fun isStubPrefix(prefix: File): Boolean = prefix.walkTopDown().onEnter { !java.nio.file.Files.isSymbolicLink(it.toPath()) }
|
||||
.none { it.isFile && it.name != "steam_autocloud.vdf" && !java.nio.file.Files.isSymbolicLink(it.toPath()) }
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package com.droiddeck.launcher.storage
|
||||
|
||||
import android.content.Context
|
||||
import android.os.StatFs
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import com.droiddeck.launcher.frontend.Library
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.runtime.FlatpakManager
|
||||
import com.droiddeck.launcher.session.ComponentsManager
|
||||
import com.droiddeck.launcher.session.SecondaryLibrary
|
||||
import com.droiddeck.launcher.session.SessionPaths
|
||||
import java.io.File
|
||||
|
||||
enum class Bucket { GAMES, GAME_DATA, TOOLS, COMPONENTS, STORES, DESKTOP, SYSTEM, DROIDDECK }
|
||||
|
||||
class GameUsage(
|
||||
val appId: Int, val name: String, val art: File?, val library: String, val internal: Boolean, val lastPlayed: Long,
|
||||
val game: Long, val prefix: Long, val shaders: Long, val workshop: Long, val shaderDir: File?,
|
||||
) {
|
||||
val data: Long get() = prefix + shaders + workshop
|
||||
val total: Long get() = game + data
|
||||
}
|
||||
|
||||
enum class ToolState { IN_USE, REQUIRED, UNUSED }
|
||||
enum class ToolReason { DEFAULT_PROTON, PROTON, FEX_RUNTIME, ARM_RUNTIME, REDISTRIBUTABLES, FEX, STEAM_TOOL, OLD_RUNTIME, X86_PROTON, CUSTOM, CUSTOM_UNKNOWN }
|
||||
|
||||
class ToolUsage(
|
||||
val key: String, val name: String, val bytes: Long, val state: ToolState, val reason: ToolReason,
|
||||
val games: Int, val isDefault: Boolean, val manifests: List<File>, val dirs: List<File>,
|
||||
)
|
||||
|
||||
class Leftover(val appId: Int, val prefix: File?, val shaders: File?, val bytes: Long, val safe: Boolean)
|
||||
|
||||
class ComponentUsage(
|
||||
val comp: String, val version: String, val bytes: Long, val inUse: Boolean,
|
||||
val packageFile: String? = null, val protonId: String? = null, val protonName: String? = null,
|
||||
)
|
||||
|
||||
class FlatpakUsage(val id: String, val name: String, val bytes: Long)
|
||||
|
||||
class StorageReport(
|
||||
val at: Long,
|
||||
val total: Long,
|
||||
val free: Long,
|
||||
val buckets: Map<Bucket, Long>,
|
||||
val prefixes: Long,
|
||||
val shaders: Long,
|
||||
val workshop: Long,
|
||||
val flatpak: Long,
|
||||
val games: List<GameUsage>,
|
||||
val tools: List<ToolUsage>,
|
||||
val leftovers: List<Leftover>,
|
||||
val logs: List<File>,
|
||||
val logBytes: Long,
|
||||
val components: List<ComponentUsage>,
|
||||
val flatpakApps: List<FlatpakUsage>,
|
||||
val flatpakRuntimes: Long,
|
||||
val otherPrefixes: Long,
|
||||
val leftoversChecked: Boolean,
|
||||
) {
|
||||
val droiddeck: Long get() = buckets.values.sum()
|
||||
val installedShaders: Long get() = games.sumOf { it.shaders }
|
||||
val unusedTools: List<ToolUsage> get() = tools.filter { it.state == ToolState.UNUSED }
|
||||
val gamesOnSd: Int get() = games.count { !it.internal }
|
||||
val unusedComponents: List<ComponentUsage> get() = components.filter { !it.inUse }
|
||||
val flatpakCache: Long get() = (flatpak - flatpakApps.sumOf { it.bytes } - flatpakRuntimes).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
object StorageScan {
|
||||
private class Walker {
|
||||
private val seen = HashSet<Long>()
|
||||
val known = HashMap<String, Long>()
|
||||
|
||||
fun size(file: File): Long {
|
||||
known[file.path]?.let { return it }
|
||||
val st = try { Os.lstat(file.path) } catch (e: Exception) { return 0 }
|
||||
if (st.st_nlink > 1 && !OsConstants.S_ISDIR(st.st_mode)) {
|
||||
if (!seen.add(st.st_ino xor (st.st_dev shl 40))) return 0
|
||||
}
|
||||
var total = st.st_blocks * 512
|
||||
if (OsConstants.S_ISDIR(st.st_mode)) file.list()?.forEach { total += size(File(file, it)) }
|
||||
return total
|
||||
}
|
||||
|
||||
fun measure(file: File): Long = size(file).also { known[file.path] = it }
|
||||
}
|
||||
|
||||
fun steamRoot(context: Context) = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam")
|
||||
|
||||
fun scan(context: Context): StorageReport {
|
||||
val walker = Walker()
|
||||
val files = context.filesDir
|
||||
val rootfs = LinuxRuntime.rootDir(context)
|
||||
val steamRoot = steamRoot(context)
|
||||
val libraries = Library.steamLibraries(context)
|
||||
val primary = runCatching { steamRoot.canonicalPath }.getOrDefault(steamRoot.path)
|
||||
val art = runCatching { Library.steamGames(context) }.getOrDefault(emptyList()).associateBy { it.appId }
|
||||
|
||||
class Lib(val root: File, val label: String, val internal: Boolean, val data: File, val listing: SteamStorage.Listing?) {
|
||||
val manifests: List<SteamStorage.Manifest>? get() = listing?.manifests
|
||||
}
|
||||
val libs = libraries.map { (root, label) ->
|
||||
val internal = runCatching { root.canonicalPath }.getOrDefault(root.path) == primary
|
||||
val data = if (internal) root else SecondaryLibrary.privateRoot(files, root)
|
||||
Lib(root, label, internal, data, SteamStorage.manifests(root))
|
||||
}
|
||||
val everyLibraryRead = libs.all { it.listing?.complete == true }
|
||||
val everyId = libs.flatMap { it.listing?.ids.orEmpty() }.toSet()
|
||||
val mappings = runCatching { SteamStorage.compatMappings(File(steamRoot, "config/config.vdf").readText()) }.getOrNull()
|
||||
val installedGames = libs.flatMap { it.manifests.orEmpty() }.filter { !SteamStorage.isTool(it) && it.installed }.map { it.appId.toString() }.toSet()
|
||||
fun gamesOn(tool: String) = mappings.orEmpty().count { (app, name) -> name == tool && app in installedGames }
|
||||
val defaultTool = mappings?.get("0")
|
||||
|
||||
val tools = LinkedHashMap<String, ToolUsage>()
|
||||
val games = ArrayList<GameUsage>()
|
||||
for (lib in libs) {
|
||||
for (m in lib.manifests.orEmpty()) {
|
||||
val dir = m.installDir
|
||||
if (SteamStorage.isTool(m)) {
|
||||
val dirs = listOfNotNull(
|
||||
dir?.let { File(lib.root, "steamapps/common/$it") },
|
||||
dir?.let { File(lib.data, "steamapps/common/$it") }.takeIf { !lib.internal },
|
||||
).filter { it.exists() }
|
||||
val bytes = dirs.sumOf { walker.measure(it) }
|
||||
val kind = SteamStorage.kind(m)
|
||||
val (state, reason) = when (kind) {
|
||||
SteamStorage.Kind.OLD_RUNTIME -> ToolState.UNUSED to ToolReason.OLD_RUNTIME
|
||||
SteamStorage.Kind.X86_PROTON -> ToolState.UNUSED to ToolReason.X86_PROTON
|
||||
SteamStorage.Kind.ARM_PROTON -> ToolState.IN_USE to (if (m.appId == SteamStorage.PROTON_ARM) ToolReason.DEFAULT_PROTON else ToolReason.PROTON)
|
||||
SteamStorage.Kind.REQUIRED -> ToolState.REQUIRED to when (m.appId) {
|
||||
SteamStorage.SNIPER -> ToolReason.FEX_RUNTIME
|
||||
SteamStorage.ARM_RUNTIME -> ToolReason.ARM_RUNTIME
|
||||
SteamStorage.REDISTRIBUTABLES -> ToolReason.REDISTRIBUTABLES
|
||||
SteamStorage.FEX -> ToolReason.FEX
|
||||
else -> ToolReason.STEAM_TOOL
|
||||
}
|
||||
}
|
||||
val count = when (m.appId) {
|
||||
SteamStorage.PROTON_ARM -> gamesOn(SteamStorage.ARM_TOOL)
|
||||
SteamStorage.PROTON_11_ARM -> gamesOn(SteamStorage.ARM_11_TOOL)
|
||||
else -> 0
|
||||
}
|
||||
val key = "app:${m.appId}"
|
||||
val prior = tools[key]
|
||||
tools[key] = ToolUsage(
|
||||
key, m.name.ifEmpty { dir ?: m.appId.toString() }, bytes + (prior?.bytes ?: 0), state, reason, count,
|
||||
false, (prior?.manifests.orEmpty()) + m.file, (prior?.dirs.orEmpty()) + dirs,
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (!m.installed) continue
|
||||
val gameBytes = dir?.let { File(lib.root, "steamapps/common/$it") }?.takeIf { it.isDirectory }?.let { walker.measure(it) } ?: 0
|
||||
val prefix = File(lib.data, "steamapps/compatdata/${m.appId}").takeIf { it.isDirectory }?.let { walker.measure(it) } ?: 0
|
||||
val shaderDir = File(lib.data, "steamapps/shadercache/${m.appId}").takeIf { it.isDirectory }
|
||||
val shaders = shaderDir?.let { walker.measure(it) } ?: 0
|
||||
val workshop = File(lib.root, "steamapps/workshop/content/${m.appId}").takeIf { it.isDirectory }?.let { walker.measure(it) } ?: 0
|
||||
val known = art[m.appId]
|
||||
games.add(GameUsage(
|
||||
m.appId, known?.name ?: m.name, known?.art, lib.label, lib.internal, m.lastPlayed,
|
||||
gameBytes, prefix, shaders, workshop, shaderDir,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
File(steamRoot, "compatibilitytools.d").listFiles()?.sortedBy { it.name }?.forEach { dir ->
|
||||
if (!dir.isDirectory) return@forEach
|
||||
val bytes = walker.measure(dir)
|
||||
if (dir.name.startsWith(".") || dir.name.startsWith("droiddeck-") || dir.name.startsWith("bannerlator-") ||
|
||||
!File(dir, "toolmanifest.vdf").isFile) return@forEach
|
||||
val name = runCatching { SteamStorage.compatToolName(File(dir, "compatibilitytool.vdf").readText()) }.getOrNull() ?: dir.name
|
||||
val count = gamesOn(name)
|
||||
val isDefault = defaultTool == name
|
||||
tools["dir:${dir.name}"] = ToolUsage(
|
||||
"dir:${dir.name}", dir.name, bytes,
|
||||
when {
|
||||
mappings == null -> ToolState.REQUIRED
|
||||
count > 0 || isDefault -> ToolState.IN_USE
|
||||
else -> ToolState.UNUSED
|
||||
},
|
||||
if (mappings == null) ToolReason.CUSTOM_UNKNOWN else ToolReason.CUSTOM, count, isDefault, emptyList(), listOf(dir),
|
||||
)
|
||||
}
|
||||
|
||||
val leftovers = ArrayList<Leftover>()
|
||||
for (lib in libs) {
|
||||
if (!everyLibraryRead) break
|
||||
val prefixes = File(lib.data, "steamapps/compatdata").listFiles().orEmpty()
|
||||
.mapNotNull { f -> SteamStorage.leftoverId(f.name)?.takeIf { it !in everyId && f.isDirectory }?.let { it to f } }.toMap()
|
||||
val caches = File(lib.data, "steamapps/shadercache").listFiles().orEmpty()
|
||||
.mapNotNull { f -> SteamStorage.leftoverId(f.name)?.takeIf { it !in everyId && f.isDirectory }?.let { it to f } }.toMap()
|
||||
for (id in (prefixes.keys + caches.keys).sorted()) {
|
||||
val prefix = prefixes[id]
|
||||
val cache = caches[id]
|
||||
val bytes = (prefix?.let { walker.measure(it) } ?: 0) + (cache?.let { walker.measure(it) } ?: 0)
|
||||
val safe = prefix == null || SteamStorage.isStubPrefix(prefix)
|
||||
leftovers.add(Leftover(id, prefix, cache, bytes, safe))
|
||||
}
|
||||
}
|
||||
|
||||
val logs = SessionPaths.sessionFolders(context).filter { it != SessionPaths.current() } +
|
||||
listOf(File(LinuxRuntime.logDir(context), SessionPaths.TOOLS_DIR)).filter { it.exists() }
|
||||
val logBytes = logs.sumOf { walker.measure(it) }
|
||||
|
||||
val flatpakDir = File(rootfs, "root/.local/share/flatpak")
|
||||
val flatpakNames = runCatching { FlatpakManager.installedApps(context) }.getOrDefault(emptyList()).associate { it.id to it.name }
|
||||
val flatpakApps = File(flatpakDir, "app").listFiles().orEmpty().filter { it.isDirectory }.map { dir ->
|
||||
FlatpakUsage(dir.name, flatpakNames[dir.name] ?: dir.name.substringAfterLast('.'), walker.measure(dir))
|
||||
}.sortedByDescending { it.bytes }
|
||||
val flatpakRuntimes = File(flatpakDir, "runtime").takeIf { it.isDirectory }?.let { walker.measure(it) } ?: 0
|
||||
|
||||
val componentsDir = File(files, "components")
|
||||
val stored = runCatching { ComponentsManager.stored(context) }.onFailure { Log.w("StorageScan", "components", it) }.getOrNull()
|
||||
val components = stored?.let { st ->
|
||||
st.packages.map { ComponentUsage(it.pkg.comp, it.pkg.version, it.pkg.size, it.inUse, packageFile = it.pkg.file) } +
|
||||
st.originals.map { ComponentUsage(it.comp, it.protonVersion, it.size, it.kept, protonId = it.protonId, protonName = it.protonName) }
|
||||
}.orEmpty()
|
||||
|
||||
val otherPrefixes = libs.flatMap { lib ->
|
||||
File(lib.data, "steamapps/compatdata").listFiles().orEmpty().filter { it.isDirectory && SteamStorage.leftoverId(it.name) == null }
|
||||
}.sumOf { walker.measure(it) }
|
||||
|
||||
val apps = File(steamRoot, "steamapps")
|
||||
val privateRoots = File(files, "steam-libraries").listFiles().orEmpty().filter { it.isDirectory }
|
||||
fun m(file: File): Long = if (file.exists()) walker.measure(file) else 0
|
||||
val toolDirs = tools.values.flatMap { it.dirs }.distinctBy { it.path }
|
||||
val toolsInApps = toolDirs.filter { it.path.startsWith(apps.path + "/") }.sumOf { walker.known[it.path] ?: 0 }
|
||||
val toolsPrivate = toolDirs.filter { d -> privateRoots.any { d.path.startsWith(it.path + "/") } }.sumOf { walker.known[it.path] ?: 0 }
|
||||
val compatTools = m(File(steamRoot, "compatibilitytools.d"))
|
||||
val installs = m(File(apps, "common")) + m(File(apps, "downloading")) + m(File(apps, "temp"))
|
||||
val privatePrefixes = privateRoots.sumOf { m(File(it, "steamapps/compatdata")) }
|
||||
val privateShaders = privateRoots.sumOf { m(File(it, "steamapps/shadercache")) }
|
||||
val prefixes = m(File(apps, "compatdata")) + privatePrefixes
|
||||
val shaders = m(File(apps, "shadercache")) + privateShaders
|
||||
val workshop = m(File(apps, "workshop"))
|
||||
val stores = m(File(rootfs, "root/Games"))
|
||||
val flatpak = m(File(rootfs, "root/.local/share/flatpak"))
|
||||
val rootfsBytes = m(rootfs)
|
||||
val privateBytes = privateRoots.sumOf { m(it) }
|
||||
val system = rootfsBytes - installs - m(File(apps, "compatdata")) - m(File(apps, "shadercache")) - workshop -
|
||||
compatTools - stores - flatpak
|
||||
val componentBytes = m(componentsDir)
|
||||
val droiddeck = m(files) + m(context.cacheDir) - rootfsBytes - privateBytes - componentBytes +
|
||||
(privateBytes - toolsPrivate - privatePrefixes - privateShaders)
|
||||
val stat = StatFs(files.path)
|
||||
return StorageReport(
|
||||
System.currentTimeMillis(), stat.totalBytes, stat.availableBytes,
|
||||
mapOf(
|
||||
Bucket.GAMES to (installs - toolsInApps).coerceAtLeast(0),
|
||||
Bucket.GAME_DATA to (prefixes + shaders + workshop),
|
||||
Bucket.TOOLS to toolsInApps + toolsPrivate + compatTools,
|
||||
Bucket.COMPONENTS to componentBytes,
|
||||
Bucket.STORES to stores,
|
||||
Bucket.DESKTOP to flatpak,
|
||||
Bucket.SYSTEM to system.coerceAtLeast(0),
|
||||
Bucket.DROIDDECK to droiddeck.coerceAtLeast(0),
|
||||
),
|
||||
prefixes, shaders, workshop, flatpak,
|
||||
games.sortedByDescending { it.total }, tools.values.toList(), leftovers, logs, logBytes,
|
||||
components, flatpakApps, flatpakRuntimes, otherPrefixes, everyLibraryRead,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package com.droiddeck.launcher.storage
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.StatFs
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.droiddeck.launcher.files.FileOps
|
||||
import com.droiddeck.launcher.runtime.FlatpakManager
|
||||
import com.droiddeck.launcher.session.ComponentsManager
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
|
||||
enum class CleanItem { OLD_TOOLS, LEFTOVERS, LEFTOVERS_UNSURE, LOGS, UNUSED_CUSTOM, UNUSED_COMPONENTS, SHADERS }
|
||||
|
||||
object StorageState {
|
||||
private const val TAG = "StorageState"
|
||||
const val LOW_SPACE_BYTES = 10L * 1024 * 1024 * 1024
|
||||
private const val FRESH_MS = 5L * 60 * 1000
|
||||
|
||||
var report by mutableStateOf<StorageReport?>(null)
|
||||
private set
|
||||
var scanning by mutableStateOf(false)
|
||||
private set
|
||||
var cleaning by mutableStateOf(false)
|
||||
private set
|
||||
var lastFreed by mutableStateOf<Long?>(null)
|
||||
var progress by mutableStateOf<String?>(null)
|
||||
private set
|
||||
var lastError by mutableStateOf<String?>(null)
|
||||
|
||||
var openRequests by mutableIntStateOf(0)
|
||||
private set
|
||||
var openPending = false
|
||||
|
||||
@Volatile var lowSpaceDismissed = false
|
||||
|
||||
private val main = Handler(Looper.getMainLooper())
|
||||
@Volatile private var appContext: Context? = null
|
||||
|
||||
fun requestOpen() {
|
||||
openPending = true
|
||||
openRequests++
|
||||
}
|
||||
|
||||
fun refresh(context: Context, force: Boolean = false) {
|
||||
val current = report
|
||||
if (scanning || cleaning) return
|
||||
if (!force && current != null && System.currentTimeMillis() - current.at < FRESH_MS) return
|
||||
val app = context.applicationContext
|
||||
scanning = true
|
||||
Thread({
|
||||
val result = runCatching { StorageScan.scan(app) }
|
||||
.onFailure { Log.w(TAG, "storage scan failed", it) }.getOrNull()
|
||||
main.post {
|
||||
if (result != null) report = result
|
||||
scanning = false
|
||||
}
|
||||
}, "storage-scan").start()
|
||||
}
|
||||
|
||||
fun freeBytes(context: Context): Long = runCatching { StatFs(context.filesDir.path).availableBytes }.getOrDefault(Long.MAX_VALUE)
|
||||
|
||||
fun lowOnSpace(context: Context): Boolean = freeBytes(context) < LOW_SPACE_BYTES
|
||||
|
||||
fun bytes(report: StorageReport, item: CleanItem): Long = when (item) {
|
||||
CleanItem.OLD_TOOLS -> report.unusedTools.filter { it.reason != ToolReason.CUSTOM }.sumOf { it.bytes }
|
||||
CleanItem.LEFTOVERS -> report.leftovers.filter { it.safe }.sumOf { it.bytes }
|
||||
CleanItem.LEFTOVERS_UNSURE -> report.leftovers.filter { !it.safe }.sumOf { it.bytes }
|
||||
CleanItem.LOGS -> report.logBytes
|
||||
CleanItem.UNUSED_CUSTOM -> report.unusedTools.filter { it.reason == ToolReason.CUSTOM }.sumOf { it.bytes }
|
||||
CleanItem.UNUSED_COMPONENTS -> report.unusedComponents.sumOf { it.bytes }
|
||||
CleanItem.SHADERS -> report.installedShaders
|
||||
}
|
||||
|
||||
val SAFE = setOf(CleanItem.OLD_TOOLS, CleanItem.LEFTOVERS, CleanItem.LOGS)
|
||||
|
||||
fun safeBytes(report: StorageReport): Long = SAFE.sumOf { bytes(report, it) }
|
||||
|
||||
fun clean(context: Context, items: Set<CleanItem>, onDone: (Long?) -> Unit) {
|
||||
if (cleaning || SessionState.running) { onDone(null); return }
|
||||
val app = context.applicationContext
|
||||
appContext = app
|
||||
cleaning = true
|
||||
Thread({
|
||||
val before = freeBytes(app)
|
||||
val fresh = runCatching { StorageScan.scan(app) }.onFailure { Log.w(TAG, "storage scan before cleanup failed", it) }.getOrNull()
|
||||
if (fresh != null && !SessionState.running) {
|
||||
for (item in items) runCatching { cleanItem(fresh, item) }.onFailure { Log.w(TAG, "could not clean $item", it) }
|
||||
}
|
||||
val freed = if (fresh == null) null else (freeBytes(app) - before).coerceAtLeast(0)
|
||||
Log.i(TAG, "cleaned ${items.joinToString()} and freed $freed bytes")
|
||||
finish(app, freed, onDone)
|
||||
}, "storage-clean").start()
|
||||
}
|
||||
|
||||
fun removeTool(context: Context, tool: ToolUsage, onDone: (Long?) -> Unit) {
|
||||
if (tool.state != ToolState.UNUSED || cleaning || SessionState.running) { onDone(null); return }
|
||||
val app = context.applicationContext
|
||||
cleaning = true
|
||||
Thread({
|
||||
val before = freeBytes(app)
|
||||
val current = runCatching { StorageScan.scan(app) }.getOrNull()?.tools?.firstOrNull { it.key == tool.key }
|
||||
val freed = if (current?.state == ToolState.UNUSED && !SessionState.running) {
|
||||
runCatching { deleteTool(current) }.onFailure { Log.w(TAG, "could not remove ${tool.key}", it) }
|
||||
(freeBytes(app) - before).coerceAtLeast(0)
|
||||
} else {
|
||||
Log.w(TAG, "kept ${tool.key}: it is in use now or could not be checked")
|
||||
null
|
||||
}
|
||||
finish(app, freed, onDone)
|
||||
}, "storage-remove-tool").start()
|
||||
}
|
||||
|
||||
// Every action works from a fresh scan so nothing is deleted on the strength of an older one.
|
||||
private fun act(context: Context, name: String, action: (Context, StorageReport) -> Boolean, onDone: (Long?) -> Unit = {}) {
|
||||
if (cleaning || SessionState.running) { onDone(null); return }
|
||||
val app = context.applicationContext
|
||||
appContext = app
|
||||
cleaning = true
|
||||
lastError = null
|
||||
Thread({
|
||||
val before = freeBytes(app)
|
||||
val fresh = runCatching { StorageScan.scan(app) }.getOrNull()
|
||||
val done = fresh != null && !SessionState.running &&
|
||||
runCatching { action(app, fresh) }.onFailure { Log.w(TAG, "$name failed", it); main.post { lastError = it.message } }.getOrDefault(false)
|
||||
val freed = if (done) (freeBytes(app) - before).coerceAtLeast(0) else null
|
||||
Log.i(TAG, "$name: freed $freed bytes")
|
||||
main.post { progress = null }
|
||||
finish(app, freed, onDone)
|
||||
}, "storage-$name").start()
|
||||
}
|
||||
|
||||
fun clearShaders(context: Context, appId: Int) = act(context, "shaders-$appId", { _, fresh ->
|
||||
val dir = fresh.games.firstOrNull { it.appId == appId }?.shaderDir ?: return@act false
|
||||
FileOps.delete(dir)
|
||||
})
|
||||
|
||||
fun removeLeftover(context: Context, appId: Int) = act(context, "leftover-$appId", { _, fresh ->
|
||||
val leftover = fresh.leftovers.firstOrNull { it.appId == appId } ?: return@act false
|
||||
deleteLeftover(leftover)
|
||||
true
|
||||
})
|
||||
|
||||
fun removeComponent(context: Context, usage: ComponentUsage) = act(context, "component", { app, fresh ->
|
||||
val current = fresh.components.firstOrNull { it.packageFile == usage.packageFile && it.protonId == usage.protonId &&
|
||||
it.comp == usage.comp && it.version == usage.version } ?: return@act false
|
||||
!current.inUse && deleteComponent(app, current)
|
||||
})
|
||||
|
||||
fun uninstallFlatpak(context: Context, id: String) = act(context, "flatpak-$id", { app, fresh ->
|
||||
if (fresh.flatpakApps.none { it.id == id }) return@act false
|
||||
val error = FlatpakManager.uninstall(app, id) { step, _ -> main.post { progress = step } }
|
||||
if (error != null) main.post { lastError = error }
|
||||
error == null
|
||||
})
|
||||
|
||||
fun pruneFlatpak(context: Context) = act(context, "flatpak-prune", { app, _ ->
|
||||
val error = FlatpakManager.prune(app) { step, _ -> main.post { progress = step } }
|
||||
if (error != null) main.post { lastError = error }
|
||||
error == null
|
||||
})
|
||||
|
||||
private fun deleteComponent(app: Context, usage: ComponentUsage): Boolean {
|
||||
if (usage.inUse) return false
|
||||
usage.packageFile?.let { file ->
|
||||
return runCatching { ComponentsManager.deletePackage(app, file) }.onFailure { Log.w(TAG, "kept $file", it) }.isSuccess
|
||||
}
|
||||
val proton = usage.protonId ?: return false
|
||||
return ComponentsManager.deleteUnusedOriginal(app, proton, usage.comp, usage.version)
|
||||
}
|
||||
|
||||
private fun finish(app: Context, freed: Long?, onDone: (Long?) -> Unit) {
|
||||
val rescanned = runCatching { StorageScan.scan(app) }.getOrNull()
|
||||
main.post {
|
||||
if (rescanned != null) report = rescanned
|
||||
cleaning = false
|
||||
lastFreed = freed
|
||||
onDone(freed)
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanItem(report: StorageReport, item: CleanItem) {
|
||||
when (item) {
|
||||
CleanItem.OLD_TOOLS -> report.unusedTools.filter { it.reason != ToolReason.CUSTOM }.forEach(::deleteTool)
|
||||
CleanItem.UNUSED_CUSTOM -> report.unusedTools.filter { it.reason == ToolReason.CUSTOM }.forEach(::deleteTool)
|
||||
CleanItem.UNUSED_COMPONENTS -> report.unusedComponents.forEach { deleteComponent(appContext!!, it) }
|
||||
CleanItem.LEFTOVERS -> report.leftovers.filter { it.safe }.forEach(::deleteLeftover)
|
||||
CleanItem.LEFTOVERS_UNSURE -> report.leftovers.filter { !it.safe }.forEach(::deleteLeftover)
|
||||
CleanItem.LOGS -> report.logs.forEach { FileOps.delete(it) }
|
||||
CleanItem.SHADERS -> report.games.mapNotNull { it.shaderDir }.forEach { FileOps.delete(it) }
|
||||
}
|
||||
}
|
||||
|
||||
// Steam treats an app as installed while its manifest exists, so the manifest goes first.
|
||||
private fun deleteTool(tool: ToolUsage) {
|
||||
tool.manifests.forEach { check(!it.exists() || it.delete()) { "cannot delete $it" } }
|
||||
tool.dirs.forEach { FileOps.delete(it) }
|
||||
}
|
||||
|
||||
private fun deleteLeftover(leftover: Leftover) {
|
||||
leftover.prefix?.let { FileOps.delete(it) }
|
||||
leftover.shaders?.let { FileOps.delete(it) }
|
||||
}
|
||||
|
||||
fun format(bytes: Long): String {
|
||||
val gb = bytes / (1024.0 * 1024 * 1024)
|
||||
return when {
|
||||
gb >= 100 -> String.format(java.util.Locale.US, "%.0f GB", gb)
|
||||
gb >= 10 -> String.format(java.util.Locale.US, "%.1f GB", gb)
|
||||
gb >= 0.995 -> String.format(java.util.Locale.US, "%.2f GB", gb)
|
||||
bytes >= 1024L * 1024 -> String.format(java.util.Locale.US, "%.0f MB", bytes / (1024.0 * 1024))
|
||||
else -> "0 MB"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.frontend.AddedGames
|
||||
import com.droiddeck.launcher.frontend.Library
|
||||
import com.droiddeck.launcher.frontend.LogoPosition
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.droiddeck.launcher.session.SessionService
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
@@ -90,6 +91,15 @@ object SteamLiveShortcuts {
|
||||
if (evaluate(context, js) != null) Log.i(TAG, "live shortcuts: art $assetType for $id")
|
||||
}
|
||||
|
||||
/** Moves and resizes a game's logo in the running client; with null the client's default position comes back. */
|
||||
fun setLogoPosition(context: Context, appId: Long, position: LogoPosition.Value?) {
|
||||
if (!clientRunning()) return
|
||||
val id = appId and 0xFFFFFFFFL
|
||||
val js = if (position == null) "(async () => { try { await SteamClient.Apps.ClearCustomLogoPositionForApp($id); } catch (e) {} return 1; })()"
|
||||
else "(async () => { try { await SteamClient.Apps.SetCustomLogoPositionForApp($id, ${JSONObject.quote(LogoPosition.toJson(position).toString())}); } catch (e) {} return 1; })()"
|
||||
if (evaluate(context, js) != null) Log.i(TAG, "live shortcuts: logo position for $id")
|
||||
}
|
||||
|
||||
private fun clientRunning(): Boolean = SessionState.running && SessionState.mode == SessionService.MODE_STEAM && !SessionState.stopRequested
|
||||
|
||||
private fun evaluate(context: Context, expression: String): String? {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.droiddeck.launcher.stores
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.droiddeck.launcher.stores.epic.EpicAchievements
|
||||
@@ -15,6 +16,7 @@ import java.util.Locale
|
||||
|
||||
abstract class StoreAchievements(val source: String, val label: String) {
|
||||
|
||||
@Immutable
|
||||
data class Game(val source: String, val id: String, val extra: Map<String, String> = emptyMap()) {
|
||||
companion object {
|
||||
fun of(item: CatalogItem) = Game(item.store.id, item.id, item.extra)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.droiddeck.launcher.stores
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.net.http.SslError
|
||||
@@ -64,6 +65,10 @@ class StoreLoginActivity : ComponentActivity() {
|
||||
private lateinit var errorPanel: LinearLayout
|
||||
private lateinit var errorText: TextView
|
||||
private lateinit var titleText: TextView
|
||||
private lateinit var topBar: LinearLayout
|
||||
private lateinit var reloadButton: ImageButton
|
||||
private lateinit var closeButton: ImageButton
|
||||
private lateinit var retryButton: Button
|
||||
private var oauthState: String? = null
|
||||
private val captured = AtomicBoolean(false)
|
||||
private var flooded = false
|
||||
@@ -130,6 +135,7 @@ class StoreLoginActivity : ComponentActivity() {
|
||||
contentDescription = getString(R.string.stores_login_close); layoutParams = LinearLayout.LayoutParams(dp(48), dp(48))
|
||||
setOnClickListener { finish() }
|
||||
}
|
||||
topBar = bar; reloadButton = reload; closeButton = close
|
||||
bar.addView(titleText); bar.addView(reload); bar.addView(close)
|
||||
progressBar = ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal).apply {
|
||||
max = 100; isIndeterminate = false
|
||||
@@ -144,6 +150,7 @@ class StoreLoginActivity : ComponentActivity() {
|
||||
}
|
||||
errorText = TextView(this).apply { setTextColor(0xFFF2F4F7.toInt()); textSize = 15f; gravity = Gravity.CENTER }
|
||||
val retry = Button(this).apply { text = getString(R.string.stores_login_retry); setOnClickListener { reloadLogin() } }
|
||||
retryButton = retry
|
||||
errorPanel.addView(errorText)
|
||||
errorPanel.addView(retry, LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { topMargin = dp(16) })
|
||||
contentHost.addView(errorPanel, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
||||
@@ -151,6 +158,24 @@ class StoreLoginActivity : ComponentActivity() {
|
||||
return root
|
||||
}
|
||||
|
||||
/**
|
||||
* The activity handles density changes itself so that folding between two displays of different
|
||||
* density keeps the page and the sign-in in progress; the dp-based chrome sizes are applied again.
|
||||
*/
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
if (!::topBar.isInitialized) return
|
||||
topBar.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(48))
|
||||
titleText.setPadding(dp(16), 0, dp(8), 0)
|
||||
titleText.textSize = 16f
|
||||
errorText.textSize = 15f
|
||||
reloadButton.layoutParams = LinearLayout.LayoutParams(dp(48), dp(48))
|
||||
closeButton.layoutParams = LinearLayout.LayoutParams(dp(48), dp(48))
|
||||
progressBar.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(3))
|
||||
errorPanel.setPadding(dp(24), dp(24), dp(24), dp(24))
|
||||
retryButton.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { topMargin = dp(16) }
|
||||
}
|
||||
|
||||
private fun showError(message: String) {
|
||||
runOnUiThread { errorText.text = message; errorPanel.visibility = View.VISIBLE; errorPanel.bringToFront(); progressBar.visibility = View.GONE }
|
||||
}
|
||||
@@ -171,6 +196,9 @@ class StoreLoginActivity : ComponentActivity() {
|
||||
setBackgroundColor(0xFF0A0B0D.toInt())
|
||||
settings.javaScriptEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
// Store sign-in pages are web pages; they never need to read local files or content:// URIs.
|
||||
settings.allowFileAccess = false
|
||||
settings.allowContentAccess = false
|
||||
settings.userAgentString = userAgent
|
||||
settings.setSupportMultipleWindows(true)
|
||||
settings.javaScriptCanOpenWindowsAutomatically = true
|
||||
|
||||
@@ -71,7 +71,7 @@ object StoresState {
|
||||
fun refresh(context: Context) {
|
||||
val app = context.applicationContext
|
||||
com.droiddeck.launcher.stores.download.SavedQueue.restore(app)
|
||||
Thread({
|
||||
background("stores-refresh") {
|
||||
val found = Store.entries.mapNotNull { store -> StoreAccounts.signedInAs(app, store)?.let { store to it } }
|
||||
val onDisk = StoreInstallRoot.gameFolders(app).mapNotNull { folder ->
|
||||
StoreGameSidecar.read(folder)?.takeIf { it.isInstalled }?.let { InstalledStoreGame(it, folder) }
|
||||
@@ -85,7 +85,7 @@ object StoresState {
|
||||
unfinished = partial
|
||||
engine = version
|
||||
}
|
||||
}, "stores-refresh").start()
|
||||
}
|
||||
}
|
||||
|
||||
fun isSignedIn(store: Store): Boolean = accounts.containsKey(store)
|
||||
@@ -136,7 +136,7 @@ object StoresState {
|
||||
|
||||
fun signOut(context: Context, store: Store) {
|
||||
val app = context.applicationContext
|
||||
Thread({
|
||||
background("stores-signout") {
|
||||
backends[store]?.signOut(app)
|
||||
if (store == Store.GOG) com.droiddeck.launcher.stores.gog.GogComet.replaceAccount { StoreAccounts.clear(app, store) }
|
||||
else StoreAccounts.clear(app, store)
|
||||
@@ -144,7 +144,7 @@ object StoresState {
|
||||
accounts.remove(store); library.remove(store); status.remove(store); problems.remove(store)
|
||||
logLine("${store.label}: signed out")
|
||||
}
|
||||
}, "stores-signout").start()
|
||||
}
|
||||
}
|
||||
|
||||
var openDownloads by mutableStateOf(false)
|
||||
@@ -199,7 +199,7 @@ object StoresState {
|
||||
val app = context.applicationContext
|
||||
val key = "${game.sidecar.store.id}:${game.sidecar.id}"
|
||||
uninstalling[key] = 0f
|
||||
Thread({
|
||||
background("stores-uninstall", onFailure = { main.post { uninstalling.remove(key); onDone() } }) {
|
||||
runCatching { backends[game.sidecar.store]?.uninstall(app, game) }
|
||||
StoreInstalls.uninstall(app, game, progress = { f -> main.post { if (key in uninstalling) uninstalling[key] = f } }, removed = {
|
||||
main.post {
|
||||
@@ -208,7 +208,7 @@ object StoresState {
|
||||
}
|
||||
})
|
||||
main.post(onDone)
|
||||
}, "stores-uninstall").start()
|
||||
}
|
||||
}
|
||||
|
||||
@Volatile var libraryListener: (() -> Unit)? = null
|
||||
@@ -231,7 +231,7 @@ object StoresState {
|
||||
appContext = context.applicationContext
|
||||
StoreLogFiles.prune(context)
|
||||
val overlay = java.io.File(com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(context), "root/.local/share/droiddeck/epic-overlay")
|
||||
if (overlay.exists()) Thread({ overlay.deleteRecursively(); Log.i(TAG, "removed the old EOS overlay download") }, "epic-overlay-cleanup").start()
|
||||
if (overlay.exists()) background("epic-overlay-cleanup") { overlay.deleteRecursively(); Log.i(TAG, "removed the old EOS overlay download") }
|
||||
val app = context.applicationContext
|
||||
Thread({
|
||||
runCatching { CloudSaves.uploadAllDirty(app, "recovery") }
|
||||
@@ -241,6 +241,21 @@ object StoresState {
|
||||
|
||||
internal fun post(block: () -> Unit) = main.post(block)
|
||||
|
||||
/**
|
||||
* A worker thread whose failure is logged. An uncaught exception on a plain Thread reaches the crash
|
||||
* handler, which ends the whole app, and none of these jobs is worth that.
|
||||
*/
|
||||
private fun background(name: String, onFailure: () -> Unit = {}, block: () -> Unit) {
|
||||
Thread({
|
||||
try {
|
||||
block()
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "$name failed", e)
|
||||
runCatching(onFailure)
|
||||
}
|
||||
}, name).start()
|
||||
}
|
||||
|
||||
private val CLOCK = SimpleDateFormat("HH:mm:ss.SSS", Locale.US)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.Image
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material.icons.outlined.Tune
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -41,6 +42,7 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -57,6 +59,8 @@ import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.frontend.AddedGameArt
|
||||
import com.droiddeck.launcher.frontend.AddedGameEdits
|
||||
import com.droiddeck.launcher.frontend.AddedGames
|
||||
import com.droiddeck.launcher.frontend.LogoPosition
|
||||
import com.droiddeck.launcher.frontend.SteamGameArt
|
||||
import com.droiddeck.launcher.frontend.SteamGridDb
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -66,6 +70,7 @@ import java.io.File
|
||||
|
||||
private fun AddedGameArt.Slot.shape(): Triple<Dp, Dp, Dp> = when (this) {
|
||||
AddedGameArt.Slot.COVER -> Triple(28.dp, 41.dp, 5.dp)
|
||||
AddedGameArt.Slot.WIDE -> Triple(60.dp, 28.dp, 5.dp)
|
||||
AddedGameArt.Slot.BACKGROUND -> Triple(60.dp, 34.dp, 5.dp)
|
||||
AddedGameArt.Slot.LOGO -> Triple(60.dp, 26.dp, 5.dp)
|
||||
AddedGameArt.Slot.ICON -> Triple(30.dp, 30.dp, 7.dp)
|
||||
@@ -75,6 +80,7 @@ private fun AddedGameArt.Slot.shape(): Triple<Dp, Dp, Dp> = when (this) {
|
||||
private fun AddedGameArt.Slot.label(): String = stringResource(
|
||||
when (this) {
|
||||
AddedGameArt.Slot.COVER -> R.string.art_cover
|
||||
AddedGameArt.Slot.WIDE -> R.string.art_wide_cover
|
||||
AddedGameArt.Slot.BACKGROUND -> R.string.art_background
|
||||
AddedGameArt.Slot.LOGO -> R.string.art_logo
|
||||
AddedGameArt.Slot.ICON -> R.string.art_icon
|
||||
@@ -142,6 +148,127 @@ internal class EditPreview(
|
||||
}
|
||||
}
|
||||
|
||||
internal class ArtTarget(
|
||||
val name: String,
|
||||
val chosenRef: (Context, AddedGameArt.Slot) -> String,
|
||||
val choices: (Context, AddedGameArt.Slot) -> AddedGameArt.Choices,
|
||||
val set: (Context, AddedGameArt.Slot, AddedGameArt.Option) -> Boolean,
|
||||
val reset: (Context, AddedGameArt.Slot) -> Unit,
|
||||
val position: (Context) -> LogoPosition.Value?,
|
||||
val setPosition: (Context, LogoPosition.Value?) -> Boolean,
|
||||
) {
|
||||
companion object {
|
||||
fun of(game: AddedGames.Game) = ArtTarget(
|
||||
game.name,
|
||||
{ c, slot -> AddedGameArt.chosenSource(c, game, slot).second },
|
||||
{ c, slot -> AddedGameArt.choices(c, game, slot) },
|
||||
{ c, slot, option -> AddedGameEdits.setArt(c, game.folder.path, slot, option) },
|
||||
{ c, slot -> AddedGameEdits.resetArt(c, game.folder.path, slot) },
|
||||
{ c -> AddedGameEdits.logoPosition(c, game.folder.path) },
|
||||
{ c, value -> AddedGameEdits.setLogoPosition(c, game.folder.path, value) },
|
||||
)
|
||||
|
||||
fun of(game: com.droiddeck.launcher.frontend.Library.SteamGame) = ArtTarget(
|
||||
game.name,
|
||||
{ c, slot -> SteamGameArt.chosenSource(c, game.appId, slot).second },
|
||||
{ c, slot -> SteamGameArt.choices(c, game, slot) },
|
||||
{ c, slot, option -> SteamGameArt.choose(c, game, slot, option) },
|
||||
{ c, slot -> SteamGameArt.reset(c, game, slot) },
|
||||
{ c -> SteamGameArt.position(c, game) },
|
||||
{ c, value -> SteamGameArt.setPosition(c, game, value) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SteamGameArtEditor(
|
||||
game: com.droiddeck.launcher.frontend.Library.SteamGame, onClose: () -> Unit, onChanged: () -> Unit,
|
||||
) {
|
||||
val target = remember(game.appId) { ArtTarget.of(game) }
|
||||
ArtOnlyEditor(game.name, target, game, { SteamGameArt.describe(it, game) }, onClose, onChanged)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun StoreGameArtEditor(folder: String, name: String, onClose: () -> Unit, onChanged: () -> Unit) {
|
||||
val app = LocalContext.current.applicationContext
|
||||
var game by remember(folder) { mutableStateOf<AddedGames.Game?>(null) }
|
||||
LaunchedEffect(folder) { game = withContext(Dispatchers.IO) { AddedGameEdits.game(app, folder) } }
|
||||
val g = game
|
||||
val target = remember(g?.appId) { g?.let { ArtTarget.of(it) } }
|
||||
ArtOnlyEditor(name, target, g, { c -> g?.let { AddedGameArt.describe(c, it) }.orEmpty() }, onClose, onChanged)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArtOnlyEditor(
|
||||
name: String, target: ArtTarget?, refresh: Any?, describe: (Context) -> List<AddedGameArt.SlotState>,
|
||||
onClose: () -> Unit, onChanged: () -> Unit,
|
||||
) {
|
||||
val app = LocalContext.current.applicationContext
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val shown = rememberShown(onClose)
|
||||
val close = { shown.targetState = false }
|
||||
var version by remember { mutableIntStateOf(0) }
|
||||
var arts by remember { mutableStateOf<List<AddedGameArt.SlotState>>(emptyList()) }
|
||||
var artSlot by remember { mutableStateOf<AddedGameArt.Slot?>(null) }
|
||||
var logoOpen by remember { mutableStateOf(false) }
|
||||
val position = rememberLogoPosition(target, version)
|
||||
LaunchedEffect(version, refresh) { if (target != null) arts = withContext(Dispatchers.IO) { describe(app) } }
|
||||
val first = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusWithinFrames({ false }) { first } }
|
||||
AppDialog(shown, close, "artEditor", wide = false, maxWidth = 460.dp) {
|
||||
DialogTitleRow(name, onClose = close)
|
||||
val maxHeight = (LocalConfiguration.current.screenHeightDp * 0.72f).dp
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.fillMaxWidth().heightIn(max = maxHeight).verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Text(stringResource(R.string.edit_artwork), fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(start = 4.dp, top = 2.dp))
|
||||
for (slot in AddedGameArt.Slot.values()) key(slot) {
|
||||
val state = arts.firstOrNull { it.slot == slot }
|
||||
FieldRow(slot.label(), focus = if (slot == AddedGameArt.Slot.COVER) first else null, onClick = { if (target != null) artSlot = slot }) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
SlotThumb(slot, state?.file)
|
||||
if (state != null) ValueText(artSourceLabel(state.source), muted = true) else Loading()
|
||||
}
|
||||
}
|
||||
}
|
||||
PickerRow(Icons.Outlined.Tune, stringResource(R.string.art_adjust_logo), logoPositionSummary(position?.value), enabled = target != null && position != null) { logoOpen = true }
|
||||
}
|
||||
}
|
||||
val chosen = target ?: return
|
||||
artSlot?.let { slot ->
|
||||
ArtChooserDialog(chosen, slot, arts.firstOrNull { it.slot == slot }, onBack = { artSlot = null }, onCloseAll = { artSlot = null; close() }) { version++; onChanged() }
|
||||
}
|
||||
val loaded = position
|
||||
if (logoOpen && loaded != null) LogoPositionHost(chosen, arts, loaded, onBack = { logoOpen = false }, onCloseAll = { logoOpen = false; close() }) { version++; onChanged() }
|
||||
}
|
||||
|
||||
/** The position as last read; null until it has been read, so the screen never opens on a guess and saves it over the real one. */
|
||||
private class LoadedPosition(val value: LogoPosition.Value?)
|
||||
|
||||
@Composable
|
||||
private fun rememberLogoPosition(target: ArtTarget?, version: Int): LoadedPosition? {
|
||||
val app = LocalContext.current.applicationContext
|
||||
var position by remember(target) { mutableStateOf<LoadedPosition?>(null) }
|
||||
LaunchedEffect(target, version) {
|
||||
position = if (target == null) null else withContext(Dispatchers.IO) { LoadedPosition(runCatching { target.position(app) }.getOrNull()) }
|
||||
}
|
||||
return position
|
||||
}
|
||||
|
||||
/** The "Adjust logo" screen for [target], over the game's background and logo as they are now. */
|
||||
@Composable
|
||||
private fun LogoPositionHost(
|
||||
target: ArtTarget, arts: List<AddedGameArt.SlotState>, start: LoadedPosition,
|
||||
onBack: () -> Unit, onCloseAll: () -> Unit, onChanged: () -> Unit,
|
||||
) {
|
||||
fun file(slot: AddedGameArt.Slot) = arts.firstOrNull { it.slot == slot }?.file
|
||||
LogoPositionDialog(
|
||||
target, hero = file(AddedGameArt.Slot.BACKGROUND) ?: file(AddedGameArt.Slot.WIDE), logo = file(AddedGameArt.Slot.LOGO), start = start.value,
|
||||
onBack = onBack, onCloseAll = onCloseAll, onChanged = onChanged,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Loading() {
|
||||
androidx.compose.material3.CircularProgressIndicator(Modifier.size(14.dp), strokeWidth = 2.dp)
|
||||
@@ -168,6 +295,9 @@ internal fun AddedGameEditor(
|
||||
var editing by remember { mutableStateOf<String?>(null) }
|
||||
var otherExe by remember { mutableStateOf(false) }
|
||||
var artSlot by remember { mutableStateOf<AddedGameArt.Slot?>(null) }
|
||||
var logoOpen by remember { mutableStateOf(false) }
|
||||
val artTarget = remember(game?.appId) { game?.let { ArtTarget.of(it) } }
|
||||
val position = rememberLogoPosition(artTarget, version)
|
||||
var unreachable by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(version) {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -241,6 +371,7 @@ internal fun AddedGameEditor(
|
||||
}
|
||||
}
|
||||
}
|
||||
PickerRow(Icons.Outlined.Tune, stringResource(R.string.art_adjust_logo), logoPositionSummary(position?.value), enabled = g != null && position != null) { logoOpen = true }
|
||||
RemoveRow(armed) {
|
||||
if (!armed) armed = true else {
|
||||
armed = false
|
||||
@@ -283,8 +414,11 @@ internal fun AddedGameEditor(
|
||||
})
|
||||
artSlot?.let { slot ->
|
||||
val current = arts.firstOrNull { it.slot == slot }
|
||||
ArtChooserDialog(g, slot, current, onBack = { artSlot = null }, onCloseAll = { artSlot = null; close() }) { version++; onChanged() }
|
||||
ArtChooserDialog(ArtTarget.of(g), slot, current, onBack = { artSlot = null }, onCloseAll = { artSlot = null; close() }) { version++; onChanged() }
|
||||
}
|
||||
val logoTarget = artTarget
|
||||
val loaded = position
|
||||
if (logoOpen && logoTarget != null && loaded != null) LogoPositionHost(logoTarget, arts, loaded, onBack = { logoOpen = false }, onCloseAll = { logoOpen = false; close() }) { version++; onChanged() }
|
||||
}
|
||||
|
||||
private fun hostPath(context: android.content.Context, game: AddedGames.Game): String {
|
||||
@@ -348,7 +482,7 @@ internal fun TextEntryDialog(title: String, initial: String, onDismiss: () -> Un
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun ArtChooserDialog(
|
||||
game: AddedGames.Game, slot: AddedGameArt.Slot, current: AddedGameArt.SlotState?,
|
||||
target: ArtTarget, slot: AddedGameArt.Slot, current: AddedGameArt.SlotState?,
|
||||
onBack: () -> Unit, onCloseAll: () -> Unit, onChanged: () -> Unit,
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
@@ -361,9 +495,9 @@ private fun ArtChooserDialog(
|
||||
var sgdb by remember { mutableStateOf(SteamGridDb.Status.OK) }
|
||||
var picking by remember { mutableStateOf(false) }
|
||||
var searching by remember { mutableStateOf(false) }
|
||||
val chosenRef = remember(current) { AddedGameArt.chosenSource(app, game, slot).second }
|
||||
val chosenRef = remember(current) { target.chosenRef(app, slot) }
|
||||
LaunchedEffect(slot) {
|
||||
val found = withContext(Dispatchers.IO) { runCatching { AddedGameArt.choices(app, game, slot) }.getOrNull() }
|
||||
val found = withContext(Dispatchers.IO) { runCatching { target.choices(app, slot) }.getOrNull() }
|
||||
options = found?.options ?: emptyList()
|
||||
sgdb = found?.sgdb ?: SteamGridDb.Status.SERVER
|
||||
}
|
||||
@@ -371,9 +505,11 @@ private fun ArtChooserDialog(
|
||||
LaunchedEffect(Unit) { focusWithinFrames({ false }) { first } }
|
||||
val apply: (AddedGameArt.Option) -> Unit = { option ->
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) { AddedGameEdits.setArt(app, game.folder.path, slot, option) }
|
||||
onChanged()
|
||||
back()
|
||||
val saved = withContext(Dispatchers.IO) { target.set(app, slot, option) }
|
||||
if (saved) {
|
||||
onChanged()
|
||||
back()
|
||||
}
|
||||
}
|
||||
}
|
||||
val slotName = slot.label()
|
||||
@@ -408,7 +544,7 @@ private fun ArtChooserDialog(
|
||||
PickerRow(Icons.Outlined.Image, stringResource(R.string.art_pick_image), null, focus = if (opts?.isEmpty() != false) first else null) { picking = true }
|
||||
PickerRow(null, stringResource(R.string.art_reset), null) {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) { AddedGameEdits.resetArt(app, game.folder.path, slot) }
|
||||
withContext(Dispatchers.IO) { target.reset(app, slot) }
|
||||
onChanged()
|
||||
back()
|
||||
}
|
||||
@@ -419,7 +555,7 @@ private fun ArtChooserDialog(
|
||||
apply(AddedGameArt.Option(AddedGameArt.FILE, file.path, file.path))
|
||||
})
|
||||
if (searching) SgdbSearchDialog(
|
||||
game, slot, current,
|
||||
target, slot, current,
|
||||
onBack = { searching = false }, onCloseAll = { searching = false; onCloseAll() }, onPick = apply,
|
||||
)
|
||||
}
|
||||
@@ -446,7 +582,7 @@ private fun SgdbNote(status: SteamGridDb.Status) {
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun SgdbSearchDialog(
|
||||
game: AddedGames.Game, slot: AddedGameArt.Slot, current: AddedGameArt.SlotState?,
|
||||
target: ArtTarget, slot: AddedGameArt.Slot, current: AddedGameArt.SlotState?,
|
||||
onBack: () -> Unit, onCloseAll: () -> Unit, onPick: (AddedGameArt.Option) -> Unit,
|
||||
) {
|
||||
val app = LocalContext.current.applicationContext
|
||||
@@ -454,28 +590,23 @@ private fun SgdbSearchDialog(
|
||||
val scope = rememberCoroutineScope()
|
||||
val shown = rememberShown(onBack)
|
||||
val back = { shown.targetState = false }
|
||||
var query by remember { mutableStateOf(game.name) }
|
||||
var query by remember { mutableStateOf(target.name) }
|
||||
var matches by remember { mutableStateOf<List<SteamGridDb.Game>?>(null) }
|
||||
var picked by remember { mutableStateOf<SteamGridDb.Game?>(null) }
|
||||
var images by remember { mutableStateOf<List<AddedGameArt.Option>>(emptyList()) }
|
||||
var page by remember { mutableIntStateOf(0) }
|
||||
var more by remember { mutableStateOf(false) }
|
||||
var busy by remember { mutableStateOf(true) }
|
||||
var status by remember { mutableStateOf(SteamGridDb.Status.OK) }
|
||||
val chosenRef = remember(current) { AddedGameArt.chosenSource(app, game, slot).second }
|
||||
val chosenRef = remember(current) { target.chosenRef(app, slot) }
|
||||
val slotName = slot.label()
|
||||
val showImages: (SteamGridDb.Game) -> Unit = { found ->
|
||||
scope.launch {
|
||||
picked = found
|
||||
images = emptyList()
|
||||
page = 0
|
||||
more = false
|
||||
busy = true
|
||||
status = SteamGridDb.Status.OK
|
||||
val result = withContext(Dispatchers.IO) { AddedGameArt.gameImages(app, found.id, slot, 0) }
|
||||
val result = withContext(Dispatchers.IO) { AddedGameArt.gameImages(app, found.id, slot) }
|
||||
images = result.items
|
||||
status = result.status
|
||||
more = result.items.isNotEmpty()
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
@@ -492,19 +623,6 @@ private fun SgdbSearchDialog(
|
||||
if (sure != null) showImages(sure)
|
||||
}
|
||||
}
|
||||
val loadMore: () -> Unit = {
|
||||
val found = picked
|
||||
if (found != null && !busy) scope.launch {
|
||||
busy = true
|
||||
val result = withContext(Dispatchers.IO) { AddedGameArt.gameImages(app, found.id, slot, page + 1) }
|
||||
val fresh = result.items.filter { next -> images.none { it.full == next.full } }
|
||||
images = images + fresh
|
||||
page += 1
|
||||
more = fresh.isNotEmpty()
|
||||
if (result.status != SteamGridDb.Status.OK) status = result.status
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) { search() }
|
||||
AppDialog(shown, back, "sgdbSearch", wide = false, maxWidth = 460.dp) {
|
||||
DialogTitleRow(stringResource(R.string.art_sgdb_title, slotName), onClose = { onCloseAll() }, leading = {
|
||||
@@ -542,7 +660,6 @@ private fun SgdbSearchDialog(
|
||||
if (images.isEmpty() && !busy && status == SteamGridDb.Status.OK) {
|
||||
Text(stringResource(R.string.art_sgdb_none), fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(start = 2.dp, top = 4.dp))
|
||||
}
|
||||
if (more && !busy) PickerRow(null, stringResource(R.string.art_sgdb_more), null) { loadMore() }
|
||||
if (list.size > 1) PickerRow(null, stringResource(R.string.art_sgdb_other_games), null) { picked = null }
|
||||
}
|
||||
if (busy) androidx.compose.material3.LinearProgressIndicator(Modifier.fillMaxWidth().padding(vertical = 8.dp))
|
||||
|
||||
@@ -56,6 +56,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
@@ -101,7 +102,7 @@ const val GPU_TAB = "gpu"
|
||||
|
||||
const val PROTON_TAB = "proton"
|
||||
|
||||
internal val LAYERS = listOf(PROTON_TAB, "dxvk", "vkd3d", "fex", GPU_TAB)
|
||||
internal val LAYERS = listOf(GPU_TAB, PROTON_TAB, "dxvk", "vkd3d", "fex")
|
||||
|
||||
internal fun layerStep(current: String, by: Int): String {
|
||||
val i = LAYERS.indexOf(current).coerceAtLeast(0)
|
||||
@@ -127,6 +128,7 @@ private val QUEUED = Color(0xFFFFB86B)
|
||||
private val ROW_SHAPE = RoundedCornerShape(10.dp)
|
||||
private val LAYER_SHAPE = RoundedCornerShape(12.dp)
|
||||
|
||||
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun ComponentsPage(
|
||||
snapshot: Snapshot?,
|
||||
@@ -417,12 +419,14 @@ fun ComponentsPage(
|
||||
var driverShown by remember { mutableStateOf<String?>(null) }
|
||||
var driverLeaving by remember { mutableStateOf(false) }
|
||||
var driverFrom by remember { mutableStateOf<Origin?>(null) }
|
||||
val slotFocus = remember { mapOf(SLOT_DISPLAY to FocusRequester(), SLOT_RUNTIME to FocusRequester()) }
|
||||
LaunchedEffect(driverPage) {
|
||||
val d = driverPage
|
||||
if (d != null) { driverLeaving = false; driverShown = d }
|
||||
else if (driverShown != null) {
|
||||
else driverShown?.let { closed ->
|
||||
if (driverFrom != null) { driverLeaving = true; delay(Motion.ms(PAGE_RETURN_MS).toLong()) }
|
||||
driverShown = null; driverLeaving = false; driverFrom = null
|
||||
if (inputMode.inputMode == InputMode.Keyboard) focusSoon { slotFocus.getValue(closed) }
|
||||
}
|
||||
}
|
||||
var narrowOpen by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
@@ -467,16 +471,6 @@ fun ComponentsPage(
|
||||
val compState = { l: String -> view?.components?.get(l) }
|
||||
val bar: @Composable (String) -> Unit = { l ->
|
||||
when (l) {
|
||||
GPU_TAB -> {
|
||||
val active = gpu.activePair
|
||||
InUseBar(
|
||||
queued = gpu.busy != null,
|
||||
sentence = (active?.let { "${it.name} ${it.version}" } ?: gpu.activeBundle ?: stringResource(R.string.gpu_builtin))
|
||||
.let { if (gpu.auto) stringResource(R.string.gpu_bar_auto, it) else it },
|
||||
note = if (gpu.busy != null) (if (gpu.percent >= 0) "${gpu.percent}%" else "…")
|
||||
else gpu.autoStatus.ifEmpty { if (active != null || gpu.activeBundle != null) "" else gpu.pairs.firstOrNull { it.recommended }?.let { stringResource(R.string.gpu_recommended_for, it.name) } ?: stringResource(R.string.gpu_refresh_hint) },
|
||||
)
|
||||
}
|
||||
PROTON_TAB -> InUseBar(
|
||||
queued = false,
|
||||
sentence = view?.let { stringResource(R.string.comp_proton_bar, it.proton.name) } ?: stringResource(R.string.comp_no_proton),
|
||||
@@ -500,51 +494,20 @@ fun ComponentsPage(
|
||||
}
|
||||
}
|
||||
|
||||
var showAllPairs by rememberSaveable { mutableStateOf(false) }
|
||||
val installedTitle: @Composable (String) -> String = { l ->
|
||||
when (l) {
|
||||
GPU_TAB -> stringResource(R.string.comp_installed_count, 1 + gpu.pairs.count { it.installed && (it.suits || showAllPairs) })
|
||||
PROTON_TAB -> stringResource(R.string.comp_installed_count, views.size)
|
||||
else -> stringResource(R.string.comp_installed_count, installedFor(l, view, snapshot).size)
|
||||
}
|
||||
}
|
||||
val availableTitle: @Composable (String) -> String = { l ->
|
||||
when (l) {
|
||||
GPU_TAB -> stringResource(R.string.comp_available_count, gpu.pairs.count { !it.installed && it.complete && (it.suits || showAllPairs) })
|
||||
PROTON_TAB -> stringResource(R.string.comp_available_count, protons.rows.count { it.installed == null || it.update != null })
|
||||
else -> stringResource(R.string.comp_nightlies_count, availableFor(l, catalog, snapshot).size)
|
||||
}
|
||||
}
|
||||
val installedRows: @Composable ColumnScope.(String, Boolean) -> Unit = { l, right ->
|
||||
when (l) {
|
||||
GPU_TAB -> {
|
||||
val autoKey = "gpu:auto"
|
||||
ChoiceLine(
|
||||
stringResource(R.string.common_auto), stringResource(R.string.gpu_auto_keeps), gpu.auto, removable = false,
|
||||
modifier = Modifier.focusRequester(firstRow).trackRow(autoKey, rowsAt),
|
||||
onSelect = {
|
||||
if (gpu.auto) return@ChoiceLine
|
||||
ask(autoKey, right, ctx.getString(R.string.gpu_use_title, ctx.getString(R.string.common_auto)), ctx.getString(R.string.gpu_auto_hint), ctx.getString(R.string.common_use), false, ctx.getString(R.string.common_auto)) {
|
||||
gpuActions.onAuto(true); flyHome(autoKey, GPU_TAB)
|
||||
}
|
||||
},
|
||||
)
|
||||
for (p in gpu.pairs.filter { it.installed && (it.suits || showAllPairs) }) {
|
||||
val key = "gpu:${p.key}"
|
||||
ChoiceLine(
|
||||
p.name, "${p.version} · ${p.detail}", !gpu.auto && p.active, removable = false,
|
||||
badge = if (p.recommended) stringResource(R.string.gpu_recommended_caps) else null,
|
||||
progress = if (gpu.busy == p.key) gpu.percent else null,
|
||||
modifier = Modifier.trackRow(key, rowsAt),
|
||||
onSelect = {
|
||||
if (!gpu.auto && p.active || gpu.busy != null) return@ChoiceLine
|
||||
ask(key, right, ctx.getString(R.string.gpu_use_title, p.name), "${p.version} · ${p.detail}", ctx.getString(R.string.common_use), false, p.name) {
|
||||
gpuActions.onAuto(false); gpuActions.onPair(p.key); flyHome(key, GPU_TAB)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
PROTON_TAB -> {
|
||||
if (views.isEmpty()) EmptyLine(stringResource(if (snapshot == null) R.string.comp_reading else R.string.comp_no_proton))
|
||||
views.forEachIndexed { i, v ->
|
||||
@@ -599,17 +562,6 @@ fun ComponentsPage(
|
||||
}
|
||||
val availableRows: @Composable ColumnScope.(String) -> Unit = { l ->
|
||||
when (l) {
|
||||
GPU_TAB -> {
|
||||
val shown = gpu.pairs.filter { !it.installed && it.complete && (it.suits || showAllPairs) }
|
||||
if (shown.isEmpty()) EmptyLine(stringResource(if (gpu.checking) R.string.gpu_checking else R.string.gpu_none_listed))
|
||||
for (p in shown) GetLine(
|
||||
p.name, "${p.version} · ${p.detail}", progress = if (gpu.busy == p.key) gpu.percent else null, enabled = gpu.busy == null,
|
||||
badge = if (p.recommended) stringResource(R.string.gpu_recommended_caps) else null,
|
||||
) { gpuActions.onAuto(false); gpuActions.onPair(p.key) }
|
||||
if (gpu.pairs.any { !it.suits }) Box(Modifier.padding(horizontal = 6.dp, vertical = 4.dp)) {
|
||||
FocusText(stringResource(if (showAllPairs) R.string.gpu_hide_other else R.string.gpu_show_other), pal.signal) { showAllPairs = !showAllPairs }
|
||||
}
|
||||
}
|
||||
PROTON_TAB -> {
|
||||
val rows = protons.rows.filter { it.installed == null || it.update != null }
|
||||
if (rows.isEmpty()) EmptyLine(stringResource(R.string.comp_nothing_new))
|
||||
@@ -640,28 +592,6 @@ fun ComponentsPage(
|
||||
val actions: @Composable (String) -> Unit = { l ->
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
when (l) {
|
||||
GPU_TAB -> {
|
||||
SmallButton(stringResource(R.string.comp_import_zip), onClick = gpuActions.onImportZip)
|
||||
var at by remember { mutableStateOf<Rect?>(null) }
|
||||
Box(Modifier.onGloballyPositioned { at = it.boundsInRoot() }) {
|
||||
FocusText(stringResource(R.string.gpu_separately) + " ›", pal.signal) {
|
||||
val from = at
|
||||
val title = ctx.getString(R.string.gpu_separately)
|
||||
if (from == null) { driverPage = "rt"; return@FocusText }
|
||||
Steps.ask(StepAsk(anchor = from, side = StepSide.Right, accent = pal.signal, pillCorner = 10.dp, items = 3, handle = {}) {
|
||||
StepTitle(title)
|
||||
StepList {
|
||||
StepChoice(ctx.getString(R.string.comp_runtime_driver), enabled = open, modifier = Modifier.fillMaxWidth().stepItem(1).focusRequester(first)) {
|
||||
Steps.fold(); driverFrom = Origin(from.translate(-pageAt), 10f * density.density); driverPage = "rt"
|
||||
}
|
||||
StepChoice(ctx.getString(R.string.comp_display_driver), enabled = open, modifier = Modifier.fillMaxWidth().stepItem(2)) {
|
||||
Steps.fold(); driverFrom = Origin(from.translate(-pageAt), 10f * density.density); driverPage = "panel"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
PROTON_TAB -> {
|
||||
val idle = protons.busyId == null && protons.runtimeReady && !protons.sessionRunning
|
||||
for (row in protons.rows.filter { it.installedAll.size > 1 }) {
|
||||
@@ -700,7 +630,10 @@ fun ComponentsPage(
|
||||
val detail: @Composable (String, Boolean) -> Unit = { l, narrowPage ->
|
||||
androidx.compose.foundation.layout.BoxWithConstraints(Modifier.fillMaxSize()) {
|
||||
val single = narrowPage || maxWidth < 600.dp
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
if (l == GPU_TAB) GpuDriversBody(gpu, gpuActions, single, firstRow, slotFocus) { slot, at ->
|
||||
driverFrom = Origin(at.translate(-pageAt), 10f * density.density); driverPage = slot
|
||||
}
|
||||
else Column(Modifier.fillMaxSize()) {
|
||||
AnimatedContent(l, transitionSpec = { fadeIn(Motion.tw(240)) togetherWith fadeOut(Motion.tw(160)) }, label = "inUse") { k -> bar(k) }
|
||||
val detailScroll = androidx.compose.runtime.key(l) { rememberScrollState() }
|
||||
Column(Modifier.weight(1f).fillMaxWidth().verticalScroll(detailScroll).padding(top = 12.dp, bottom = 16.dp)) {
|
||||
@@ -857,7 +790,8 @@ fun ComponentsPage(
|
||||
Row(Modifier.weight(1f).fillMaxWidth()) {
|
||||
Box(
|
||||
(if (narrow) Modifier.fillMaxWidth() else Modifier.width(250.dp)).fillMaxHeight()
|
||||
.onGloballyPositioned { stackAt = it.positionInRoot() },
|
||||
.onGloballyPositioned { stackAt = it.positionInRoot() }
|
||||
.focusProperties { enter = { layerFocus.getValue(layer) } }.focusGroup(),
|
||||
) {
|
||||
val tint = rememberUpdatedState(pal.signal)
|
||||
val local = cardsAt[layer]?.translate(-stackAt)
|
||||
@@ -897,17 +831,17 @@ fun ComponentsPage(
|
||||
driverShown?.let { d ->
|
||||
val body: @Composable () -> Unit = {
|
||||
Box(Modifier.fillMaxSize().background(colors.background).pointerInput(Unit) { detectTapGestures { } }) {
|
||||
if (d == "rt") DriverPage(
|
||||
if (d == SLOT_RUNTIME) DriverPage(
|
||||
title = stringResource(R.string.comp_runtime_driver), hint = stringResource(R.string.comp_runtime_driver_hint),
|
||||
rows = gpu.linuxRows, selected = gpu.linuxSelected, downloads = gpu.linuxDownloads,
|
||||
status = gpu.releaseStatus, checking = gpu.checking, importLabel = stringResource(R.string.comp_import_turnip), canRestore = false,
|
||||
status = gpu.releaseStatus, checking = gpu.checking, checkedAt = gpu.checkedAt, importLabel = stringResource(R.string.comp_import_turnip), canRestore = false,
|
||||
onSelect = gpuActions.onSelectLinux, onDelete = gpuActions.onRemoveLinux, onRefresh = gpuActions.onRefresh,
|
||||
onDownload = gpuActions.onDownloadDriver, onImport = gpuActions.onImportLinux, onRestore = {}, onBack = { driverPage = null },
|
||||
)
|
||||
else DriverPage(
|
||||
title = stringResource(R.string.comp_display_driver), hint = stringResource(R.string.comp_display_driver_hint),
|
||||
rows = gpu.androidRows, selected = gpu.androidSelected, downloads = gpu.androidDownloads,
|
||||
status = gpu.releaseStatus, checking = gpu.checking, importLabel = stringResource(R.string.comp_import_adrenotools),
|
||||
status = gpu.releaseStatus, checking = gpu.checking, checkedAt = gpu.checkedAt, importLabel = stringResource(R.string.comp_import_adrenotools),
|
||||
canRestore = gpu.canRestoreBundled,
|
||||
onSelect = gpuActions.onSelectAndroid, onDelete = gpuActions.onRemoveAndroid, onRefresh = gpuActions.onRefresh,
|
||||
onDownload = gpuActions.onDownloadDriver, onImport = gpuActions.onImportAndroid, onRestore = gpuActions.onRestoreBundled,
|
||||
@@ -1010,7 +944,7 @@ private fun Stack(
|
||||
LayerCard(
|
||||
title = LayerTitle(l),
|
||||
value = when (l) {
|
||||
GPU_TAB -> if (gpu.auto) auto else gpu.activePair?.name ?: gpu.activeBundle ?: auto
|
||||
GPU_TAB -> gpuCardValue(gpu, auto)
|
||||
PROTON_TAB -> (view?.proton?.name ?: "") + " ▾"
|
||||
else -> st?.let { if (it.activeFile == null) original else it.inUse } ?: ""
|
||||
},
|
||||
@@ -1022,6 +956,9 @@ private fun Stack(
|
||||
)
|
||||
}
|
||||
Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
StackHeading(stringResource(R.string.comp_this_device))
|
||||
card(GPU_TAB)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
StackHeading(stringResource(R.string.comp_this_proton))
|
||||
card(PROTON_TAB)
|
||||
Row(Modifier.fillMaxWidth().height(IntrinsicSize.Min)) {
|
||||
@@ -1030,9 +967,6 @@ private fun Stack(
|
||||
card("dxvk"); card("vkd3d"); card("fex")
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(6.dp))
|
||||
StackHeading(stringResource(R.string.comp_every_proton))
|
||||
card(GPU_TAB)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1534,8 +1468,8 @@ private fun ProgressBit(progress: Int) {
|
||||
private fun DeviceChip(s: GpuDriversState) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val tint = if (s.supported && !s.unsupported) pal.good else AttentionAmber
|
||||
val label = stringResource(
|
||||
val tint = if (s.supported && !s.unsupported && !s.display.pending) pal.good else AttentionAmber
|
||||
val label = if (s.display.pending) stringResource(R.string.gpu_chip_restart, s.gpuName) else stringResource(
|
||||
when {
|
||||
s.unsupported -> R.string.comp_device_unsupported
|
||||
s.supported -> R.string.comp_device_tested
|
||||
@@ -1590,3 +1524,13 @@ class ProtonRow(
|
||||
val id: String, val name: String, val installed: String?, val queued: Boolean,
|
||||
val installedAll: List<String> = listOfNotNull(installed), val update: String? = null,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun gpuCardValue(gpu: GpuDriversState, auto: String): String {
|
||||
val value = when (gpu.mode) {
|
||||
GPU_MODE_AUTO -> gpu.activePair?.name?.let { stringResource(R.string.drivers_summary, auto, it) } ?: auto
|
||||
else -> gpu.activePair?.name?.takeIf { gpu.mode == GPU_MODE_PAIR }
|
||||
?: if (gpu.display.name == gpu.runtime.name) gpu.display.name else stringResource(R.string.gpu_summary_mix, gpu.display.name, gpu.runtime.name)
|
||||
}
|
||||
return if (gpu.display.pending) stringResource(R.string.gpu_summary_restart, value) else value
|
||||
}
|
||||
@@ -65,6 +65,7 @@ fun DriverPage(
|
||||
status: String,
|
||||
checking: Boolean,
|
||||
importLabel: String,
|
||||
checkedAt: Long = Long.MAX_VALUE,
|
||||
canRestore: Boolean,
|
||||
onSelect: (String) -> Unit,
|
||||
onDelete: (String) -> Unit,
|
||||
@@ -79,6 +80,9 @@ fun DriverPage(
|
||||
val pal = LocalPalette.current
|
||||
var confirm by remember { mutableStateOf<DriverRow?>(null) }
|
||||
BackHandler(onBack = onBack)
|
||||
LaunchedEffect(Unit) {
|
||||
if (!checking && System.currentTimeMillis() - checkedAt > 6 * 3_600_000L) onRefresh()
|
||||
}
|
||||
val refreshFocus = remember { FocusRequester() }
|
||||
val selectedFocus = remember { FocusRequester() }
|
||||
val inputMode = LocalInputModeManager.current
|
||||
|
||||
@@ -67,7 +67,7 @@ internal fun GamesPage(s: FrontEndState, a: FrontEndActions, selected: String, o
|
||||
val games = remember(s.steamGames) { s.steamGames.sortedByDescending { it.lastPlayed } }
|
||||
val narrow = LocalNarrowPane.current
|
||||
val current = games.firstOrNull { "app:${it.appId}" == selected } ?: games.firstOrNull()
|
||||
GamesDialogs(a)
|
||||
GamesDialogs(a, s.steamGames)
|
||||
val onAdd: (() -> Unit)? = if (s.shortcutPicker) null else ({ GamesDialogState.adding = true })
|
||||
val pageContext = androidx.compose.ui.platform.LocalContext.current
|
||||
val onEdit: (Library.SteamGame) -> Unit = { g ->
|
||||
@@ -154,6 +154,8 @@ private fun GameActions(g: Library.SteamGame, s: FrontEndState, a: FrontEndActio
|
||||
}
|
||||
if (!s.shortcutPicker) GameShortcutMenu(g, a)
|
||||
if (!s.shortcutPicker && g.library == Library.ADDED && g.source == Library.ADDED && g.gameFiles != null) EditGameButton { onEdit(g) }
|
||||
else if (!s.shortcutPicker && g.library != Library.ADDED && g.source == Library.SOURCE_STEAM) EditGameButton { GamesDialogState.steamArt = g }
|
||||
else if (!s.shortcutPicker && g.library == Library.ADDED && g.source != Library.ADDED && g.gameFiles != null) EditGameButton { GamesDialogState.storeArt = g }
|
||||
BusyChip(s)
|
||||
}
|
||||
}
|
||||
@@ -328,11 +330,13 @@ internal object GamesDialogState {
|
||||
var summary by androidx.compose.runtime.mutableStateOf<Pair<List<com.droiddeck.launcher.frontend.AddedGames.Game>, Int>?>(null)
|
||||
var editing by androidx.compose.runtime.mutableStateOf<EditRequest?>(null)
|
||||
var checking by androidx.compose.runtime.mutableStateOf<String?>(null)
|
||||
var steamArt by androidx.compose.runtime.mutableStateOf<Library.SteamGame?>(null)
|
||||
var storeArt by androidx.compose.runtime.mutableStateOf<Library.SteamGame?>(null)
|
||||
var behindEditor: Pair<List<com.droiddeck.launcher.frontend.AddedGames.Game>, Int>? = null
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GamesDialogs(a: FrontEndActions) {
|
||||
private fun GamesDialogs(a: FrontEndActions, games: List<Library.SteamGame>) {
|
||||
val ctx = androidx.compose.ui.platform.LocalContext.current
|
||||
val scope = androidx.compose.runtime.rememberCoroutineScope()
|
||||
val st = GamesDialogState
|
||||
@@ -366,6 +370,13 @@ private fun GamesDialogs(a: FrontEndActions) {
|
||||
a.onAddedGameRemoved(folder, appId, name)
|
||||
}, onDismiss = { st.summary = null; st.checking = null })
|
||||
}
|
||||
st.steamArt?.let { game ->
|
||||
SteamGameArtEditor(games.firstOrNull { it.appId == game.appId && it.library != Library.ADDED } ?: game, onClose = { st.steamArt = null }, onChanged = { a.onSteamGameArtChanged(game.appId) })
|
||||
}
|
||||
st.storeArt?.let { game ->
|
||||
val folder = game.gameFiles?.path
|
||||
if (folder != null) StoreGameArtEditor(folder, game.name, onClose = { st.storeArt = null }, onChanged = { a.onAddedGamesChanged(listOf(folder)) })
|
||||
}
|
||||
st.editing?.let { request ->
|
||||
AddedGameEditor(
|
||||
request,
|
||||
@@ -386,8 +397,8 @@ private fun EditGameButton(onClick: () -> Unit) {
|
||||
val label = stringResource(R.string.common_edit)
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.paneItem("game:edit").size(40.dp).clip(Shape12)
|
||||
.background(if (hot) pal.signal.copy(alpha = 0.18f) else colors.surface.copy(alpha = 0.55f))
|
||||
modifier = Modifier.paneItem("game:edit").downToFirstTile().size(44.dp).clip(Shape12)
|
||||
.background(if (hot) pal.signal.copy(alpha = 0.14f) else Color.White.copy(alpha = 0.03f))
|
||||
.glideBorder(hot, Shape12, pal.signal, pal.line2)
|
||||
.hoverable(src).clickable(interactionSource = src, indication = LocalIndication.current, role = androidx.compose.ui.semantics.Role.Button, onClick = onClick)
|
||||
.controllerConfirm(onClick = onClick)
|
||||
|
||||
@@ -77,6 +77,7 @@ import com.droiddeck.launcher.frontend.Library
|
||||
import com.droiddeck.launcher.gpu.FrameGen
|
||||
import com.droiddeck.launcher.gpu.Lossless
|
||||
import com.droiddeck.launcher.input.SecondScreenDisplay
|
||||
import com.droiddeck.launcher.core.DownloadSource
|
||||
import com.droiddeck.launcher.core.PhantomProcessLimit
|
||||
import com.droiddeck.launcher.core.PhantomProcessStatus
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
@@ -131,6 +132,9 @@ class FrontEndState(
|
||||
val showPhantomGate: Boolean = false,
|
||||
val orientation: Int = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE,
|
||||
val launcherFullscreen: Boolean = true,
|
||||
val downloadSource: String = DownloadSource.MODE_AUTO,
|
||||
val downloadMirror: String = "",
|
||||
val downloadPrefix: String = "",
|
||||
val animationsEnabled: Boolean = true,
|
||||
val gameStoresEnabled: Boolean = true,
|
||||
val gameStoresSpeedTier: String = "fast",
|
||||
@@ -148,12 +152,14 @@ class FrontEndState(
|
||||
class FrontEndActions(
|
||||
val onPlay: () -> Unit,
|
||||
val onPlayDesktopUi: () -> Unit,
|
||||
val onSteamUrl: (String) -> Unit = {},
|
||||
val onSteamGame: (Library.SteamGame) -> Unit,
|
||||
val onGameShortcut: (Library.SteamGame) -> Unit = {},
|
||||
val onExportGameFile: (Library.SteamGame) -> Unit = {},
|
||||
val onSyncGameFiles: () -> Unit = {},
|
||||
val onAddGameExe: (path: String) -> Unit = {},
|
||||
val onAddedGamesChanged: (folders: List<String>) -> Unit = {},
|
||||
val onSteamGameArtChanged: (appId: Int) -> Unit = {},
|
||||
val onAddedGameRemoved: (folder: String, appId: Long?, name: String) -> Unit = { _, _, _ -> },
|
||||
val onAddedGamesArt: (Boolean) -> Unit = {},
|
||||
val onSgdbKey: (String) -> Unit = {},
|
||||
@@ -196,6 +202,9 @@ class FrontEndActions(
|
||||
val onOrientation: (Int) -> Unit = {},
|
||||
val onLauncherFullscreen: (Boolean) -> Unit = {},
|
||||
val onAnimationsEnabled: (Boolean) -> Unit = {},
|
||||
val onDownloadSource: (String) -> Unit = {},
|
||||
val onDownloadMirror: (String) -> Unit = {},
|
||||
val onDownloadPrefix: (String) -> Unit = {},
|
||||
val onGameStoresEnabled: (Boolean) -> Unit = {},
|
||||
val onGameStoresSpeedTier: (String) -> Unit = {},
|
||||
val onStoresOpenTab: (String) -> Unit = {},
|
||||
@@ -368,7 +377,11 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com
|
||||
LaunchedEffect(s.shortcutPicker) { if (s.shortcutPicker) selected = "games" }
|
||||
LaunchedEffect(s.navigate) {
|
||||
val target = s.navigate ?: return@LaunchedEffect
|
||||
if (target == "stores:downloads") { com.droiddeck.launcher.stores.StoresState.openDownloads = true; selected = "stores" } else selected = target
|
||||
when (target) {
|
||||
"stores:downloads" -> { com.droiddeck.launcher.stores.StoresState.openDownloads = true; selected = "stores" }
|
||||
"components" -> a.onComponents(true)
|
||||
else -> selected = target
|
||||
}
|
||||
a.onNavigated()
|
||||
}
|
||||
var showWirelessAdbFix by rememberSaveable { mutableStateOf(false) }
|
||||
@@ -436,6 +449,9 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com
|
||||
selected = key
|
||||
}
|
||||
}
|
||||
LaunchedEffect(com.droiddeck.launcher.storage.StorageState.openRequests) {
|
||||
if (com.droiddeck.launcher.storage.StorageState.openPending) showRailPage("setup", true)
|
||||
}
|
||||
val onRailFocus: (String) -> Unit = { key ->
|
||||
if (key != railSelection) showRailPage(key, false)
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ import androidx.compose.ui.layout.boundsInRoot
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import com.droiddeck.launcher.core.AppLanguage
|
||||
import com.droiddeck.launcher.core.DeviceSupport
|
||||
import com.droiddeck.launcher.core.DownloadSource
|
||||
import com.droiddeck.launcher.core.PhantomProcessLimit
|
||||
import com.droiddeck.launcher.core.PhantomProcessStatus
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
@@ -84,6 +85,8 @@ import com.droiddeck.launcher.R
|
||||
|
||||
private enum class CheckState { OK, WARN, BUSY }
|
||||
|
||||
private const val STORAGE_TAB = 3
|
||||
|
||||
@Composable
|
||||
private fun CheckRow(state: CheckState, title: String, detail: String?, divider: Boolean = true, action: (@Composable () -> Unit)? = null) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
@@ -164,13 +167,26 @@ internal fun SetupPanel(
|
||||
val signedIn = s.offlineAccount != null
|
||||
var showLimitDetails by rememberSaveable { mutableStateOf(false) }
|
||||
var editSgdbKey by remember { mutableStateOf(false) }
|
||||
var editDownloadPrefix by remember { mutableStateOf(false) }
|
||||
if (editSgdbKey) SgdbKeyDialog(s.sgdbUserKey, onSave = { a.onSgdbKey(it); editSgdbKey = false }, onDismiss = { editSgdbKey = false })
|
||||
val tabs = listOf(stringResource(R.string.setup_tab_overview), stringResource(R.string.setup_tab_controller), stringResource(R.string.setup_tab_launcher), stringResource(R.string.setup_tab_diagnostics))
|
||||
|
||||
if (editDownloadPrefix) TextEntryDialog(
|
||||
stringResource(R.string.setup_download_prefix), s.downloadPrefix,
|
||||
onDismiss = { editDownloadPrefix = false },
|
||||
onSave = { a.onDownloadPrefix(it); editDownloadPrefix = false },
|
||||
)
|
||||
val tabs = listOf(stringResource(R.string.setup_tab_overview), stringResource(R.string.setup_tab_controller), stringResource(R.string.setup_tab_launcher), stringResource(R.string.setup_tab_storage), stringResource(R.string.setup_tab_diagnostics))
|
||||
var tab by rememberSaveable { mutableStateOf(0) }
|
||||
val chipFocus = remember { List(4) { FocusRequester() } }
|
||||
val chipFocus = remember { List(tabs.size) { FocusRequester() } }
|
||||
var tabTurned by remember { mutableStateOf(false) }
|
||||
val pick: (Int) -> Unit = { i -> tab = i; tabTurned = true }
|
||||
val inputModeManager = LocalInputModeManager.current
|
||||
LaunchedEffect(com.droiddeck.launcher.storage.StorageState.openRequests) {
|
||||
if (com.droiddeck.launcher.storage.StorageState.openPending) {
|
||||
com.droiddeck.launcher.storage.StorageState.openPending = false
|
||||
tab = STORAGE_TAB
|
||||
}
|
||||
}
|
||||
LaunchedEffect(tab) {
|
||||
if (tabTurned && inputModeManager.inputMode == InputMode.Keyboard) {
|
||||
androidx.compose.runtime.withFrameNanos { }
|
||||
@@ -382,12 +398,42 @@ internal fun SetupPanel(
|
||||
)
|
||||
}
|
||||
}
|
||||
val download: @Composable () -> Unit = {
|
||||
SettingsGroup(stringResource(R.string.setup_group_download)) {
|
||||
ChoiceRow(
|
||||
host, "download-source", stringResource(R.string.setup_download_source), null,
|
||||
listOf(
|
||||
DownloadSource.MODE_AUTO to stringResource(R.string.setup_download_source_auto),
|
||||
DownloadSource.MODE_DIRECT to stringResource(R.string.setup_download_source_direct),
|
||||
DownloadSource.MODE_MIRROR to stringResource(R.string.setup_download_source_mirror),
|
||||
DownloadSource.MODE_CUSTOM to stringResource(R.string.setup_download_source_custom),
|
||||
),
|
||||
s.downloadSource, onPick = a.onDownloadSource,
|
||||
)
|
||||
if (s.downloadSource == DownloadSource.MODE_MIRROR) {
|
||||
ChoiceRow(
|
||||
host, "download-mirror", stringResource(R.string.setup_download_mirror), null,
|
||||
DownloadSource.MIRRORS.map { it.host to it.host },
|
||||
s.downloadMirror, onPick = a.onDownloadMirror,
|
||||
)
|
||||
}
|
||||
if (s.downloadSource == DownloadSource.MODE_CUSTOM) {
|
||||
ActionRow(
|
||||
stringResource(R.string.setup_download_prefix),
|
||||
s.downloadPrefix.takeIf { it.isNotEmpty() },
|
||||
stringResource(R.string.mode_sgdb_edit),
|
||||
onClick = { editDownloadPrefix = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wide) Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.Top) {
|
||||
Column(Modifier.weight(1f)) { look() }
|
||||
Column(Modifier.weight(1f)) { device(); art() }
|
||||
} else { look(); device(); art() }
|
||||
Column(Modifier.weight(1f)) { device(); art(); download() }
|
||||
} else { look(); device(); art(); download() }
|
||||
}
|
||||
3 -> {
|
||||
STORAGE_TAB -> StoragePanel(s, a)
|
||||
4 -> {
|
||||
SettingsGroup(stringResource(R.string.setup_tab_diagnostics)) {
|
||||
ToggleRow(host, "logs", stringResource(R.string.setup_logs), stringResource(R.string.setup_logs_hint), s.logsEnabled) { a.onLogs() }
|
||||
ToggleRow(host, "agent-commands", stringResource(R.string.setup_agent_commands), null, s.agentCommands) { a.onAgentCommands() }
|
||||
|
||||
@@ -2,7 +2,20 @@ package com.droiddeck.launcher.ui
|
||||
|
||||
import com.droiddeck.launcher.R
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.layout.LayoutCoordinates
|
||||
import androidx.compose.ui.layout.boundsInRoot
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.icons.outlined.Monitor
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material.icons.outlined.SportsEsports
|
||||
import androidx.compose.material.icons.outlined.ExpandLess
|
||||
import androidx.compose.material.icons.outlined.ExpandMore
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -58,6 +71,9 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
@@ -74,6 +90,17 @@ class PairRow(
|
||||
val active: Boolean,
|
||||
)
|
||||
|
||||
const val GPU_MODE_AUTO = "auto"
|
||||
const val GPU_MODE_PAIR = "pair"
|
||||
const val GPU_MODE_MIX = "mix"
|
||||
|
||||
class DriverSlot(
|
||||
val name: String = "",
|
||||
val source: String = "",
|
||||
val pending: Boolean = false,
|
||||
val status: String = "",
|
||||
)
|
||||
|
||||
class GpuDriversState(
|
||||
val gpuName: String = "",
|
||||
val gpuFamily: String = "",
|
||||
@@ -96,6 +123,11 @@ class GpuDriversState(
|
||||
val androidDownloads: List<DownloadRow> = emptyList(),
|
||||
val canRestoreBundled: Boolean = false,
|
||||
val activeBundle: String? = null,
|
||||
val mode: String = GPU_MODE_AUTO,
|
||||
val display: DriverSlot = DriverSlot(),
|
||||
val runtime: DriverSlot = DriverSlot(),
|
||||
val canRestart: Boolean = true,
|
||||
val checkedAt: Long = 0L,
|
||||
) {
|
||||
val activePair: PairRow? get() = pairs.firstOrNull { it.active }
|
||||
}
|
||||
@@ -113,6 +145,8 @@ class GpuDriversActions(
|
||||
val onImportAndroid: () -> Unit = {},
|
||||
val onImportZip: () -> Unit = {},
|
||||
val onRestoreBundled: () -> Unit = {},
|
||||
val onMode: (String) -> Unit = {},
|
||||
val onRestart: () -> Unit = {},
|
||||
)
|
||||
|
||||
@Composable
|
||||
@@ -168,3 +202,257 @@ private fun DeviceInfoSection(section: DeviceInfo.Section) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val STALE_MS = 6 * 3_600_000L
|
||||
|
||||
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
internal fun GpuDriversBody(
|
||||
gpu: GpuDriversState,
|
||||
actions: GpuDriversActions,
|
||||
single: Boolean,
|
||||
first: FocusRequester,
|
||||
slotFocus: Map<String, FocusRequester>,
|
||||
onOpenSlot: (String, Rect) -> Unit,
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
if (!gpu.checking && gpu.busy == null && System.currentTimeMillis() - gpu.checkedAt > STALE_MS) actions.onRefresh()
|
||||
}
|
||||
val scroll = rememberScrollState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val placed = remember { arrayOfNulls<LayoutCoordinates>(2) }
|
||||
// Focusing the top mode shows as much of the in-use card above it as fits, without hiding the mode itself.
|
||||
val revealCard = {
|
||||
val (viewport, top) = placed
|
||||
if (viewport != null && top != null && viewport.isAttached && top.isAttached) scope.launch {
|
||||
val bottom = viewport.localPositionOf(top, Offset(0f, top.size.height.toFloat())).y + scroll.value
|
||||
scroll.animateScrollTo((bottom - viewport.size.height).toInt().coerceAtLeast(0))
|
||||
}
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
modifier = Modifier.fillMaxSize().onGloballyPositioned { placed[0] = it }.verticalScroll(scroll).padding(bottom = 16.dp)
|
||||
.focusProperties { enter = { first } }.focusGroup(),
|
||||
) {
|
||||
InUseCard(gpu, actions, first)
|
||||
PanelHeading(stringResource(R.string.gpu_mode_title))
|
||||
val modes = listOf(
|
||||
Triple(GPU_MODE_AUTO, R.string.common_auto, R.string.gpu_mode_auto_detail),
|
||||
Triple(GPU_MODE_PAIR, R.string.gpu_mode_pair, R.string.gpu_mode_pair_detail),
|
||||
Triple(GPU_MODE_MIX, R.string.gpu_mode_mix, R.string.gpu_mode_mix_detail),
|
||||
)
|
||||
val choice: @Composable (Int, Modifier) -> Unit = { i, mod ->
|
||||
val (id, title, detail) = modes[i]
|
||||
PanelRow(
|
||||
stringResource(title), stringResource(detail), gpu.mode == id,
|
||||
modifier = mod.then(if (gpu.mode == id) Modifier.focusRequester(first) else Modifier)
|
||||
.then(if (i == 0) Modifier.onGloballyPositioned { placed[1] = it }.onFocusChanged { if (it.isFocused) revealCard() } else Modifier),
|
||||
onClick = { if (gpu.mode != id) actions.onMode(id) },
|
||||
)
|
||||
}
|
||||
if (single) Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { modes.indices.forEach { choice(it, Modifier) } }
|
||||
else Row(horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min)) {
|
||||
modes.indices.forEach { choice(it, Modifier.weight(1f).fillMaxHeight()) }
|
||||
}
|
||||
when (gpu.mode) {
|
||||
GPU_MODE_AUTO -> AutoBody(gpu)
|
||||
GPU_MODE_PAIR -> PairBody(gpu, actions)
|
||||
else -> MixBody(single, actions, slotFocus, onOpenSlot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InUseCard(gpu: GpuDriversState, actions: GpuDriversActions, first: FocusRequester) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val shape = RoundedCornerShape(14.dp)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.fillMaxWidth().clip(shape).background(colors.surface).border(1.dp, pal.line, shape).padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
) {
|
||||
SlotLine(Icons.Outlined.Monitor, stringResource(R.string.comp_display_driver), gpu.display)
|
||||
Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line))
|
||||
SlotLine(Icons.Outlined.SportsEsports, stringResource(R.string.comp_runtime_driver), gpu.runtime)
|
||||
if (gpu.busy != null) LinearProgressIndicator(
|
||||
progress = { (gpu.percent.coerceAtLeast(0)) / 100f }, modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
if (gpu.display.pending) RestartBanner(gpu.display.name, gpu.canRestart, first, actions.onRestart)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SlotLine(icon: ImageVector, label: String, slot: DriverSlot) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(40.dp).clip(RoundedCornerShape(10.dp)).background(colors.surfaceVariant)) {
|
||||
Icon(icon, contentDescription = null, tint = colors.onBackground, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
Column(Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(label, fontSize = 12.5.sp, color = colors.onSurfaceVariant)
|
||||
Pill(slot.source, pal.line2.copy(alpha = 0.6f), colors.onBackground)
|
||||
}
|
||||
Text(slot.name, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
if (slot.status.isNotEmpty()) Text(
|
||||
slot.status, fontSize = 12.5.sp, color = if (slot.pending) AttentionAmber else colors.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 2.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RestartBanner(target: String, canRestart: Boolean, afterLater: FocusRequester, onRestart: () -> Unit) {
|
||||
var later by rememberSaveable(target) { mutableStateOf(false) }
|
||||
if (later) return
|
||||
val shape = RoundedCornerShape(12.dp)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.fillMaxWidth().clip(shape).background(AttentionAmber.copy(alpha = 0.12f))
|
||||
.border(1.dp, AttentionAmber.copy(alpha = 0.45f), shape).padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
) {
|
||||
Icon(Icons.Outlined.Refresh, contentDescription = null, tint = AttentionAmber, modifier = Modifier.size(20.dp))
|
||||
Text(
|
||||
stringResource(if (canRestart) R.string.gpu_restart_banner else R.string.gpu_restart_blocked),
|
||||
fontSize = 13.5.sp, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.weight(1f),
|
||||
)
|
||||
FocusText(stringResource(R.string.gpu_restart_later), MaterialTheme.colorScheme.onBackground) {
|
||||
runCatching { afterLater.requestFocus() }
|
||||
later = true
|
||||
}
|
||||
if (canRestart) FocusText(stringResource(R.string.gpu_restart_now), AttentionAmber, onClick = onRestart)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AutoBody(gpu: GpuDriversState) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
PanelTitle(
|
||||
stringResource(if (gpu.activePair != null) R.string.gpu_auto_title else R.string.gpu_auto_builtin_title, gpu.gpuName),
|
||||
stringResource(R.string.gpu_auto_reason, gpu.gpuFamily),
|
||||
)
|
||||
gpu.activePair?.let { p -> PanelRow(p.name, "${p.version} · ${p.detail}", selected = true, onClick = null) }
|
||||
if (gpu.autoStatus.isNotEmpty()) Text(gpu.autoStatus, fontSize = 13.sp, color = colors.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PairBody(gpu: GpuDriversState, actions: GpuDriversActions) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
var showAll by rememberSaveable { mutableStateOf(false) }
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
PanelTitle(stringResource(R.string.gpu_pair_title), stringResource(R.string.gpu_pair_hint))
|
||||
val rows = gpu.pairs.filter { (it.installed || it.complete) && (it.suits || showAll) }
|
||||
if (rows.isEmpty()) Text(
|
||||
stringResource(if (gpu.checking) R.string.gpu_checking else R.string.gpu_none_listed),
|
||||
fontSize = 13.sp, color = colors.onSurfaceVariant,
|
||||
)
|
||||
for (p in rows) PanelRow(
|
||||
p.name, "${p.version} · ${p.detail}", selected = p.active,
|
||||
badge = if (p.recommended) stringResource(R.string.gpu_autos_pick) else null,
|
||||
trailing = when {
|
||||
gpu.busy == p.key -> "${gpu.percent.coerceAtLeast(0)}%"
|
||||
p.installed -> stringResource(R.string.gpu_installed)
|
||||
else -> stringResource(R.string.common_download)
|
||||
},
|
||||
enabled = gpu.busy == null,
|
||||
onClick = { if (!p.active) actions.onPair(p.key) },
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
if (gpu.pairs.any { !it.suits }) FocusText(stringResource(if (showAll) R.string.gpu_hide_other else R.string.gpu_show_other), pal.signal) { showAll = !showAll }
|
||||
FocusText(stringResource(R.string.comp_import_zip), pal.signal, onClick = actions.onImportZip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MixBody(single: Boolean, actions: GpuDriversActions, slotFocus: Map<String, FocusRequester>, onOpenSlot: (String, Rect) -> Unit) {
|
||||
val pal = LocalPalette.current
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
PanelTitle(stringResource(R.string.gpu_mix_title), stringResource(R.string.gpu_mix_hint))
|
||||
val at = remember { mutableMapOf<String, Rect>() }
|
||||
val slotButton: @Composable (String, Int, Modifier) -> Unit = { slot, label, mod ->
|
||||
SecondaryButton(
|
||||
stringResource(label),
|
||||
modifier = mod.focusRequester(slotFocus.getValue(slot)).onGloballyPositioned { at[slot] = it.boundsInRoot() },
|
||||
) { onOpenSlot(slot, at[slot] ?: Rect.Zero) }
|
||||
}
|
||||
val buttons: @Composable (Modifier) -> Unit = { mod ->
|
||||
slotButton(SLOT_DISPLAY, R.string.gpu_change_display, mod)
|
||||
slotButton(SLOT_RUNTIME, R.string.gpu_change_runtime, mod)
|
||||
}
|
||||
if (single) Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { buttons(Modifier.fillMaxWidth()) }
|
||||
else Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { buttons(Modifier.weight(1f)) }
|
||||
FocusText(stringResource(R.string.comp_import_zip), pal.signal, onClick = actions.onImportZip)
|
||||
}
|
||||
}
|
||||
|
||||
internal const val SLOT_DISPLAY = "panel"
|
||||
internal const val SLOT_RUNTIME = "rt"
|
||||
|
||||
@Composable
|
||||
private fun PanelHeading(text: String) {
|
||||
Text(text.uppercase(), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 1.5.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PanelTitle(title: String, hint: String) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(3.dp), modifier = Modifier.padding(top = 4.dp)) {
|
||||
Text(title, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onBackground)
|
||||
Text(hint, fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Pill(text: String, fill: Color, ink: Color) {
|
||||
if (text.isEmpty()) return
|
||||
Text(
|
||||
text, fontSize = 11.sp, fontWeight = FontWeight.Bold, letterSpacing = 0.8.sp, color = ink, maxLines = 1,
|
||||
modifier = Modifier.clip(RoundedCornerShape(6.dp)).background(fill).padding(horizontal = 7.dp, vertical = 3.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PanelRow(
|
||||
title: String,
|
||||
detail: String,
|
||||
selected: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
badge: String? = null,
|
||||
trailing: String? = null,
|
||||
enabled: Boolean = true,
|
||||
onClick: (() -> Unit)?,
|
||||
) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val src = remember { MutableInteractionSource() }
|
||||
val hot = rememberHot(src)
|
||||
val shape = RoundedCornerShape(12.dp)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = modifier.fillMaxWidth().heightIn(min = 56.dp).clip(shape)
|
||||
.background(if (hot) pal.signal.copy(alpha = 0.14f) else if (selected) pal.signal.copy(alpha = 0.08f) else colors.surface)
|
||||
.glideBorder(hot, shape, pal.signal, if (selected) pal.signal.copy(alpha = 0.7f) else pal.line)
|
||||
.then(
|
||||
if (onClick != null) Modifier.hoverable(src)
|
||||
.clickable(interactionSource = src, indication = null, enabled = enabled, role = Role.RadioButton, onClick = onClick)
|
||||
else Modifier,
|
||||
)
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.size(20.dp).border(2.dp, if (selected) pal.signal else colors.onSurfaceVariant, CircleShape),
|
||||
) { if (selected) Box(Modifier.size(10.dp).background(pal.signal, CircleShape)) }
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(title, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = if (selected) pal.signal else colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
if (detail.isNotEmpty()) Text(detail, fontSize = 12.5.sp, color = colors.onSurfaceVariant, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
if (badge != null) Pill(badge, pal.signal.copy(alpha = 0.16f), pal.signal)
|
||||
if (trailing != null) Text(trailing, fontSize = 13.sp, color = colors.onSurfaceVariant, maxLines = 1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.selected
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.frontend.LogoPosition
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
/** Steam's library hero is 3840 x 1240, so the preview keeps that shape. */
|
||||
private const val HERO_RATIO = 3840f / 1240f
|
||||
|
||||
/** The gap Steam leaves between the pinned logo and the edge of the hero, as a share of the hero's width. */
|
||||
private const val EDGE = 0.03f
|
||||
|
||||
@Composable
|
||||
private fun LogoPosition.Pin.label(): String = stringResource(
|
||||
when (this) {
|
||||
LogoPosition.Pin.UPPER_LEFT -> R.string.logo_pos_upper_left
|
||||
LogoPosition.Pin.UPPER_CENTER -> R.string.logo_pos_upper_center
|
||||
LogoPosition.Pin.CENTER -> R.string.logo_pos_center
|
||||
LogoPosition.Pin.BOTTOM_LEFT -> R.string.logo_pos_bottom_left
|
||||
LogoPosition.Pin.BOTTOM_CENTER -> R.string.logo_pos_bottom_center
|
||||
},
|
||||
)
|
||||
|
||||
private fun LogoPosition.Pin.alignment(): Alignment = when (this) {
|
||||
LogoPosition.Pin.UPPER_LEFT -> Alignment.TopStart
|
||||
LogoPosition.Pin.UPPER_CENTER -> Alignment.TopCenter
|
||||
LogoPosition.Pin.CENTER -> Alignment.Center
|
||||
LogoPosition.Pin.BOTTOM_LEFT -> Alignment.BottomStart
|
||||
LogoPosition.Pin.BOTTOM_CENTER -> Alignment.BottomCenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun logoPositionSummary(value: LogoPosition.Value?): String =
|
||||
if (value == null) stringResource(R.string.logo_pos_default) else "${value.pin.label()} · ${value.widthPct}%"
|
||||
|
||||
/**
|
||||
* Where a game's logo sits on its background in Steam's library: the logo box is [Value.widthPct] x [Value.heightPct] of the
|
||||
* background, pinned to one of five places, and the logo is drawn as large as fits in it.
|
||||
*/
|
||||
@Composable
|
||||
private fun LogoPreview(hero: File?, logo: File?, value: LogoPosition.Value) {
|
||||
val shape = RoundedCornerShape(10.dp)
|
||||
BoxWithConstraints(Modifier.fillMaxWidth().aspectRatio(HERO_RATIO).clip(shape).background(Color(0xFF0E1218)).border(1.dp, LocalPalette.current.line, shape)) {
|
||||
if (hero != null) AsyncImage(model = hero, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize())
|
||||
if (logo != null) {
|
||||
val edge = maxWidth * EDGE
|
||||
Box(
|
||||
Modifier.align(value.pin.alignment()).padding(edge)
|
||||
.size((maxWidth - edge * 2) * (value.widthPct / 100f), (maxHeight - edge * 2) * (value.heightPct / 100f)),
|
||||
) {
|
||||
AsyncImage(model = logo, contentDescription = null, contentScale = ContentScale.Fit, alignment = value.pin.alignment(), modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PinChip(label: String, selected: Boolean, onClick: () -> Unit) {
|
||||
val pal = LocalPalette.current
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val src = remember { MutableInteractionSource() }
|
||||
val hot = rememberHot(src)
|
||||
val shape = RoundedCornerShape(8.dp)
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.clip(shape)
|
||||
.background(if (selected) pal.signal.copy(alpha = 0.14f) else Color.White.copy(alpha = 0.03f))
|
||||
.border(if (selected) 2.dp else 1.dp, if (selected) pal.signal else pal.line, shape)
|
||||
.glideBorder(hot, shape, pal.signal)
|
||||
.hoverable(src)
|
||||
.clickable(interactionSource = src, indication = androidx.compose.foundation.LocalIndication.current, role = Role.RadioButton, onClick = onClick)
|
||||
.controllerConfirm(onClick = onClick)
|
||||
.semantics { contentDescription = label; this.selected = selected }
|
||||
.padding(horizontal = 12.dp, vertical = 9.dp),
|
||||
) {
|
||||
Text(label, fontSize = 13.sp, fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal, color = colors.onBackground, maxLines = 1)
|
||||
}
|
||||
}
|
||||
|
||||
/** The "Adjust logo" screen: the game's background with its logo on it, where to pin the logo and how large to draw it. */
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
internal fun LogoPositionDialog(
|
||||
target: ArtTarget, hero: File?, logo: File?, start: LogoPosition.Value?,
|
||||
onBack: () -> Unit, onCloseAll: () -> Unit, onChanged: () -> Unit,
|
||||
) {
|
||||
val app = LocalContext.current.applicationContext
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val scope = rememberCoroutineScope()
|
||||
val shown = rememberShown(onBack)
|
||||
val back = { shown.targetState = false }
|
||||
var value by remember { mutableStateOf(start ?: LogoPosition.DEFAULT) }
|
||||
var failed by remember { mutableStateOf(false) }
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
val save: (LogoPosition.Value?) -> Unit = { next ->
|
||||
if (!busy) {
|
||||
busy = true
|
||||
scope.launch {
|
||||
val saved = withContext(Dispatchers.IO) { runCatching { target.setPosition(app, next) }.getOrDefault(false) }
|
||||
busy = false
|
||||
failed = !saved
|
||||
if (saved) {
|
||||
onChanged()
|
||||
back()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AppDialog(shown, back, "logoPosition", wide = false, maxWidth = 460.dp) {
|
||||
DialogTitleRow(stringResource(R.string.art_adjust_logo), onClose = onCloseAll, leading = {
|
||||
SmallIconButton(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.common_back), colors.onBackground, onClick = back)
|
||||
})
|
||||
val maxHeight = (LocalConfiguration.current.screenHeightDp * 0.72f).dp
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.fillMaxWidth().heightIn(max = maxHeight).verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
LogoPreview(hero, logo, value)
|
||||
Text(
|
||||
stringResource(if (logo == null) R.string.logo_pos_no_logo else R.string.logo_pos_hint),
|
||||
fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(horizontal = 2.dp),
|
||||
)
|
||||
Text(stringResource(R.string.logo_pos_position), fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(start = 2.dp, top = 2.dp))
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
for (pin in LogoPosition.Pin.values()) PinChip(pin.label(), pin == value.pin) { value = value.copy(pin = pin) }
|
||||
}
|
||||
ValueSlider(
|
||||
value.widthPct, LogoPosition.MIN_PCT..LogoPosition.MAX_PCT, step = 1, enabled = true, label = stringResource(R.string.logo_pos_size),
|
||||
format = { "$it%" },
|
||||
) { value = LogoPosition.sized(value.pin, it) }
|
||||
if (failed) Text(stringResource(R.string.logo_pos_failed), fontSize = 12.sp, color = colors.error, modifier = Modifier.padding(horizontal = 2.dp))
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End)) {
|
||||
SecondaryButton(stringResource(R.string.logo_pos_reset), enabled = !busy, compact = true) { save(null) }
|
||||
PrimaryButton(stringResource(R.string.logo_pos_save), enabled = !busy, compact = true) { save(value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import androidx.compose.ui.layout.boundsInRoot
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import com.droiddeck.launcher.core.FexPreset
|
||||
import com.droiddeck.launcher.runtime.DeckyManager
|
||||
import com.droiddeck.launcher.session.BootMovie
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.session.SessionService
|
||||
|
||||
@@ -84,6 +85,11 @@ class ModeSettings(
|
||||
val steamController: String? = null,
|
||||
val unfoldedControls: Boolean? = null,
|
||||
val runSteamAtStartup: Boolean = false,
|
||||
val bootMovie: String? = null,
|
||||
val bootMovieSound: Boolean = true,
|
||||
val bootMovieCustomName: String? = null,
|
||||
val bootMovieImporting: Boolean = false,
|
||||
val bootMovieProgress: Int = -1,
|
||||
val wifiDiscovery: Boolean? = null,
|
||||
val wifiDiscoveryPermission: Boolean = false,
|
||||
val wifiDiscoveryLocation: Boolean = false,
|
||||
@@ -128,6 +134,10 @@ class ModeSettingsActions(
|
||||
val onSteamController: (String) -> Unit = {},
|
||||
val onUnfoldedControls: (Boolean) -> Unit = {},
|
||||
val onRunSteamAtStartup: (Boolean) -> Unit = {},
|
||||
val onBootMovie: (String) -> Unit = {},
|
||||
val onBootMovieSound: (Boolean) -> Unit = {},
|
||||
val onImportBootMovie: () -> Unit = {},
|
||||
val onRemoveBootMovie: () -> Unit = {},
|
||||
val onWifiDiscovery: (Boolean) -> Unit = {},
|
||||
val onWifiDiscoverySettings: () -> Unit = {},
|
||||
val onDeckyInstall: (DeckyManager.Release) -> Unit = {},
|
||||
@@ -417,6 +427,41 @@ fun ModeSheetRows(s: ModeSettings, a: ModeSettingsActions, tab: String, extras:
|
||||
SecondaryButton(stringResource(R.string.mode_steam_repair_button), enabled = !s.steamRepairQueued, onClick = a.onSteamRepair)
|
||||
}
|
||||
}
|
||||
if (s.bootMovie != null) SettingsGroup(stringResource(R.string.boot_movie)) {
|
||||
ChoiceRow(
|
||||
host, "boot-movie", stringResource(R.string.boot_movie_video), stringResource(R.string.common_applies_next_session),
|
||||
buildList {
|
||||
add(BootMovie.DROIDDECK_30 to stringResource(R.string.boot_movie_droiddeck, 30))
|
||||
add(BootMovie.DROIDDECK_60 to stringResource(R.string.boot_movie_droiddeck, 60))
|
||||
s.bootMovieCustomName?.let { add(BootMovie.CUSTOM to it) }
|
||||
}, s.bootMovie,
|
||||
note = stringResource(R.string.boot_movie_note), onPick = a.onBootMovie,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "boot-movie-sound", stringResource(R.string.boot_movie_sound),
|
||||
stringResource(R.string.common_applies_next_session), s.bootMovieSound, onChange = a.onBootMovieSound,
|
||||
)
|
||||
SettingsRow(
|
||||
stringResource(R.string.boot_movie_custom),
|
||||
stringResource(R.string.boot_movie_custom_hint, (BootMovie.MAX_BYTES shr 20).toInt(), BootMovie.MAX_CONVERT_SECONDS),
|
||||
) {
|
||||
Row(horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(6.dp)) {
|
||||
SecondaryButton(
|
||||
when {
|
||||
s.bootMovieProgress >= 0 -> stringResource(R.string.boot_movie_converting, s.bootMovieProgress)
|
||||
s.bootMovieImporting -> stringResource(R.string.common_working)
|
||||
else -> stringResource(R.string.boot_movie_import)
|
||||
},
|
||||
enabled = !s.bootMovieImporting,
|
||||
progress = s.bootMovieProgress.takeIf { it >= 0 }?.let { it / 100f },
|
||||
onClick = a.onImportBootMovie,
|
||||
)
|
||||
if (s.bootMovieCustomName != null) SecondaryButton(
|
||||
stringResource(R.string.boot_movie_remove), enabled = !s.bootMovieImporting, onClick = a.onRemoveBootMovie,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (s.wifiDiscovery != null) SettingsGroup(stringResource(R.string.mode_network)) {
|
||||
val hint = when {
|
||||
s.wifiDiscovery && !s.wifiDiscoveryLocation -> stringResource(R.string.mode_wifi_location_off)
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.widget.HorizontalScrollView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.droiddeck.launcher.input.KeyboardHost
|
||||
import com.droiddeck.launcher.input.PadBridge
|
||||
import com.droiddeck.launcher.input.PointerGestures
|
||||
import com.droiddeck.launcher.input.SecondScreenMode
|
||||
import com.droiddeck.launcher.input.TouchpadGestures
|
||||
@@ -45,6 +46,7 @@ class SecondScreenPresentation(
|
||||
private val onSteamMenu: () -> Unit,
|
||||
private val onQam: () -> Unit,
|
||||
private val onClose: () -> Unit,
|
||||
private val pad: PadBridge? = null,
|
||||
) : Presentation(sessionActivity, display) {
|
||||
private val root = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
@@ -114,7 +116,7 @@ class SecondScreenPresentation(
|
||||
when (currentMode) {
|
||||
SecondScreenMode.KEYBOARD_TRACKPAD -> renderKeyboardAndTrackpad()
|
||||
SecondScreenMode.TERMINAL -> renderTerminal()
|
||||
SecondScreenMode.DECK_CONTROLS -> root.addView(DeckControlsPanel(context, onClose, onSteamMenu, onQam), weightParams())
|
||||
SecondScreenMode.DECK_CONTROLS -> root.addView(DeckControlsPanel(context, onClose, onSteamMenu, onQam, pad), weightParams())
|
||||
SecondScreenMode.NONE -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ internal enum class SessionDrawerPage(val firstControl: String) {
|
||||
SECOND_SCREEN("second-screen:none"),
|
||||
DISPLAY("hud"),
|
||||
EFFECTS("look"),
|
||||
VR("steamvr"),
|
||||
GAMES("anisotropy"),
|
||||
SESSION("suspend"),
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ private val SessionDrawerPage.title: Int get() = when (this) {
|
||||
SessionDrawerPage.CONTROLLER -> R.string.drawer_page_controller
|
||||
SessionDrawerPage.SECOND_SCREEN -> R.string.drawer_second_screen
|
||||
SessionDrawerPage.DISPLAY -> R.string.drawer_page_display
|
||||
SessionDrawerPage.VR -> R.string.vr_title
|
||||
SessionDrawerPage.EFFECTS -> R.string.drawer_page_effects
|
||||
SessionDrawerPage.GAMES -> R.string.game_settings_title
|
||||
SessionDrawerPage.SESSION -> R.string.mode_session
|
||||
@@ -160,6 +161,7 @@ private val SessionDrawerPage.icon get() = when (this) {
|
||||
SessionDrawerPage.CONTROLLER -> Icons.Outlined.SportsEsports
|
||||
SessionDrawerPage.SECOND_SCREEN -> Icons.Outlined.ScreenShare
|
||||
SessionDrawerPage.DISPLAY -> Icons.Outlined.DesktopWindows
|
||||
SessionDrawerPage.VR -> VrHeadsetIcon
|
||||
SessionDrawerPage.EFFECTS -> Icons.Outlined.AutoFixHigh
|
||||
SessionDrawerPage.GAMES -> Icons.Outlined.Layers
|
||||
SessionDrawerPage.SESSION -> Icons.Outlined.Settings
|
||||
@@ -209,6 +211,7 @@ class DrawerActions(
|
||||
val cardboard: Cardboard = Cardboard.OFF,
|
||||
val cardboardSensor: Boolean = false,
|
||||
val steamVr: SteamVr.Status? = null,
|
||||
val immersiveSteamVr: Boolean = false,
|
||||
val textureAnisotropy: Int? = 0,
|
||||
val textureLodBias: String? = TextureFiltering.LOD_BIAS_OFF,
|
||||
val secondScreenMode: SecondScreenMode,
|
||||
@@ -284,6 +287,9 @@ class DrawerActions(
|
||||
val onGameNoSteam: (Long, Boolean) -> Unit = { _, _ -> },
|
||||
val onComponentSwap: (protonId: String, comp: String, value: String) -> Unit = { _, _, _ -> },
|
||||
val onGameComponent: (appId: Long, comp: String, file: String?) -> Unit = { _, _, _ -> },
|
||||
val defaultProtonId: String? = null,
|
||||
val onDefaultProton: (protonId: String) -> Unit = {},
|
||||
val onGameProton: (appId: Long, protonId: String?) -> Unit = { _, _ -> },
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@@ -452,12 +458,15 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont
|
||||
stringResource(R.string.display_sharpness), null, a.upscaleSharpness, 0..100, step = 5,
|
||||
format = { "$it%" }, modifier = focus.track(page, "upscale-sharpness"), onChange = a.onUpscaleSharpness)
|
||||
}
|
||||
CardboardGroup(host, a) { key -> focus.track(page, key) }
|
||||
}
|
||||
SessionDrawerPage.EFFECTS -> {
|
||||
ScreenEffectsGroup(host, a, advancedEffectsExpanded,
|
||||
onAdvancedToggle = { advancedEffectsExpanded = !advancedEffectsExpanded }) { key -> focus.track(page, key) }
|
||||
}
|
||||
SessionDrawerPage.VR -> {
|
||||
SteamVrGroup(a) { key -> focus.track(page, key) }
|
||||
CardboardGroup(host, a) { key -> focus.track(page, key) }
|
||||
}
|
||||
SessionDrawerPage.CONTROLLER -> {
|
||||
SettingsGroup(stringResource(R.string.drawer_page_controller)) {
|
||||
a.padType?.let { type ->
|
||||
@@ -915,17 +924,32 @@ private fun CardboardGroup(host: MenuHost, a: DrawerActions, track: (String) ->
|
||||
}
|
||||
if (c.headTracking && a.cardboardSensor)
|
||||
ActionRow(stringResource(R.string.cardboard_recenter), null, stringResource(R.string.cardboard_recenter_button), a.onCardboardRecenter)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SteamVrGroup(a: DrawerActions, track: (String) -> Modifier) {
|
||||
SettingsGroup(stringResource(R.string.steamvr_title)) {
|
||||
val vrLabel = stringResource(R.string.cardboard_steamvr)
|
||||
val enterLabel = stringResource(if (a.immersiveSteamVr) R.string.steamvr_enter else R.string.cardboard_steamvr_start)
|
||||
@Composable fun button(label: String, text: String, action: String) {
|
||||
SettingsRow(label, null) {
|
||||
SecondaryButton(text, modifier = track("steamvr"), onClick = { a.onSteamVr(action) })
|
||||
}
|
||||
}
|
||||
when (a.steamVr) {
|
||||
SteamVr.Status.NOT_INSTALLED -> ActionRow(vrLabel, stringResource(R.string.cardboard_steamvr_install_hint),
|
||||
stringResource(R.string.cardboard_steamvr_install), { a.onSteamVr("install") })
|
||||
SteamVr.Status.NOT_INSTALLED -> button(vrLabel, stringResource(R.string.cardboard_steamvr_install), "install")
|
||||
SteamVr.Status.INSTALLING -> SettingsRow(vrLabel, stringResource(R.string.cardboard_steamvr_installing)) {}
|
||||
SteamVr.Status.READY -> ActionRow(vrLabel, stringResource(R.string.cardboard_steamvr_ready_hint),
|
||||
stringResource(R.string.cardboard_steamvr_start), { a.onSteamVr("start") })
|
||||
SteamVr.Status.RUNNING -> ActionRow(vrLabel, stringResource(R.string.cardboard_steamvr_running_hint),
|
||||
stringResource(R.string.cardboard_steamvr_stop), { a.onSteamVr("stop") })
|
||||
SteamVr.Status.READY -> button(vrLabel, enterLabel, "start")
|
||||
SteamVr.Status.RUNNING -> {
|
||||
if (a.immersiveSteamVr) button(vrLabel, enterLabel, "start")
|
||||
SettingsRow(vrLabel, null) {
|
||||
SecondaryButton(stringResource(R.string.cardboard_steamvr_stop),
|
||||
modifier = track(if (a.immersiveSteamVr) "steamvr-stop" else "steamvr"), onClick = { a.onSteamVr("stop") })
|
||||
}
|
||||
}
|
||||
SteamVr.Status.UNAVAILABLE -> SettingsRow(vrLabel, stringResource(R.string.cardboard_steamvr_unavailable)) {}
|
||||
null -> {}
|
||||
null -> SettingsRow(vrLabel, stringResource(R.string.steamvr_checking)) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1184,9 +1208,21 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin
|
||||
return
|
||||
}
|
||||
SettingsGroup(stringResource(R.string.drawer_page_components)) {
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 11.dp)) {
|
||||
ProfileSummaryRow(stringResource(R.string.comp_proton), gameProfile.proton.value)
|
||||
}
|
||||
DrawerStackedChoice(
|
||||
host = host,
|
||||
key = "game-cmp-proton",
|
||||
label = stringResource(R.string.comp_proton),
|
||||
hint = if (gameProfile.proton.inherited && gameProfile.proton.value.isNotEmpty()) {
|
||||
stringResource(R.string.drawer_component_inherited, gameProfile.proton.value)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
options = listOf<Pair<String?, String>>(null to stringResource(R.string.comp_game_use_general)) +
|
||||
snap.protons.map { it.proton.id to it.proton.name },
|
||||
selected = if (gameProfile.proton.inherited) null else gameProfile.protonId,
|
||||
chipModifier = track("cmp-proton"),
|
||||
onPick = { id -> a.onGameProton(gameProfile.appId, id) },
|
||||
)
|
||||
for (comp in ComponentsManager.COMPONENTS) {
|
||||
val value = gameProfile.components.getValue(comp)
|
||||
val options: List<Pair<String?, String>> =
|
||||
@@ -1217,8 +1253,7 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin
|
||||
return
|
||||
}
|
||||
val running = snap.protons.filter { it.inUseByGame }
|
||||
var pick by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val view = snap.protons.firstOrNull { it.proton.id == pick } ?: running.firstOrNull() ?: snap.protons.firstOrNull()
|
||||
val view = snap.protons.firstOrNull { it.proton.id == a.defaultProtonId } ?: running.firstOrNull() ?: snap.protons.firstOrNull()
|
||||
SettingsGroup(stringResource(R.string.drawer_running_now)) {
|
||||
val src = remember { MutableInteractionSource() }
|
||||
val hot = src.collectIsFocusedAsState().value
|
||||
@@ -1259,7 +1294,7 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin
|
||||
view.reappliedAt.takeIf { it > 0 }?.let { stringResource(R.string.drawer_reapplied, java.text.DateFormat.getTimeInstance(java.text.DateFormat.SHORT).format(java.util.Date(it * 1000))) },
|
||||
).joinToString(" · "),
|
||||
snap.protons.map { it.proton.id to it.proton.name }, p.id,
|
||||
chipModifier = track("cmp-proton"), onPick = { pick = it },
|
||||
chipModifier = track("cmp-proton"), onPick = { if (it != p.id) a.onDefaultProton(it) },
|
||||
)
|
||||
val originalLabel = stringResource(R.string.drawer_original)
|
||||
val build = ComponentsManager.safeName(p.version)
|
||||
|
||||
@@ -81,8 +81,9 @@ internal fun stepPath(
|
||||
val rbr = minOf(box, (pb - tt) / 2f)
|
||||
val rbl = minOf(lerpF(minOf(box, (pb - tt) / 2f), minOf(box, h / 2f), (sw / box).coerceIn(0f, 1f)), (r - l) / 2f)
|
||||
val rpl = minOf(box, sw / 2f, h / 2f)
|
||||
val f = minOf(fillet, sw / 2f)
|
||||
val yl = minOf(pt + rpl, pb - rbl)
|
||||
val sy = yl - rpl
|
||||
val f = minOf(fillet, sw / 2f, (sy - tt - rt).coerceAtLeast(0f))
|
||||
fun corner(rect: Rect, start: Float, sweep: Float, endX: Float, endY: Float) {
|
||||
if (rect.width < 0.5f) path.lineTo(endX, endY) else path.arcTo(rect, start, sweep, false)
|
||||
}
|
||||
@@ -94,9 +95,9 @@ internal fun stepPath(
|
||||
path.lineTo(l + rbl, pb)
|
||||
corner(Rect(l, pb - 2 * rbl, l + 2 * rbl, pb), 90f, 90f, l, pb - rbl)
|
||||
path.lineTo(l, yl)
|
||||
corner(Rect(l, pt, l + 2 * rpl, pt + 2 * rpl), 180f, 90f, l + rpl, pt)
|
||||
path.lineTo(tl - f, pt)
|
||||
corner(Rect(tl - 2 * f, pt - 2 * f, tl, pt), 90f, -90f, tl, pt - f)
|
||||
corner(Rect(l, sy, l + 2 * rpl, sy + 2 * rpl), 180f, 90f, l + rpl, sy)
|
||||
path.lineTo(tl - f, sy)
|
||||
corner(Rect(tl - 2 * f, sy - 2 * f, tl, sy), 90f, -90f, tl, sy - f)
|
||||
path.lineTo(tl, tt + rt)
|
||||
corner(Rect(tl, tt, tl + 2 * rt, tt + 2 * rt), 180f, 90f, tl + rt, tt)
|
||||
path.close()
|
||||
|
||||
@@ -0,0 +1,843 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.icons.outlined.Extension
|
||||
import androidx.compose.material.icons.outlined.Folder
|
||||
import androidx.compose.material.icons.outlined.Layers
|
||||
import androidx.compose.material.icons.outlined.Lock
|
||||
import androidx.compose.material.icons.outlined.Monitor
|
||||
import androidx.compose.material.icons.outlined.SaveAlt
|
||||
import androidx.compose.material.icons.outlined.ShoppingBag
|
||||
import androidx.compose.material.icons.outlined.Storage
|
||||
import androidx.compose.material.icons.outlined.VideogameAsset
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.storage.Bucket
|
||||
import com.droiddeck.launcher.storage.CleanItem
|
||||
import com.droiddeck.launcher.storage.ComponentUsage
|
||||
import com.droiddeck.launcher.storage.GameUsage
|
||||
import com.droiddeck.launcher.storage.StorageReport
|
||||
import com.droiddeck.launcher.storage.StorageState
|
||||
import com.droiddeck.launcher.storage.ToolReason
|
||||
import com.droiddeck.launcher.storage.ToolState
|
||||
import com.droiddeck.launcher.storage.ToolUsage
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
private val BucketColors = mapOf(
|
||||
Bucket.GAMES to Color(0xFF4B9CF5),
|
||||
Bucket.GAME_DATA to Color(0xFF8F7CF6),
|
||||
Bucket.TOOLS to Color(0xFF3FC1C9),
|
||||
Bucket.COMPONENTS to Color(0xFF9BD65B),
|
||||
Bucket.STORES to Color(0xFFF0B44C),
|
||||
Bucket.DESKTOP to Color(0xFFE57FB1),
|
||||
Bucket.SYSTEM to Color(0xFF7D8796),
|
||||
Bucket.DROIDDECK to Color(0xFF6FD08C),
|
||||
)
|
||||
private val OtherApps = Color(0xFF3A3F48)
|
||||
private val Shape22 = RoundedCornerShape(18.dp)
|
||||
|
||||
private fun gb(bytes: Long) = StorageState.format(bytes)
|
||||
private const val ONE_MB = 1024L * 1024
|
||||
|
||||
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun StoragePanel(s: FrontEndState, a: FrontEndActions) {
|
||||
val ctx = LocalContext.current
|
||||
var view by remember { mutableStateOf("overview") }
|
||||
var cleanup by remember { mutableStateOf(false) }
|
||||
val report = StorageState.report
|
||||
LaunchedEffect(Unit) { StorageState.refresh(ctx) }
|
||||
LaunchedEffect(s.sessionRunning) { if (!s.sessionRunning) StorageState.refresh(ctx, force = report != null) }
|
||||
if (report == null) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp).clip(Shape14).background(MaterialTheme.colorScheme.surface)
|
||||
.border(1.dp, LocalPalette.current.line, Shape14).padding(18.dp),
|
||||
) {
|
||||
CircularProgressIndicator(strokeWidth = 3.dp, modifier = Modifier.size(28.dp), color = LocalPalette.current.signal)
|
||||
Column {
|
||||
Text(stringResource(R.string.storage_measuring), fontSize = 16.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onBackground)
|
||||
Text(stringResource(R.string.storage_measuring_hint), fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if (cleanup) CleanUpDialog(report, s.sessionRunning) { cleanup = false }
|
||||
val top = remember { androidx.compose.foundation.relocation.BringIntoViewRequester() }
|
||||
LaunchedEffect(view) {
|
||||
StorageState.lastFreed = null
|
||||
StorageState.lastError = null
|
||||
runCatching { top.bringIntoView() }
|
||||
}
|
||||
Box(Modifier.fillMaxWidth().height(1.dp).bringIntoViewRequester(top))
|
||||
when (view) {
|
||||
"games" -> GamesView(report, s.sessionRunning, a) { view = "overview" }
|
||||
"data" -> GameDataView(report, s.sessionRunning) { view = "overview" }
|
||||
"components" -> ComponentsView(report, s.sessionRunning) { view = "overview" }
|
||||
"desktop" -> DesktopView(report, s.sessionRunning) { view = "overview" }
|
||||
"tools" -> ToolsView(report, s.sessionRunning) { view = "overview" }
|
||||
else -> Overview(report, s.sessionRunning, onCleanUp = { cleanup = true }, onOpen = { view = it })
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun Overview(report: StorageReport, sessionRunning: Boolean, onCleanUp: () -> Unit, onOpen: (String) -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val wide = !LocalNarrowPane.current
|
||||
val used = report.total - report.free
|
||||
val safe = StorageState.safeBytes(report)
|
||||
val low = report.free < StorageState.LOW_SPACE_BYTES
|
||||
ActionStatus()
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(18.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp).clip(Shape14).background(colors.surface).border(1.dp, pal.line, Shape14)
|
||||
.padding(horizontal = 18.dp, vertical = 14.dp),
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(stringResource(R.string.storage_internal), fontSize = 17.sp, fontWeight = FontWeight.Bold, color = colors.onBackground)
|
||||
Text("· " + stringResource(R.string.storage_used_of, gb(used), gb(report.total)), fontSize = 15.sp, color = colors.onSurfaceVariant)
|
||||
Pill(stringResource(R.string.storage_free, gb(report.free)), if (low) AttentionAmber else pal.good)
|
||||
}
|
||||
val time = DateFormat.getTimeInstance(DateFormat.SHORT).format(Date(report.at))
|
||||
Text(
|
||||
stringResource(R.string.storage_droiddeck_uses, gb(report.droiddeck), time) +
|
||||
if (StorageState.scanning) " · " + stringResource(R.string.storage_rescanning) else "",
|
||||
fontSize = 13.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 2.dp),
|
||||
)
|
||||
UsageBar(report, Modifier.padding(top = 12.dp, bottom = 10.dp))
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(14.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Bucket.entries.forEach { Legend(BucketColors.getValue(it), bucketLabel(it)) }
|
||||
Legend(OtherApps, stringResource(R.string.storage_other_apps))
|
||||
}
|
||||
}
|
||||
Box(Modifier.width(1.dp).height(120.dp).background(pal.line))
|
||||
Column(Modifier.widthIn(min = 170.dp, max = 210.dp)) {
|
||||
Text(stringResource(R.string.storage_safe_now), fontSize = 13.sp, color = colors.onSurfaceVariant)
|
||||
Text(gb(safe), fontSize = 26.sp, fontWeight = FontWeight.Bold, color = if (safe > 0) pal.good else colors.onBackground)
|
||||
Text(stringResource(R.string.storage_safe_hint), fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(bottom = 10.dp))
|
||||
PrimaryButton(stringResource(R.string.storage_clean_up), enabled = !sessionRunning && !StorageState.cleaning, compact = true, onClick = onCleanUp)
|
||||
}
|
||||
}
|
||||
if (sessionRunning) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Note(stringResource(R.string.storage_session_running))
|
||||
}
|
||||
SectionTitle(stringResource(R.string.storage_where), null)
|
||||
val unused = report.unusedTools.sumOf { it.bytes }
|
||||
val tiles = listOf<@Composable (Modifier) -> Unit>(
|
||||
{ m ->
|
||||
BucketCard(Bucket.GAMES, Icons.Outlined.VideogameAsset, stringResource(R.string.storage_games),
|
||||
pluralStringResource(R.plurals.storage_games_installed, report.games.size, report.games.size) +
|
||||
if (report.gamesOnSd > 0) " · " + pluralStringResource(R.plurals.storage_games_on_sd, report.gamesOnSd, report.gamesOnSd) else "",
|
||||
report.buckets.getValue(Bucket.GAMES), m) { onOpen("games") }
|
||||
},
|
||||
{ m ->
|
||||
BucketCard(Bucket.GAME_DATA, Icons.Outlined.SaveAlt, stringResource(R.string.storage_game_data),
|
||||
stringResource(R.string.storage_game_data_hint, gb(report.prefixes), gb(report.workshop), gb(report.shaders)),
|
||||
report.buckets.getValue(Bucket.GAME_DATA), m) { onOpen("data") }
|
||||
},
|
||||
{ m ->
|
||||
BucketCard(Bucket.TOOLS, Icons.Outlined.Layers, stringResource(R.string.storage_tools),
|
||||
if (unused > 0) stringResource(R.string.storage_tools_unused, gb(unused)) else pluralStringResource(R.plurals.storage_tools_count, report.tools.size, report.tools.size),
|
||||
report.buckets.getValue(Bucket.TOOLS), m, attention = unused > 0) { onOpen("tools") }
|
||||
},
|
||||
{ m ->
|
||||
val unusedComponents = report.unusedComponents.sumOf { it.bytes }
|
||||
BucketCard(Bucket.COMPONENTS, Icons.Outlined.Extension, stringResource(R.string.storage_components),
|
||||
if (unusedComponents > 0) stringResource(R.string.storage_tools_unused, gb(unusedComponents))
|
||||
else pluralStringResource(R.plurals.storage_components_count, report.components.size, report.components.size),
|
||||
report.buckets.getValue(Bucket.COMPONENTS), m, attention = unusedComponents > 0) { onOpen("components") }
|
||||
},
|
||||
{ m ->
|
||||
BucketCard(Bucket.STORES, Icons.Outlined.ShoppingBag, stringResource(R.string.storage_stores),
|
||||
stringResource(R.string.storage_stores_hint), report.buckets.getValue(Bucket.STORES), m)
|
||||
},
|
||||
{ m ->
|
||||
BucketCard(Bucket.DESKTOP, Icons.Outlined.Monitor, stringResource(R.string.storage_desktop),
|
||||
pluralStringResource(R.plurals.storage_desktop_apps, report.flatpakApps.size, report.flatpakApps.size) +
|
||||
" · " + stringResource(R.string.storage_desktop_shared, gb(report.flatpakRuntimes + report.flatpakCache)),
|
||||
report.buckets.getValue(Bucket.DESKTOP), m) { onOpen("desktop") }
|
||||
},
|
||||
{ m ->
|
||||
BucketCard(Bucket.SYSTEM, Icons.Outlined.Storage, stringResource(R.string.storage_system),
|
||||
stringResource(R.string.storage_system_hint), report.buckets.getValue(Bucket.SYSTEM), m, locked = true)
|
||||
},
|
||||
{ m ->
|
||||
BucketCard(Bucket.DROIDDECK, Icons.Outlined.Folder, stringResource(R.string.storage_app_data),
|
||||
stringResource(R.string.storage_app_data_hint, gb(report.logBytes)), report.buckets.getValue(Bucket.DROIDDECK), m)
|
||||
},
|
||||
)
|
||||
val columns = if (wide) 2 else 1
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
tiles.chunked(columns).forEach { row ->
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min)) {
|
||||
row.forEach { it(Modifier.weight(1f).fillMaxHeight()) }
|
||||
repeat(columns - row.size) { Spacer(Modifier.weight(1f)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun bucketLabel(bucket: Bucket) = stringResource(when (bucket) {
|
||||
Bucket.GAMES -> R.string.storage_legend_games
|
||||
Bucket.GAME_DATA -> R.string.storage_legend_data
|
||||
Bucket.TOOLS -> R.string.storage_legend_tools
|
||||
Bucket.COMPONENTS -> R.string.storage_legend_components
|
||||
Bucket.STORES -> R.string.storage_legend_stores
|
||||
Bucket.DESKTOP -> R.string.storage_legend_desktop
|
||||
Bucket.SYSTEM -> R.string.storage_legend_system
|
||||
Bucket.DROIDDECK -> R.string.storage_legend_app
|
||||
})
|
||||
|
||||
@Composable
|
||||
private fun UsageBar(report: StorageReport, modifier: Modifier) {
|
||||
val total = report.total.coerceAtLeast(1).toFloat()
|
||||
val other = (report.total - report.free - report.droiddeck).coerceAtLeast(0)
|
||||
Row(modifier.fillMaxWidth().height(14.dp).clip(RoundedCornerShape(7.dp)).background(Color(0xFF0A0C0F))) {
|
||||
Bucket.entries.forEach { b ->
|
||||
val v = report.buckets.getValue(b)
|
||||
if (v > 0) Box(Modifier.weight(v / total).fillMaxHeight().background(BucketColors.getValue(b)))
|
||||
}
|
||||
if (other > 0) Box(Modifier.weight(other / total).fillMaxHeight().background(OtherApps))
|
||||
if (report.free > 0) Spacer(Modifier.weight(report.free / total))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Legend(color: Color, label: String) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Box(Modifier.size(9.dp).clip(RoundedCornerShape(3.dp)).background(color))
|
||||
Text(label, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Pill(text: String, color: Color) {
|
||||
Text(
|
||||
text, fontSize = 11.sp, fontWeight = FontWeight.SemiBold, color = color, maxLines = 1,
|
||||
modifier = Modifier.clip(RoundedCornerShape(8.dp)).background(color.copy(alpha = 0.16f)).padding(horizontal = 7.dp, vertical = 2.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BucketCard(
|
||||
bucket: Bucket, icon: ImageVector, title: String, detail: String, bytes: Long, modifier: Modifier,
|
||||
attention: Boolean = false, locked: Boolean = false, onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val src = remember { MutableInteractionSource() }
|
||||
val hot = rememberHot(src) && onClick != null
|
||||
val pressed by src.collectIsPressedAsState()
|
||||
val scale by animateFloatAsState(if (pressed) 0.97f else 1f, Motion.sp(0.5f, Spring.StiffnessMedium), label = "bucketScale")
|
||||
val tint = BucketColors.getValue(bucket)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
modifier = modifier.paneItem("storage:${bucket.name}")
|
||||
.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
.clip(Shape14)
|
||||
.background(if (hot) pal.signal.copy(alpha = 0.10f) else colors.surface)
|
||||
.glideBorder(hot, Shape14, pal.signal, pal.line)
|
||||
.then(if (onClick != null) Modifier.hoverable(src)
|
||||
.clickable(interactionSource = src, indication = LocalIndication.current, role = Role.Button, onClick = onClick)
|
||||
.controllerConfirm(onClick = onClick) else Modifier.hoverable(src))
|
||||
.heightIn(min = 64.dp)
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)).background(tint)) {
|
||||
Icon(icon, contentDescription = null, tint = Color(0xFF0B0D10), modifier = Modifier.size(20.dp))
|
||||
}
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(title, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(detail, fontSize = 12.5.sp, color = if (attention) AttentionAmber else colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
Text(gb(bytes), fontSize = 16.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground)
|
||||
when {
|
||||
locked -> Icon(Icons.Outlined.Lock, contentDescription = null, tint = colors.onSurfaceVariant, modifier = Modifier.size(16.dp))
|
||||
onClick != null -> Icon(Icons.Filled.ChevronRight, contentDescription = null, tint = if (hot) pal.signal else colors.onSurfaceVariant, modifier = Modifier.size(18.dp))
|
||||
else -> Spacer(Modifier.width(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CleanUpDialog(report: StorageReport, sessionRunning: Boolean, onDismiss: () -> Unit) {
|
||||
val ctx = LocalContext.current
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val available = CleanItem.entries.filter { StorageState.bytes(report, it) > 0 }
|
||||
var picked by remember { mutableStateOf(available.filter { it in StorageState.SAFE }.toSet()) }
|
||||
val total = picked.sumOf { StorageState.bytes(report, it) }
|
||||
val first = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { runCatching { first.requestFocus() } }
|
||||
AlertDialog(
|
||||
properties = androidx.compose.ui.window.DialogProperties(usePlatformDefaultWidth = false),
|
||||
modifier = Modifier.fillMaxWidth(0.62f).widthIn(min = 420.dp, max = 760.dp),
|
||||
onDismissRequest = { if (!StorageState.cleaning) onDismiss() },
|
||||
title = { Text(stringResource(R.string.storage_clean_up_title)) },
|
||||
text = {
|
||||
Column(Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(stringResource(R.string.storage_clean_up_lede), fontSize = 13.sp, color = colors.onSurfaceVariant)
|
||||
if (available.isEmpty()) Text(stringResource(R.string.storage_nothing_to_clean), fontSize = 14.sp, color = colors.onBackground)
|
||||
available.forEachIndexed { i, item ->
|
||||
val on = item in picked
|
||||
CleanRow(
|
||||
item, report, on, if (i == 0) Modifier.focusRequester(first) else Modifier,
|
||||
) { picked = if (on) picked - item else picked + item }
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.storage_selected, gb(total), gb(report.free), gb(report.free + total)),
|
||||
fontSize = 13.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
if (StorageState.cleaning) Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
CircularProgressIndicator(strokeWidth = 2.dp, modifier = Modifier.size(16.dp), color = pal.signal)
|
||||
Text(stringResource(R.string.storage_cleaning), fontSize = 13.sp, color = colors.onBackground)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
if (picked.isNotEmpty() && !sessionRunning && !StorageState.cleaning) {
|
||||
FocusText(stringResource(R.string.storage_free_button, gb(total)), pal.signal) {
|
||||
StorageState.clean(ctx, picked) { onDismiss() }
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
FocusText(stringResource(R.string.common_cancel), colors.onBackground) { if (!StorageState.cleaning) onDismiss() }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CleanRow(item: CleanItem, report: StorageReport, on: Boolean, modifier: Modifier, onToggle: () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val src = remember { MutableInteractionSource() }
|
||||
val hot = src.collectIsFocusedAsState().value || src.collectIsHoveredAsState().value
|
||||
val unsureCount = report.leftovers.count { !it.safe }
|
||||
val safeCount = report.leftovers.count { it.safe }
|
||||
val oldTools = report.unusedTools.filter { it.reason != ToolReason.CUSTOM }
|
||||
val custom = report.unusedTools.filter { it.reason == ToolReason.CUSTOM }
|
||||
val unusedLabels = report.unusedComponents.map { componentLabel(it) }
|
||||
val (title, detail, tag) = when (item) {
|
||||
CleanItem.OLD_TOOLS -> Triple(stringResource(R.string.storage_item_runtimes), oldTools.joinToString(", ") { it.name }, 0)
|
||||
CleanItem.LEFTOVERS -> Triple(stringResource(R.string.storage_item_leftovers), pluralStringResource(R.plurals.storage_item_leftovers_detail, safeCount, safeCount), 0)
|
||||
CleanItem.LEFTOVERS_UNSURE -> Triple(stringResource(R.string.storage_item_leftovers_unsure), pluralStringResource(R.plurals.storage_item_leftovers_unsure_detail, unsureCount, unsureCount), 2)
|
||||
CleanItem.LOGS -> Triple(stringResource(R.string.storage_item_logs), stringResource(R.string.storage_item_logs_detail), 0)
|
||||
CleanItem.UNUSED_CUSTOM -> Triple(custom.joinToString(", ") { it.name }, stringResource(R.string.storage_item_custom_detail), 1)
|
||||
CleanItem.UNUSED_COMPONENTS -> Triple(stringResource(R.string.storage_item_components), unusedLabels.joinToString(", "), 1)
|
||||
CleanItem.SHADERS -> Triple(stringResource(R.string.storage_item_shaders), stringResource(R.string.storage_item_shaders_detail), 2)
|
||||
}
|
||||
val shape = RoundedCornerShape(12.dp)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = modifier.fillMaxWidth().clip(shape)
|
||||
.background(if (hot) pal.signal.copy(alpha = 0.10f) else colors.surfaceVariant.copy(alpha = 0.5f))
|
||||
.glideBorder(hot, shape, pal.signal, pal.line)
|
||||
.hoverable(src).clickable(interactionSource = src, indication = LocalIndication.current, role = Role.Checkbox, onClick = onToggle)
|
||||
.controllerConfirm(onClick = onToggle)
|
||||
.padding(horizontal = 12.dp, vertical = 9.dp),
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.size(22.dp).clip(RoundedCornerShape(6.dp))
|
||||
.background(if (on) pal.signal else Color.Transparent)
|
||||
.border(2.dp, if (on) pal.signal else colors.onSurfaceVariant, RoundedCornerShape(6.dp)),
|
||||
) { if (on) Icon(Icons.Filled.Check, contentDescription = null, tint = Color.White, modifier = Modifier.size(16.dp)) }
|
||||
Column(Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(title, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false))
|
||||
when (tag) {
|
||||
0 -> Pill(stringResource(R.string.storage_tag_safe), pal.good)
|
||||
1 -> Pill(stringResource(R.string.storage_tag_unused), colors.onSurfaceVariant)
|
||||
else -> Pill(stringResource(if (item == CleanItem.SHADERS) R.string.storage_tag_stutter else R.string.storage_tag_saves), AttentionAmber)
|
||||
}
|
||||
}
|
||||
Text(detail, fontSize = 12.sp, color = colors.onSurfaceVariant, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
Text(gb(StorageState.bytes(report, item)), fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SubHeader(title: String, detail: String, onBack: () -> Unit, trailing: @Composable () -> Unit = {}) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 6.dp, bottom = 8.dp).controllerBack(onBack),
|
||||
) {
|
||||
BackLink(stringResource(R.string.storage_back), compact = true, onClick = onBack)
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(title, fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(detail, fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
trailing()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GamesView(report: StorageReport, sessionRunning: Boolean, a: FrontEndActions, onBack: () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
var sort by rememberSaveable { mutableStateOf(0) }
|
||||
var confirm by remember { mutableStateOf<GameUsage?>(null) }
|
||||
val games = when (sort) {
|
||||
1 -> report.games.sortedBy { if (it.lastPlayed == 0L) Long.MIN_VALUE else it.lastPlayed }
|
||||
2 -> report.games.filter { !it.internal }
|
||||
else -> report.games
|
||||
}
|
||||
val max = report.games.maxOfOrNull { it.total }?.coerceAtLeast(1) ?: 1
|
||||
SubHeader(stringResource(R.string.storage_games), stringResource(R.string.storage_games_with_data, gb(report.games.sumOf { it.total })), onBack)
|
||||
Box(Modifier.fillMaxWidth().padding(bottom = 10.dp)) {
|
||||
SheetChips(listOf(stringResource(R.string.storage_sort_largest), stringResource(R.string.storage_sort_least_played), stringResource(R.string.storage_sort_sd)), sort, null) { sort = it }
|
||||
}
|
||||
if (sessionRunning) { Note(stringResource(R.string.storage_session_running)); Spacer(Modifier.height(8.dp)) }
|
||||
Column(Modifier.fillMaxWidth().clip(Shape14).background(colors.surface).border(1.dp, pal.line, Shape14)) {
|
||||
if (games.isEmpty()) Text(stringResource(R.string.storage_no_games), fontSize = 14.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(16.dp))
|
||||
games.forEachIndexed { i, g ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 8.dp),
|
||||
) {
|
||||
Box(Modifier.size(42.dp).clip(RoundedCornerShape(8.dp)).background(artBrush(hueOf(g.name)))) {
|
||||
if (g.art != null) AsyncImage(model = g.art, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.size(42.dp))
|
||||
}
|
||||
Column(Modifier.weight(1.3f)) {
|
||||
Text(g.name, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(
|
||||
listOfNotNull(
|
||||
g.library.takeIf { !g.internal },
|
||||
if (g.lastPlayed > 0) stringResource(R.string.storage_played, DateFormat.getDateInstance(DateFormat.MEDIUM).format(Date(g.lastPlayed * 1000))) else stringResource(R.string.storage_never_played),
|
||||
).joinToString(" · "),
|
||||
fontSize = 12.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Column(Modifier.weight(1f)) {
|
||||
Row(Modifier.fillMaxWidth().height(8.dp).clip(RoundedCornerShape(4.dp)).background(Color(0xFF0A0C0F))) {
|
||||
listOf(g.game to BucketColors.getValue(Bucket.GAMES), g.prefix to BucketColors.getValue(Bucket.GAME_DATA), (g.shaders + g.workshop) to Color(0xFFC5B8FF))
|
||||
.forEach { (v, c) -> if (v > 0) Box(Modifier.weight(v.toFloat() / max).fillMaxHeight().background(c)) }
|
||||
val rest = max - g.total
|
||||
if (rest > 0) Spacer(Modifier.weight(rest.toFloat() / max))
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.storage_game_parts, gb(g.game), gb(g.data)),
|
||||
fontSize = 11.5.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 3.dp), maxLines = 1,
|
||||
)
|
||||
}
|
||||
Text(gb(g.total), fontSize = 16.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, modifier = Modifier.widthIn(min = 72.dp))
|
||||
SecondaryButton(stringResource(R.string.storage_uninstall), compact = true, enabled = !sessionRunning) { confirm = g }
|
||||
}
|
||||
if (i < games.lastIndex) Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line))
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Note(stringResource(R.string.storage_games_note))
|
||||
confirm?.let { g ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { confirm = null },
|
||||
title = { Text(stringResource(R.string.storage_uninstall_title, g.name)) },
|
||||
text = { Text(stringResource(R.string.storage_uninstall_body, gb(g.game)), fontSize = 14.sp) },
|
||||
confirmButton = { FocusText(stringResource(R.string.storage_open_steam), pal.signal) { confirm = null; a.onSteamUrl("steam://uninstall/${g.appId}") } },
|
||||
dismissButton = {
|
||||
val cancel = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { runCatching { cancel.requestFocus() } }
|
||||
FocusText(stringResource(R.string.common_cancel), colors.onBackground, modifier = Modifier.focusRequester(cancel)) { confirm = null }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToolsView(report: StorageReport, sessionRunning: Boolean, onBack: () -> Unit) {
|
||||
val ctx = LocalContext.current
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
var confirm by remember { mutableStateOf<ToolUsage?>(null) }
|
||||
var confirmAll by remember { mutableStateOf(false) }
|
||||
val unused = report.unusedTools
|
||||
val inUse = report.tools.filter { it.state != ToolState.UNUSED }
|
||||
SubHeader(
|
||||
stringResource(R.string.storage_tools),
|
||||
stringResource(R.string.storage_tools_detail, gb(report.tools.sumOf { it.bytes }), gb(unused.sumOf { it.bytes })), onBack,
|
||||
) {
|
||||
if (unused.isNotEmpty()) PrimaryButton(stringResource(R.string.storage_remove_unused, gb(unused.sumOf { it.bytes })), compact = true,
|
||||
enabled = !sessionRunning && !StorageState.cleaning) { confirmAll = true }
|
||||
}
|
||||
if (sessionRunning) { Note(stringResource(R.string.storage_session_running)); Spacer(Modifier.height(4.dp)) }
|
||||
SectionTitle(stringResource(R.string.storage_in_use), null)
|
||||
ToolList(inUse, sessionRunning) { }
|
||||
if (unused.isNotEmpty()) {
|
||||
SectionTitle(stringResource(R.string.storage_not_used), null)
|
||||
ToolList(unused, sessionRunning) { confirm = it }
|
||||
}
|
||||
confirm?.let { t ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { confirm = null },
|
||||
title = { Text(stringResource(R.string.storage_remove_title, t.name)) },
|
||||
text = { Text(toolDetail(t) + "\n\n" + stringResource(R.string.storage_remove_body, gb(t.bytes)), fontSize = 14.sp) },
|
||||
confirmButton = { FocusText(stringResource(R.string.storage_remove), colors.error) { confirm = null; StorageState.removeTool(ctx, t) { } } },
|
||||
dismissButton = {
|
||||
val cancel = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { runCatching { cancel.requestFocus() } }
|
||||
FocusText(stringResource(R.string.common_cancel), colors.onBackground, modifier = Modifier.focusRequester(cancel)) { confirm = null }
|
||||
},
|
||||
)
|
||||
}
|
||||
if (confirmAll) AlertDialog(
|
||||
onDismissRequest = { confirmAll = false },
|
||||
title = { Text(stringResource(R.string.storage_remove_unused, gb(unused.sumOf { it.bytes }))) },
|
||||
text = { Text(unused.joinToString("\n") { "• " + it.name + " " + gb(it.bytes) }, fontSize = 14.sp) },
|
||||
confirmButton = {
|
||||
FocusText(stringResource(R.string.storage_remove), colors.error) {
|
||||
confirmAll = false
|
||||
StorageState.clean(ctx, setOf(CleanItem.OLD_TOOLS, CleanItem.UNUSED_CUSTOM)) { }
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
val cancel = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { runCatching { cancel.requestFocus() } }
|
||||
FocusText(stringResource(R.string.common_cancel), colors.onBackground, modifier = Modifier.focusRequester(cancel)) { confirmAll = false }
|
||||
},
|
||||
)
|
||||
if (StorageState.cleaning) Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.padding(top = 10.dp)) {
|
||||
CircularProgressIndicator(strokeWidth = 2.dp, modifier = Modifier.size(16.dp), color = pal.signal)
|
||||
Text(stringResource(R.string.storage_cleaning), fontSize = 13.sp, color = colors.onBackground)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun toolDetail(t: ToolUsage): String = when (t.reason) {
|
||||
ToolReason.DEFAULT_PROTON -> stringResource(R.string.storage_tool_default_proton)
|
||||
ToolReason.PROTON -> stringResource(R.string.storage_tool_proton)
|
||||
ToolReason.FEX_RUNTIME -> stringResource(R.string.storage_tool_fex_runtime)
|
||||
ToolReason.ARM_RUNTIME -> stringResource(R.string.storage_tool_arm_runtime)
|
||||
ToolReason.REDISTRIBUTABLES -> stringResource(R.string.storage_tool_redist)
|
||||
ToolReason.FEX -> stringResource(R.string.storage_tool_fex)
|
||||
ToolReason.STEAM_TOOL -> stringResource(R.string.storage_tool_steam)
|
||||
ToolReason.OLD_RUNTIME -> stringResource(R.string.storage_tool_old_runtime)
|
||||
ToolReason.X86_PROTON -> stringResource(R.string.storage_tool_x86_proton)
|
||||
ToolReason.CUSTOM_UNKNOWN -> stringResource(R.string.storage_tool_custom_unknown)
|
||||
ToolReason.CUSTOM -> when {
|
||||
t.isDefault -> stringResource(R.string.storage_tool_custom_default)
|
||||
t.games > 0 -> pluralStringResource(R.plurals.storage_tool_custom_games, t.games, t.games)
|
||||
else -> stringResource(R.string.storage_tool_custom_unused)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToolList(tools: List<ToolUsage>, sessionRunning: Boolean, onRemove: (ToolUsage) -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
Column(Modifier.fillMaxWidth().clip(Shape14).background(colors.surface).border(1.dp, pal.line, Shape14)) {
|
||||
tools.forEachIndexed { i, t ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp).padding(horizontal = 14.dp, vertical = 8.dp),
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(t.name, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(toolDetail(t), fontSize = 12.5.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
when (t.state) {
|
||||
ToolState.IN_USE -> Pill(if (t.isDefault) stringResource(R.string.storage_tag_default) else pluralStringResource(R.plurals.storage_tag_games, t.games, t.games), pal.signal)
|
||||
ToolState.REQUIRED -> Pill(stringResource(R.string.storage_tag_required), colors.onSurfaceVariant)
|
||||
ToolState.UNUSED -> Pill(stringResource(R.string.storage_tag_not_used), AttentionAmber)
|
||||
}
|
||||
Text(gb(t.bytes), fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, modifier = Modifier.widthIn(min = 70.dp))
|
||||
if (t.state == ToolState.UNUSED) SecondaryButton(stringResource(R.string.storage_remove), compact = true, enabled = !sessionRunning && !StorageState.cleaning) { onRemove(t) }
|
||||
else Icon(Icons.Outlined.Lock, contentDescription = null, tint = colors.onSurfaceVariant, modifier = Modifier.size(16.dp))
|
||||
}
|
||||
if (i < tools.lastIndex) Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun LowSpaceDialog(free: Long, onFreeUp: () -> Unit, onPlay: () -> Unit, onDismiss: () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val report = StorageState.report
|
||||
val focus = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { runCatching { focus.requestFocus() } }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.storage_low_title, gb(free))) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(stringResource(R.string.storage_low_body), fontSize = 14.sp)
|
||||
if (report != null && StorageState.safeBytes(report) > 0) Text(
|
||||
stringResource(R.string.storage_low_safe, gb(StorageState.safeBytes(report))), fontSize = 13.sp, color = pal.good,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = { FocusText(stringResource(R.string.storage_free_up), pal.signal, modifier = Modifier.focusRequester(focus), onClick = onFreeUp) },
|
||||
dismissButton = { FocusText(stringResource(R.string.storage_play_anyway), colors.onBackground, onClick = onPlay) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionStatus() {
|
||||
val pal = LocalPalette.current
|
||||
val colors = MaterialTheme.colorScheme
|
||||
when {
|
||||
StorageState.cleaning -> Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp).clip(Shape14).background(colors.surface).border(1.dp, pal.line, Shape14).padding(12.dp),
|
||||
) {
|
||||
CircularProgressIndicator(strokeWidth = 2.dp, modifier = Modifier.size(16.dp), color = pal.signal)
|
||||
Text(StorageState.progress ?: stringResource(R.string.storage_cleaning), fontSize = 13.sp, color = colors.onBackground)
|
||||
}
|
||||
StorageState.lastError != null -> { Note(stringResource(R.string.storage_failed, StorageState.lastError ?: "")); Spacer(Modifier.height(8.dp)) }
|
||||
StorageState.lastFreed != null -> { Note(stringResource(R.string.storage_freed, gb(StorageState.lastFreed ?: 0))); Spacer(Modifier.height(8.dp)) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun componentLabel(c: ComponentUsage): String {
|
||||
val name = com.droiddeck.launcher.session.ComponentsManager.LABEL[c.comp] ?: c.comp
|
||||
return if (c.packageFile != null) "$name ${c.version}"
|
||||
else stringResource(R.string.storage_component_original, name, c.protonName ?: c.protonId.orEmpty())
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ListCard(content: @Composable () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
Column(Modifier.fillMaxWidth().clip(Shape14).background(colors.surface).border(1.dp, pal.line, Shape14)) { content() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ItemRow(title: String, detail: String?, bytes: Long, last: Boolean, pill: (@Composable () -> Unit)? = null, action: (@Composable () -> Unit)? = null) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp).padding(horizontal = 14.dp, vertical = 8.dp),
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(title, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
if (detail != null) Text(detail, fontSize = 12.5.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
pill?.invoke()
|
||||
Text(gb(bytes), fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, modifier = Modifier.widthIn(min = 70.dp))
|
||||
action?.invoke()
|
||||
}
|
||||
if (!last) Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConfirmDialog(title: String, body: String, confirm: String, danger: Boolean = true, onConfirm: () -> Unit, onDismiss: () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(title) },
|
||||
text = { Text(body, fontSize = 14.sp) },
|
||||
confirmButton = { FocusText(confirm, if (danger) colors.error else pal.signal) { onDismiss(); onConfirm() } },
|
||||
dismissButton = {
|
||||
val cancel = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { runCatching { cancel.requestFocus() } }
|
||||
FocusText(stringResource(R.string.common_cancel), colors.onBackground, modifier = Modifier.focusRequester(cancel), onClick = onDismiss)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GameDataView(report: StorageReport, sessionRunning: Boolean, onBack: () -> Unit) {
|
||||
val ctx = LocalContext.current
|
||||
val busy = sessionRunning || StorageState.cleaning
|
||||
var confirm by remember { mutableStateOf<com.droiddeck.launcher.storage.Leftover?>(null) }
|
||||
SubHeader(stringResource(R.string.storage_game_data), stringResource(R.string.storage_game_data_hint, gb(report.prefixes), gb(report.workshop), gb(report.shaders)), onBack)
|
||||
ActionStatus()
|
||||
if (sessionRunning) { Note(stringResource(R.string.storage_session_running)); Spacer(Modifier.height(4.dp)) }
|
||||
val games = report.games.filter { it.data >= ONE_MB }.sortedByDescending { it.data }
|
||||
SectionTitle(stringResource(R.string.storage_data_installed), gb(games.sumOf { it.data }))
|
||||
ListCard {
|
||||
if (games.isEmpty()) Text(stringResource(R.string.storage_no_games), fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(16.dp))
|
||||
games.forEachIndexed { i, g ->
|
||||
ItemRow(
|
||||
g.name, stringResource(R.string.storage_data_parts, gb(g.prefix), gb(g.shaders), gb(g.workshop)), g.data, i == games.lastIndex,
|
||||
action = { SecondaryButton(stringResource(R.string.storage_clear_shaders), compact = true, enabled = !busy && g.shaders >= ONE_MB) { StorageState.clearShaders(ctx, g.appId) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
val leftovers = report.leftovers.sortedByDescending { it.bytes }
|
||||
if (!report.leftoversChecked) {
|
||||
SectionTitle(stringResource(R.string.storage_data_leftovers), null)
|
||||
Note(stringResource(R.string.storage_leftovers_unchecked))
|
||||
}
|
||||
if (leftovers.isNotEmpty()) {
|
||||
SectionTitle(stringResource(R.string.storage_data_leftovers), gb(leftovers.sumOf { it.bytes }))
|
||||
val safe = leftovers.filter { it.safe }
|
||||
ListCard {
|
||||
if (safe.isNotEmpty()) ItemRow(
|
||||
pluralStringResource(R.plurals.storage_item_leftovers_detail, safe.size, safe.size), null, safe.sumOf { it.bytes }, leftovers.none { !it.safe },
|
||||
pill = { Pill(stringResource(R.string.storage_tag_safe), LocalPalette.current.good) },
|
||||
action = { SecondaryButton(stringResource(R.string.storage_remove), compact = true, enabled = !busy) { StorageState.clean(ctx, setOf(CleanItem.LEFTOVERS)) { } } },
|
||||
)
|
||||
val unsure = leftovers.filter { !it.safe }
|
||||
unsure.forEachIndexed { i, l ->
|
||||
ItemRow(
|
||||
stringResource(R.string.storage_steam_app, l.appId), stringResource(R.string.storage_leftover_unsure_detail), l.bytes, i == unsure.lastIndex,
|
||||
pill = { Pill(stringResource(R.string.storage_tag_saves), AttentionAmber) },
|
||||
action = { SecondaryButton(stringResource(R.string.storage_remove), compact = true, enabled = !busy) { confirm = l } },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (report.otherPrefixes > 0) {
|
||||
SectionTitle(stringResource(R.string.storage_data_other), null)
|
||||
ListCard { ItemRow(stringResource(R.string.storage_data_other_title), stringResource(R.string.storage_data_other_detail), report.otherPrefixes, true) }
|
||||
}
|
||||
confirm?.let { l ->
|
||||
ConfirmDialog(
|
||||
stringResource(R.string.storage_remove_title, stringResource(R.string.storage_steam_app, l.appId)),
|
||||
stringResource(R.string.storage_leftover_confirm, gb(l.bytes)), stringResource(R.string.storage_remove),
|
||||
onConfirm = { StorageState.removeLeftover(ctx, l.appId) }, onDismiss = { confirm = null },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ComponentsView(report: StorageReport, sessionRunning: Boolean, onBack: () -> Unit) {
|
||||
val ctx = LocalContext.current
|
||||
val pal = LocalPalette.current
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val busy = sessionRunning || StorageState.cleaning
|
||||
var confirm by remember { mutableStateOf<ComponentUsage?>(null) }
|
||||
val unused = report.unusedComponents
|
||||
SubHeader(stringResource(R.string.storage_components),
|
||||
stringResource(R.string.storage_tools_detail, gb(report.components.sumOf { it.bytes }), gb(unused.sumOf { it.bytes })), onBack) {
|
||||
if (unused.isNotEmpty()) PrimaryButton(stringResource(R.string.storage_remove_unused, gb(unused.sumOf { it.bytes })), compact = true, enabled = !busy) {
|
||||
StorageState.clean(ctx, setOf(CleanItem.UNUSED_COMPONENTS)) { }
|
||||
}
|
||||
}
|
||||
ActionStatus()
|
||||
if (sessionRunning) { Note(stringResource(R.string.storage_session_running)); Spacer(Modifier.height(4.dp)) }
|
||||
val packages = report.components.filter { it.packageFile != null }
|
||||
val originals = report.components.filter { it.packageFile == null }
|
||||
SectionTitle(stringResource(R.string.storage_components_packages), null)
|
||||
ListCard {
|
||||
if (packages.isEmpty()) Text(stringResource(R.string.storage_components_none), fontSize = 14.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(16.dp))
|
||||
packages.forEachIndexed { i, c ->
|
||||
ItemRow(componentLabel(c), stringResource(if (c.inUse) R.string.storage_component_in_use else R.string.storage_component_unused), c.bytes, i == packages.lastIndex,
|
||||
pill = { Pill(stringResource(if (c.inUse) R.string.storage_tag_in_use else R.string.storage_tag_not_used), if (c.inUse) pal.signal else AttentionAmber) },
|
||||
action = { if (!c.inUse) SecondaryButton(stringResource(R.string.storage_remove), compact = true, enabled = !busy) { confirm = c } })
|
||||
}
|
||||
}
|
||||
if (originals.isNotEmpty()) {
|
||||
SectionTitle(stringResource(R.string.storage_components_originals), null)
|
||||
ListCard {
|
||||
originals.forEachIndexed { i, c ->
|
||||
ItemRow(componentLabel(c), c.version + " · " + stringResource(if (c.inUse) R.string.storage_original_kept else R.string.storage_original_old), c.bytes, i == originals.lastIndex,
|
||||
pill = { Pill(stringResource(if (c.inUse) R.string.storage_tag_required else R.string.storage_tag_not_used), if (c.inUse) colors.onSurfaceVariant else AttentionAmber) },
|
||||
action = { if (!c.inUse) SecondaryButton(stringResource(R.string.storage_remove), compact = true, enabled = !busy) { confirm = c } })
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Note(stringResource(R.string.storage_components_note))
|
||||
confirm?.let { c ->
|
||||
ConfirmDialog(stringResource(R.string.storage_remove_title, componentLabel(c)),
|
||||
stringResource(if (c.packageFile != null) R.string.storage_component_confirm else R.string.storage_original_confirm, gb(c.bytes)),
|
||||
stringResource(R.string.storage_remove), onConfirm = { StorageState.removeComponent(ctx, c) }, onDismiss = { confirm = null })
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DesktopView(report: StorageReport, sessionRunning: Boolean, onBack: () -> Unit) {
|
||||
val ctx = LocalContext.current
|
||||
val busy = sessionRunning || StorageState.cleaning
|
||||
var confirm by remember { mutableStateOf<com.droiddeck.launcher.storage.FlatpakUsage?>(null) }
|
||||
var confirmPrune by remember { mutableStateOf(false) }
|
||||
SubHeader(stringResource(R.string.storage_desktop), stringResource(R.string.storage_desktop_detail, gb(report.flatpak)), onBack) {
|
||||
PrimaryButton(stringResource(R.string.storage_desktop_prune), compact = true, enabled = !busy && report.flatpakRuntimes + report.flatpakCache >= ONE_MB) { confirmPrune = true }
|
||||
}
|
||||
ActionStatus()
|
||||
if (sessionRunning) { Note(stringResource(R.string.storage_session_running)); Spacer(Modifier.height(4.dp)) }
|
||||
SectionTitle(stringResource(R.string.storage_desktop_apps_title), null)
|
||||
ListCard {
|
||||
if (report.flatpakApps.isEmpty()) Text(stringResource(R.string.storage_desktop_none), fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(16.dp))
|
||||
report.flatpakApps.forEachIndexed { i, app ->
|
||||
ItemRow(app.name, app.id, app.bytes, i == report.flatpakApps.lastIndex,
|
||||
action = { SecondaryButton(stringResource(R.string.storage_uninstall), compact = true, enabled = !busy) { confirm = app } })
|
||||
}
|
||||
}
|
||||
SectionTitle(stringResource(R.string.storage_desktop_shared_title), null)
|
||||
ListCard {
|
||||
ItemRow(stringResource(R.string.storage_desktop_runtimes), stringResource(R.string.storage_desktop_runtimes_detail), report.flatpakRuntimes, false)
|
||||
ItemRow(stringResource(R.string.storage_desktop_cache), stringResource(R.string.storage_desktop_cache_detail), report.flatpakCache, true)
|
||||
}
|
||||
confirm?.let { app ->
|
||||
ConfirmDialog(stringResource(R.string.storage_uninstall_title, app.name), stringResource(R.string.storage_flatpak_uninstall_body, gb(app.bytes)),
|
||||
stringResource(R.string.storage_uninstall), onConfirm = { StorageState.uninstallFlatpak(ctx, app.id) }, onDismiss = { confirm = null })
|
||||
}
|
||||
if (confirmPrune) ConfirmDialog(stringResource(R.string.storage_desktop_prune), stringResource(R.string.storage_desktop_prune_body),
|
||||
stringResource(R.string.storage_desktop_prune), danger = false, onConfirm = { StorageState.pruneFlatpak(ctx) }, onDismiss = { confirmPrune = false })
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.StrokeJoin
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.addPathNodes
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
internal val VrHeadsetIcon: ImageVector = ImageVector.Builder("VrHeadset", 24.dp, 24.dp, 24f, 24f).apply {
|
||||
for (d in listOf(
|
||||
"M5,8H19A3,3 0 0 1 22,11V15A3,3 0 0 1 19,18H15.6L14.1,15.6A2.4,2.4 0 0 0 9.9,15.6L8.4,18H5A3,3 0 0 1 2,15V11A3,3 0 0 1 5,8Z",
|
||||
"M7,8V6.5A1.5,1.5 0 0 1 8.5,5H15.5A1.5,1.5 0 0 1 17,6.5V8",
|
||||
)) addPath(addPathNodes(d), stroke = SolidColor(Color.Black), strokeLineWidth = 1.8f,
|
||||
strokeLineCap = StrokeCap.Round, strokeLineJoin = StrokeJoin.Round)
|
||||
}.build()
|
||||
@@ -3,8 +3,10 @@ package com.droiddeck.launcher.update
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.BuildConfig
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.core.DownloadSource
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
@@ -13,6 +15,7 @@ import java.net.URL
|
||||
import java.security.MessageDigest
|
||||
|
||||
object AppUpdates {
|
||||
private const val TAG = "AppUpdates"
|
||||
private const val REPO = "Droid-Deck/DroidDeck"
|
||||
private const val CI_REPO = "Droid-Deck/DroidDeck-CI"
|
||||
private const val CATALOG_URL = "https://raw.githubusercontent.com/$CI_REPO/main/catalog.json"
|
||||
@@ -353,7 +356,24 @@ object AppUpdates {
|
||||
}
|
||||
|
||||
private fun get(context: Context, url: String): String {
|
||||
val c = URL(url).openConnection() as HttpURLConnection
|
||||
val sources = DownloadSource.candidates(url)
|
||||
var last: IOException? = null
|
||||
for ((index, source) in sources.withIndex()) {
|
||||
try {
|
||||
val body = get(context, source, url)
|
||||
DownloadSource.succeeded(source)
|
||||
return body
|
||||
} catch (e: IOException) {
|
||||
last = e
|
||||
if (index < sources.lastIndex) Log.i(TAG, "retrying $url via $source")
|
||||
}
|
||||
}
|
||||
DownloadSource.failed()
|
||||
throw last ?: IOException(context.getString(R.string.appupd_http, 0))
|
||||
}
|
||||
|
||||
private fun get(context: Context, source: String, url: String): String {
|
||||
val c = URL(source).openConnection() as HttpURLConnection
|
||||
c.connectTimeout = 15_000
|
||||
c.readTimeout = 20_000
|
||||
c.setRequestProperty("Accept", "application/json")
|
||||
|
||||
@@ -10,13 +10,11 @@ import android.os.Build
|
||||
import android.provider.Settings
|
||||
import com.droiddeck.launcher.BuildConfig
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.core.Downloader
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import com.droiddeck.launcher.core.Hashes
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.security.MessageDigest
|
||||
|
||||
object SelfInstaller {
|
||||
@@ -30,32 +28,12 @@ object SelfInstaller {
|
||||
|
||||
fun download(context: Context, apk: AppUpdates.Apk, progress: (Int) -> Unit): File {
|
||||
val dir = File(context.cacheDir, "updates")
|
||||
FileUtils.delete(dir)
|
||||
dir.listFiles()?.forEach { if (it.name != apk.name) FileUtils.delete(it) }
|
||||
dir.mkdirs()
|
||||
val target = File(dir, apk.name)
|
||||
val c = URL(apk.url).openConnection() as HttpURLConnection
|
||||
c.connectTimeout = 15_000
|
||||
c.readTimeout = 60_000
|
||||
c.setRequestProperty("User-Agent", "DroidDeck-app")
|
||||
c.instanceFollowRedirects = true
|
||||
val ok = Downloader.downloadFile(apk.url, target, true, apk.sha256) { f -> progress(if (f < 0) -1 else (f * 100).toInt()) }
|
||||
if (!ok) throw IOException(context.getString(R.string.selfinst_download_failed))
|
||||
try {
|
||||
if (c.responseCode != 200) throw IOException(context.getString(R.string.selfinst_http, c.responseCode))
|
||||
val total = c.contentLengthLong.takeIf { it > 0 } ?: apk.size
|
||||
c.inputStream.use { input ->
|
||||
FileOutputStream(target).use { out ->
|
||||
val buf = ByteArray(1 shl 16)
|
||||
var done = 0L
|
||||
var last = -1
|
||||
while (true) {
|
||||
val r = input.read(buf)
|
||||
if (r <= 0) break
|
||||
out.write(buf, 0, r)
|
||||
done += r
|
||||
val pct = if (total > 0) (done * 100 / total).toInt() else -1
|
||||
if (pct != last) { last = pct; progress(pct) }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (apk.size > 0 && target.length() != apk.size) {
|
||||
throw IOException(context.getString(R.string.selfinst_size_mismatch))
|
||||
}
|
||||
@@ -63,13 +41,11 @@ object SelfInstaller {
|
||||
throw IOException(context.getString(R.string.selfinst_checksum_mismatch))
|
||||
}
|
||||
validateDownloaded(context, target, apk)
|
||||
return target
|
||||
} catch (e: Exception) {
|
||||
FileUtils.delete(target)
|
||||
throw e
|
||||
} finally {
|
||||
c.disconnect()
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
private fun validateDownloaded(context: Context, file: File, apk: AppUpdates.Apk) {
|
||||
|
||||
@@ -7,6 +7,11 @@ import com.droiddeck.launcher.session.SessionState
|
||||
object CompositorHost {
|
||||
@Volatile
|
||||
private var started = false
|
||||
|
||||
@Volatile
|
||||
var displayChoice: String? = null
|
||||
|
||||
@Volatile
|
||||
private var vsyncRunning = false
|
||||
|
||||
private val vsync = object : Choreographer.FrameCallback {
|
||||
@@ -37,6 +42,7 @@ object CompositorHost {
|
||||
return false
|
||||
}
|
||||
WaylandCompositor.nativeSetOutputRefreshRate(refreshHz)
|
||||
com.droiddeck.launcher.gpu.TpUbwcHint.applyToProcess()
|
||||
WaylandCompositor.nativeStartWithSurface(
|
||||
surface, xdgRuntimeDir, driverPath, libraryName, nativeLibDir,
|
||||
)
|
||||
@@ -71,6 +77,14 @@ object CompositorHost {
|
||||
|
||||
private var attached: Surface? = null
|
||||
|
||||
@Synchronized
|
||||
fun attachXr() {
|
||||
check(started) { "DroidDeck compositor is not running" }
|
||||
pauseVsync()
|
||||
attached = null
|
||||
WaylandCompositor.nativeSetSurface(null)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun detach(surface: Surface?) {
|
||||
if (!started) return
|
||||
|
||||
@@ -45,6 +45,10 @@ public final class WaylandCompositor {
|
||||
|
||||
private static volatile long lastFrameNanos;
|
||||
private static volatile long frameIntervalNanos;
|
||||
private static volatile long gameFrames; // written by the compositor thread only
|
||||
|
||||
/** Frames the game has presented since the app started; the rate is the difference over time. */
|
||||
public static long gameFrameCount() { return gameFrames; }
|
||||
|
||||
public static long recentFrameIntervalMs() {
|
||||
long last = lastFrameNanos;
|
||||
@@ -57,6 +61,7 @@ public final class WaylandCompositor {
|
||||
long now = System.nanoTime();
|
||||
long previous = lastFrameNanos;
|
||||
lastFrameNanos = now;
|
||||
gameFrames++;
|
||||
if (previous != 0) {
|
||||
long interval = now - previous;
|
||||
long smoothed = frameIntervalNanos;
|
||||
@@ -148,6 +153,7 @@ public final class WaylandCompositor {
|
||||
String nativeLibDir);
|
||||
|
||||
public static native void nativeSetSurface(Surface surface);
|
||||
public static native boolean nativeWaitSurface(int timeoutMs);
|
||||
|
||||
public static native void nativeSendPointer(int action, int x, int y);
|
||||
|
||||
|
||||
@@ -278,6 +278,9 @@
|
||||
<string name="mode_suspend_steam_note">«تلقائي» يوقف مؤقتًا ويستأنف عند الظهور. «أصلي» يجهّز Steam للسكون أولًا. «يدوي» ينتظر الضغط على «استئناف». «أبدًا» يُبقيه قيد التشغيل.</string>
|
||||
<string name="mode_background_downloads">مواصلة التنزيلات</string>
|
||||
<string name="mode_background_downloads_hint">تأخير التعليق في الخلفية أثناء قيام Steam بالتنزيل.</string>
|
||||
<string name="mode_zram">ضغط zRAM</string>
|
||||
<string name="mode_zram_hint">أثناء تشغيل اللعبة يتم ضغط ذاكرة عميل Steam إلى zram في الجهاز، فتحصل اللعبة على ذاكرة RAM أكبر. يسري عند تشغيل Steam في المرة القادمة.</string>
|
||||
<string name="mode_zram_unsupported">غير متاح: لا يحتوي هذا الجهاز على zram أو swap، أو أن النواة أقدم من 5.4.</string>
|
||||
<string name="mode_suspend_note">«تلقائي» يوقف مؤقتًا في الخلفية ويستأنف عند الظهور. «يدوي» ينتظر الضغط على «استئناف». «أبدًا» يُبقيه قيد التشغيل.</string>
|
||||
<string name="mode_startup">بدء التشغيل</string>
|
||||
<string name="mode_steam_startup">تشغيل Steam مع DroidDeck</string>
|
||||
@@ -583,6 +586,10 @@
|
||||
<string name="drawer_cas">الحدة (CAS)</string>
|
||||
<string name="drawer_cas_level">مستوى الحدة</string>
|
||||
<string name="drawer_fake_hdr">HDR مزيّف</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">تشغيل SteamVR</string>
|
||||
<string name="steamvr_checking">جارٍ التحقق من SteamVR…</string>
|
||||
<string name="drawer_deband">إزالة التدرّج اللوني</string>
|
||||
<string name="drawer_deband_strength">قوة إزالة التدرّج اللوني</string>
|
||||
<string name="drawer_brightness">السطوع</string>
|
||||
@@ -600,6 +607,11 @@
|
||||
<string name="drawer_texture_sharpness_note">انحياز mip سالب: ملمس أكثر حدة مع بعض التلألؤ. يطابق الخيار «تلقائي» مقدار تكبير الجلسة إلى الشاشة.</string>
|
||||
<string name="drawer_touch_auto">تلقائي (%1$s)</string>
|
||||
<string name="drawer_keyboard">لوحة المفاتيح</string>
|
||||
<string name="drawer_latency">زمن الاستجابة</string>
|
||||
<string name="drawer_low_latency_controller">وحدة تحكم بزمن استجابة منخفض</string>
|
||||
<string name="drawer_low_latency_controller_hint">تبلغ وحدة التحكم بمعدل 1000 هرتز بدلًا من 250 هرتز. استهلاك معالج أعلى قليلًا.</string>
|
||||
<string name="drawer_low_latency_touch">لمس بزمن استجابة منخفض</string>
|
||||
<string name="drawer_low_latency_touch_hint">تتجاوز اللمسات تجميع الإطارات، فيعمل اللمس في Steam بمعدل 1000 هرتز. استهلاك معالج أعلى قليلًا.</string>
|
||||
<string name="drawer_pc_keyboard">لوحة مفاتيح الكمبيوتر</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">الشاشة الثانية</string>
|
||||
@@ -695,6 +707,14 @@
|
||||
<string name="setup_saved_logs">سجلات الجلسة المحفوظة</string>
|
||||
<string name="setup_saved_logs_hint">يحتفظ بأحدث %1$d</string>
|
||||
<string name="setup_clear_logs">مسح السجلات</string>
|
||||
<string name="setup_reset_settings">إعادة تعيين كل الإعدادات</string>
|
||||
<string name="setup_reset_settings_hint">إرجاع كل إعداد إلى قيمته الافتراضية</string>
|
||||
<string name="setup_reset_settings_button">إعادة تعيين</string>
|
||||
<string name="setup_reset_settings_title">إعادة تعيين كل الإعدادات؟</string>
|
||||
<string name="setup_reset_settings_body">تعود إعدادات الصورة والأداء ووحدة التحكم والمظهر واللغة والمشغّل إلى الافتراضي. تبقى ألعابك وتسجيل الدخول إلى Steam وحسابات المتاجر وبرامج التشغيل المنزّلة ومجلدات التخزين.</string>
|
||||
<string name="setup_reset_settings_profiles">إعادة تعيين ملفات تعريف الألعاب أيضًا (Proton وFEX والمكوّنات وبيئة التشغيل لكل لعبة، وبيئة ملف التعريف الافتراضي)</string>
|
||||
<string name="setup_reset_settings_session_running">أوقف الجلسة قبل إعادة تعيين الإعدادات.</string>
|
||||
<string name="setup_reset_settings_partial">أُعيد تعيين الإعدادات، لكن ليس كلها. التفاصيل في السجلات.</string>
|
||||
<string name="setup_offline">وضع عدم الاتصال</string>
|
||||
<string name="setup_sign_in_first">سجّل الدخول إلى Steam أولًا</string>
|
||||
<string name="setup_launcher">المشغّل</string>
|
||||
@@ -867,7 +887,7 @@
|
||||
<string name="gate_eyebrow">Steam</string>
|
||||
<string name="gate_lede">يوقف Android التطبيقات التي تبدأ عمليات خلفية كثيرة، وSteam يبدأ عشرات منها. وإلى أن يتغيّر هذا، تُغلَق الألعاب دون أي خطأ.</string>
|
||||
<string name="gate_fix_for_me">إصلاحه لي</string>
|
||||
<string name="gate_toggle_steps">فعِّل «تعطيل تقييد العمليات الفرعية»، ثم عُد. اترك خيارات المطوّرين مفعّلة، وإلا سيُعاد تعطيل المفتاح.</string>
|
||||
<string name="gate_toggle_steps">فعِّل «إيقاف القيود المفروضة على العمليات الثانوية»، ثم عُد. اترك خيارات المطوّرين مفعّلة، وإلا سيُعاد تعطيل المفتاح.</string>
|
||||
<string name="gate_open_dev">فتح خيارات المطوّرين</string>
|
||||
<string name="gate_no_toggle">لا يحتوي إصدار Android هذا على مفتاح لذلك، لذا يغيّره DroidDeck عبر تصحيح الأخطاء اللاسلكي. يستغرق نحو دقيقة، ولا يحتاج إلى جهاز كمبيوتر.</string>
|
||||
<string name="gate_needs_wifi">يحتاج تصحيح الأخطاء اللاسلكي إلى Wi-Fi. تعمل أي شبكة؛ فهو لا يحتاج إلى اتصال بالإنترنت.</string>
|
||||
@@ -1033,7 +1053,6 @@
|
||||
<string name="gpu_set_with_runtime">يُضبَط مع برنامج تشغيل بيئة التشغيل معًا: يحمل %1$s كلاهما.</string>
|
||||
<string name="gpu_runtime_hint">يُستخدَم في الألعاب وسطح المكتب.</string>
|
||||
<string name="gpu_runtime_default">الافتراضي لبيئة التشغيل</string>
|
||||
<string name="gpu_display_hint">يرسم الإطارات على الشاشة. يُطبَّق بعد إعادة تشغيل DroidDeck.</string>
|
||||
<string name="gpu_auto_by_gpu">تلقائي - يُحدَّد بحسب GPU</string>
|
||||
<string name="gpu_manage">تنزيل أو استيراد أو حذف…</string>
|
||||
<string name="gpu_in_use_caps">قيد الاستخدام</string>
|
||||
@@ -1279,10 +1298,16 @@
|
||||
<string name="drivers_downloading_pair">جارٍ تنزيل %1$s %2$s…</string>
|
||||
<string name="drivers_updated_to">تم التحديث إلى %1$s %2$s.</string>
|
||||
<string name="drivers_updated_to_restart">تم التحديث إلى %1$s %2$s. يُطبَّق برنامج تشغيل العرض بعد إعادة تشغيل DroidDeck.</string>
|
||||
<string name="drivers_auto_waits_for_session">تنتظر تحديثات برامج التشغيل توقف الجلسة.</string>
|
||||
<string name="drivers_auto_waits_for_network">سيتم تنزيل %1$s %2$s بعد إيقاف بيانات الجوال وتوفير البطارية.</string>
|
||||
<string name="drivers_skipping_bad">لم يبدأ %1$s %2$s أي جلسة هنا، لذا يبقى الوضع التلقائي على برامج التشغيل التي بدأت.</string>
|
||||
<string name="drivers_rolled_back">فشل %1$s في بدء الجلسة مرتين، لذا رجع الوضع التلقائي إلى برامج التشغيل التي عملت.</string>
|
||||
<string name="drivers_switched_to">تم التبديل إلى %1$s %2$s.</string>
|
||||
<string name="drivers_switched_to_restart">تم التبديل إلى %1$s %2$s. يُطبَّق برنامج تشغيل العرض بعد إعادة تشغيل DroidDeck.</string>
|
||||
<string name="drivers_using">يتم استخدام %1$s %2$s.</string>
|
||||
<string name="drivers_using_restart">يتم استخدام %1$s %2$s. يُطبَّق برنامج تشغيل العرض بعد إعادة تشغيل DroidDeck.</string>
|
||||
<string name="drivers_using_quest">يُستخدم برنامج تشغيل الصورة المرفق مع Quest وبيئة التشغيل الافتراضية.</string>
|
||||
<string name="drivers_using_quest_restart">يُستخدم برنامج تشغيل الصورة المرفق مع Quest وبيئة التشغيل الافتراضية. يسري برنامج تشغيل الصورة بعد إعادة تشغيل DroidDeck.</string>
|
||||
<string name="drivers_not_a_driver">ليس ملف zip لبرنامج تشغيل: ليس برنامج تشغيل AdrenoTools ولا Turnip بلاحقة -Linux ولا حزمة Android + Linux</string>
|
||||
<string name="drivers_import_failed">فشل الاستيراد: %1$s</string>
|
||||
<string name="drivers_imported_unnamed">برنامج تشغيل مستورَد</string>
|
||||
@@ -1748,7 +1773,7 @@
|
||||
<string name="adbpair_done_text">اضغط للعودة إلى DroidDeck. يمكنك إيقاف تصحيح الأخطاء اللاسلكي الآن.</string>
|
||||
<string name="adbpair_title_failed">توقَّف إعداد تصحيح الأخطاء اللاسلكي</string>
|
||||
|
||||
<string name="selfinst_http">استجاب التنزيل بـ HTTP %1$d</string>
|
||||
<string name="selfinst_download_failed">فشل التنزيل</string>
|
||||
<string name="selfinst_size_mismatch">حجم التنزيل غير مطابق لملف APK المنشور، وتم تجاهله - حاوِل مرة أخرى</string>
|
||||
<string name="selfinst_checksum_mismatch">التنزيل غير مطابق لمجموعه الاختباري، وتم تجاهله - حاوِل مرة أخرى</string>
|
||||
<string name="selfinst_wrong_package">التحديث مخصَّص لحزمة Android %1$s، لا %2$s</string>
|
||||
@@ -1846,7 +1871,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">تم الوصول إلى حدّ الطلبات في GitHub - حاوِل مرة أخرى لاحقًا</string>
|
||||
<string name="turnip_github_http">استجاب GitHub بـ HTTP %1$d</string>
|
||||
<string name="turnip_download_http">استجاب التنزيل بـ HTTP %1$d</string>
|
||||
<string name="turnip_download_failed">فشل التنزيل</string>
|
||||
<string name="turnip_list_predates_checksums">قائمة برامج التشغيل هذه أقدم من إضافة المجاميع الاختبارية - حدِّثها وحاوِل مرة أخرى</string>
|
||||
<string name="turnip_checksum_mismatch">عدم تطابق المجموع الاختباري - تم تجاهل التنزيل</string>
|
||||
<string name="turnip_not_adrenotools">ليس برنامج تشغيل AdrenoTools: %1$s</string>
|
||||
@@ -1883,7 +1908,7 @@
|
||||
<string name="phantom_instructions_fix">%1$s إذا لم يكن ذلك ممكنًا، وصِّل هذا الجهاز بكمبيوتر يستخدم ADB ونفِّذ الأمر أدناه. عُد إلى هنا وتحقَّق مرة أخرى.</string>
|
||||
<string name="phantom_instructions_off">يمكن بدء جلسات Steam.</string>
|
||||
<string name="phantom_instructions_not_applicable">لا يستخدم إصدار Android هذا هذا الحدّ.</string>
|
||||
<string name="phantom_fix_toggle">فعِّل «تعطيل قيود العمليات الفرعية» في خيارات المطوّرين، واترك خيارات المطوّرين مفعَّلة بعد ذلك.</string>
|
||||
<string name="phantom_fix_toggle">فعِّل «إيقاف القيود المفروضة على العمليات الثانوية» في خيارات المطوّرين، واترك خيارات المطوّرين مفعَّلة بعد ذلك.</string>
|
||||
<string name="phantom_fix_no_toggle">لا يحتوي إصدار Android هذا على مفتاح لذلك. استخدِم تصحيح الأخطاء اللاسلكي لتغييره من هذا الجهاز.</string>
|
||||
<string name="upd_block_no_apk">لا يتوفّر تنزيل لهذا الإصدار الخاص بهذه النسخة من DroidDeck.</string>
|
||||
<string name="upd_block_version_code">لن يثبِّته Android فوق هذا الإصدار لأن versionCode %1$d أقل من %2$d المثبَّت.</string>
|
||||
@@ -2115,6 +2140,9 @@
|
||||
<string name="stores_install_title">تثبيت %1$s:</string>
|
||||
<string name="stores_target_internal">التخزين الداخلي</string>
|
||||
<string name="stores_target_free">%1$s متاحة</string>
|
||||
<string name="stores_target_free_unknown">المساحة الحرة غير معروفة</string>
|
||||
<string name="stores_storage_insufficient">المساحة الحرة غير كافية: يلزم %1$s والمتوفر %2$s فقط</string>
|
||||
<string name="stores_storage_low">المساحة غير كافية (%1$s متوفرة، ويلزم %2$s)</string>
|
||||
<string name="stores_notification_channel">تنزيلات المتجر</string>
|
||||
<string name="stores_notification_channel_hint">يعرض تقدّم تنزيل الألعاب ويحافظ على استمرار التنزيلات في الخلفية</string>
|
||||
<string name="stores_notification_one">جارٍ التنزيل</string>
|
||||
@@ -2150,6 +2178,21 @@
|
||||
<string name="edit_other_exe">ملف exe آخر…</string>
|
||||
<string name="edit_remove">إزالة</string>
|
||||
<string name="art_cover">الغلاف</string>
|
||||
<string name="art_wide_cover">غلاف عريض</string>
|
||||
<string name="art_adjust_logo">ضبط الشعار</string>
|
||||
<string name="logo_pos_default">افتراضي</string>
|
||||
<string name="logo_pos_upper_left">أعلى اليسار</string>
|
||||
<string name="logo_pos_upper_center">أعلى الوسط</string>
|
||||
<string name="logo_pos_center">الوسط</string>
|
||||
<string name="logo_pos_bottom_left">أسفل اليسار</string>
|
||||
<string name="logo_pos_bottom_center">أسفل الوسط</string>
|
||||
<string name="logo_pos_position">الموضع</string>
|
||||
<string name="logo_pos_size">الحجم</string>
|
||||
<string name="logo_pos_save">حفظ</string>
|
||||
<string name="logo_pos_reset">العودة إلى الافتراضي</string>
|
||||
<string name="logo_pos_hint">يُرسم فوق خلفية اللعبة كما يفعل Steam. قد يختلف الموضع الدقيق ببضع بكسلات.</string>
|
||||
<string name="logo_pos_no_logo">لا توجد لهذه اللعبة شعار بعد. اختر واحدًا من الشعار لتراه هنا.</string>
|
||||
<string name="logo_pos_failed">تعذّر حفظ موضع الشعار. حاول مرة أخرى.</string>
|
||||
<string name="art_background">الخلفية</string>
|
||||
<string name="art_logo">الشعار</string>
|
||||
<string name="art_icon">الأيقونة</string>
|
||||
@@ -2162,6 +2205,17 @@
|
||||
<string name="art_pick_image">اختيار صورة…</string>
|
||||
<string name="art_pick_title">اختيار صورة: %1$s</string>
|
||||
<string name="art_reset">إعادة الضبط إلى تلقائي</string>
|
||||
<string name="art_search_sgdb">البحث في SteamGridDB…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB: %1$s</string>
|
||||
<string name="art_sgdb_search_label">البحث عن لعبة</string>
|
||||
<string name="art_sgdb_other_games">نتائج أخرى</string>
|
||||
<string name="art_sgdb_none">لا نتائج.</string>
|
||||
<string name="art_sgdb_no_key">يحتاج SteamGridDB إلى مفتاح API. أضف مفتاحك من الإعدادات › المشغّل.</string>
|
||||
<string name="art_sgdb_bad_key">لم يقبل SteamGridDB مفتاح API. تحقق منه من الإعدادات › المشغّل.</string>
|
||||
<string name="art_sgdb_limit">يحدّ SteamGridDB من عدد الطلبات الآن. حاول بعد دقيقة.</string>
|
||||
<string name="art_sgdb_offline">تعذّر الوصول إلى SteamGridDB. تحقق من الاتصال.</string>
|
||||
<string name="art_sgdb_server">حدث خطأ في SteamGridDB. حاول لاحقًا.</string>
|
||||
<string name="art_sgdb_no_game">لا نتائج لهذه اللعبة في SteamGridDB. جرّب البحث عنها.</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="zero">لم تتم إضافة أي لعبة</item>
|
||||
<item quantity="one">تمت إضافة لعبة واحدة</item>
|
||||
@@ -2197,6 +2251,45 @@
|
||||
<string name="gpu_bar_auto">تلقائي · %1$s</string>
|
||||
<string name="gpu_separately">برنامجا تشغيل بيئة التشغيل والعرض بشكل منفصل</string>
|
||||
<string name="gpu_use_title">استخدام %1$s؟</string>
|
||||
<string name="gpu_mode_title">كيف تُختار برامج التشغيل</string>
|
||||
<string name="gpu_mode_auto_detail">أفضل زوج لهذا المعالج الرسومي، محدَّث دائمًا</string>
|
||||
<string name="gpu_mode_pair">زوج متوافق</string>
|
||||
<string name="gpu_mode_pair_detail">اختر زوجًا يضبط برنامجي التشغيل</string>
|
||||
<string name="gpu_mode_mix">خلط حر</string>
|
||||
<string name="gpu_mode_mix_detail">اختر الصورة وبيئة التشغيل بشكل منفصل</string>
|
||||
<string name="gpu_mode_pair_short">زوج</string>
|
||||
<string name="gpu_mode_mix_short">مختلط</string>
|
||||
<string name="gpu_src_auto">تلقائي</string>
|
||||
<string name="gpu_src_pair">زوج</string>
|
||||
<string name="gpu_src_bundle">حزمة</string>
|
||||
<string name="gpu_src_chosen">مختار</string>
|
||||
<string name="gpu_src_by_gpu">حسب المعالج</string>
|
||||
<string name="gpu_src_default">افتراضي</string>
|
||||
<string name="gpu_display_live">يرسم الآن</string>
|
||||
<string name="gpu_display_pending">يلزم إعادة تشغيل · يرسم %1$s حتى ذلك الحين</string>
|
||||
<string name="gpu_runtime_live">نشط</string>
|
||||
<string name="gpu_runtime_pending">الجلسة القادمة · هذه الجلسة تبقي %1$s</string>
|
||||
<string name="gpu_display_next">يرسم الشاشة. يسري من الجلسة القادمة.</string>
|
||||
<string name="gpu_previous_driver">برنامج التشغيل السابق</string>
|
||||
<string name="gpu_restart_banner">أعد التشغيل لتغيير برنامج تشغيل الصورة.</string>
|
||||
<string name="gpu_restart_blocked">أوقف الجلسة الحالية وأعد تشغيل DroidDeck لتغيير برنامج تشغيل الصورة.</string>
|
||||
<string name="gpu_restart_now">إعادة التشغيل الآن</string>
|
||||
<string name="gpu_restart_later">لاحقًا</string>
|
||||
<string name="gpu_auto_title">اختار الوضع التلقائي هذا الزوج لـ %1$s</string>
|
||||
<string name="gpu_auto_builtin_title">يستخدم الوضع التلقائي برامج التشغيل المدمجة لـ %1$s</string>
|
||||
<string name="gpu_auto_reason">مختار لعائلة %1$s. تُثبَّت الإصدارات الجديدة في الخلفية.</string>
|
||||
<string name="gpu_pair_title">اختر زوجًا</string>
|
||||
<string name="gpu_pair_hint">الزوج يضبط برنامج تشغيل الصورة وبرنامج تشغيل بيئة التشغيل معًا.</string>
|
||||
<string name="gpu_mix_title">اختر كل برنامج تشغيل</string>
|
||||
<string name="gpu_mix_hint">الحزمة تملأ الموضعين. تسري تغييرات الصورة بعد إعادة التشغيل، وتغييرات بيئة التشغيل من الجلسة القادمة.</string>
|
||||
<string name="gpu_change_display">تغيير برنامج تشغيل الصورة</string>
|
||||
<string name="gpu_change_runtime">تغيير برنامج تشغيل بيئة التشغيل</string>
|
||||
<string name="gpu_autos_pick">اختيار الوضع التلقائي</string>
|
||||
<string name="gpu_installed">مثبَّت</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · أعد التشغيل للتطبيق</string>
|
||||
<string name="gpu_chip_restart">%1$s · يلزم إعادة تشغيل</string>
|
||||
<string name="comp_this_device">هذا الجهاز</string>
|
||||
<string name="proton_remove_title">إزالة %1$s؟</string>
|
||||
<string name="setup_tab_diagnostics">التشخيص</string>
|
||||
<string name="setup_ready">جاهز للّعب</string>
|
||||
@@ -2208,6 +2301,14 @@
|
||||
<string name="setup_group_look">المظهر</string>
|
||||
<string name="setup_group_device">هذا الجهاز</string>
|
||||
<string name="setup_group_art">صور المكتبة الفنية</string>
|
||||
<string name="setup_group_download">التنزيل</string>
|
||||
<string name="setup_download_source">المصدر</string>
|
||||
<string name="setup_download_source_auto">تلقائي</string>
|
||||
<string name="setup_download_source_direct">مباشر</string>
|
||||
<string name="setup_download_source_mirror">مرآة</string>
|
||||
<string name="setup_download_source_custom">بادئة خاصة</string>
|
||||
<string name="setup_download_mirror">الموقع</string>
|
||||
<string name="setup_download_prefix">البادئة</string>
|
||||
<string name="flathub_eyebrow">تطبيقات Linux</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">تطبيقات سطح المكتب لسطح مكتب Linux، تُفتح من سطح المكتب.</string>
|
||||
@@ -2323,12 +2424,9 @@
|
||||
<string name="cardboard_recenter_button">إعادة التوسيط</string>
|
||||
<string name="cardboard_steamvr">SteamVR (تجريبي)</string>
|
||||
<string name="cardboard_steamvr_install">تثبيت</string>
|
||||
<string name="cardboard_steamvr_install_hint">ينزّل SteamVR لمعمارية ARM64 من Valve (571 ميغابايت).</string>
|
||||
<string name="cardboard_steamvr_installing">جارٍ تنزيل SteamVR وفك ضغطه…</string>
|
||||
<string name="cardboard_steamvr_start">بدء</string>
|
||||
<string name="cardboard_steamvr_ready_hint">يشغّل SteamVR مع استخدام رأسك كنظارة؛ وتتحول الصورة إلى ثلاثي الأبعاد جنبًا إلى جنب.</string>
|
||||
<string name="cardboard_steamvr_stop">إيقاف</string>
|
||||
<string name="cardboard_steamvr_running_hint">SteamVR قيد التشغيل.</string>
|
||||
<string name="cardboard_steamvr_unavailable">غير متوفر في هذه الجلسة.</string>
|
||||
<string name="cardboard_steamvr_failed">لم يستجب SteamVR؛ تحقّق من سجل الجلسة.</string>
|
||||
<string name="drawer_component_downloads">تنزيل المكوّنات</string>
|
||||
@@ -2342,4 +2440,195 @@
|
||||
<string name="stores_hltb_completionist">الإكمال التام</string>
|
||||
<string name="stores_hltb_hours">%1$s س</string>
|
||||
<string name="stores_hltb_minutes">%1$d د</string>
|
||||
<string name="display_picture_position">موضع الصورة</string>
|
||||
<string name="display_position_top">أعلى</string>
|
||||
<string name="display_position_bottom">أسفل</string>
|
||||
<string name="display_position_center">الوسط (افتراضي)</string>
|
||||
<string name="setup_tab_storage">التخزين</string>
|
||||
<string name="storage_measuring">جارٍ قياس التخزين…</string>
|
||||
<string name="storage_measuring_hint">يستغرق القياس الأول بضع ثوانٍ.</string>
|
||||
<string name="storage_internal">التخزين الداخلي</string>
|
||||
<string name="storage_used_of">المستخدم %1$s من %2$s</string>
|
||||
<string name="storage_free">%1$s متاحة</string>
|
||||
<string name="storage_droiddeck_uses">يستخدم DroidDeck %1$s. قيس في %2$s.</string>
|
||||
<string name="storage_rescanning">جارٍ القياس من جديد…</string>
|
||||
<string name="storage_other_apps">تطبيقات Android الأخرى</string>
|
||||
<string name="storage_safe_now">يمكن تحريره الآن</string>
|
||||
<string name="storage_safe_hint">بيئات تشغيل غير مستخدمة وبقايا وسجلات قديمة</string>
|
||||
<string name="storage_clean_up">جارٍ التنظيف…</string>
|
||||
<string name="storage_session_running">أوقف الجلسة لتنظيف التخزين أو حذف شيء. تبقى الأحجام ظاهرة.</string>
|
||||
<string name="storage_where">إلى أين يذهب</string>
|
||||
<string name="storage_games">ألعاب Steam</string>
|
||||
<string name="storage_game_data">بيانات الألعاب</string>
|
||||
<string name="storage_game_data_hint">البادئات %1$s · الورشة %2$s · الشيدرات %3$s</string>
|
||||
<string name="storage_tools">Proton وبيئات التشغيل</string>
|
||||
<string name="storage_tools_unused">%1$s غير مستخدم</string>
|
||||
<string name="storage_stores">GOG وEpic ومتاجر أخرى</string>
|
||||
<string name="storage_stores_hint">احذفها من المتاجر</string>
|
||||
<string name="storage_desktop">تطبيقات سطح المكتب</string>
|
||||
<string name="storage_desktop_hint">تطبيقات Flatpak وبيئات التشغيل</string>
|
||||
<string name="storage_system">نظام Linux وعميل Steam</string>
|
||||
<string name="storage_system_hint">يلزم لتشغيل أي شيء</string>
|
||||
<string name="storage_app_data">بيانات DroidDeck</string>
|
||||
<string name="storage_app_data_hint">سجلات الجلسات القديمة %1$s · ذاكرة مؤقتة · برامج تشغيل</string>
|
||||
<string name="storage_legend_games">الألعاب</string>
|
||||
<string name="storage_legend_data">بيانات الألعاب</string>
|
||||
<string name="storage_legend_tools">Proton وبيئات التشغيل</string>
|
||||
<string name="storage_legend_stores">متاجر أخرى</string>
|
||||
<string name="storage_legend_desktop">تطبيقات سطح المكتب</string>
|
||||
<string name="storage_legend_system">نظام Linux</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">التنظيف</string>
|
||||
<string name="storage_clean_up_lede">لا يمس أي شيء هنا الألعاب المثبَّتة ولا التنزيلات الجارية.</string>
|
||||
<string name="storage_nothing_to_clean">لا يوجد ما يُنظَّف الآن.</string>
|
||||
<string name="storage_selected">المحدد: %1$s · %2$s ← %3$s ستُحرَّر</string>
|
||||
<string name="storage_cleaning">جارٍ التنظيف…</string>
|
||||
<string name="storage_free_button">تحرير %1$s</string>
|
||||
<string name="storage_freed">حُرِّر %1$s.</string>
|
||||
<string name="storage_item_runtimes">بيئات تشغيل Steam وProton غير المستخدمة</string>
|
||||
<string name="storage_item_leftovers">بقايا ألعاب محذوفة</string>
|
||||
<string name="storage_item_leftovers_unsure">بقايا قد تحتوي على حفظات</string>
|
||||
<string name="storage_item_logs">سجلات جلسات قديمة</string>
|
||||
<string name="storage_item_logs_detail">تبقى سجلات الجلسة الحالية</string>
|
||||
<string name="storage_item_custom_detail">لا تستخدمه أي لعبة. أعد تثبيته من المكوّنات إذا احتجته.</string>
|
||||
<string name="storage_item_shaders">ذاكرة الشيدرات المؤقتة للألعاب المثبَّتة</string>
|
||||
<string name="storage_item_shaders_detail">تُبنى من جديد أثناء اللعب. ستعود التقطعات في أول دقائق من كل لعبة.</string>
|
||||
<string name="storage_tag_safe">آمن</string>
|
||||
<string name="storage_tag_unused">غير مستخدم</string>
|
||||
<string name="storage_tag_stutter">تقطعات</string>
|
||||
<string name="storage_tag_saves">تحقق من الحفظات</string>
|
||||
<string name="storage_tag_default">افتراضي</string>
|
||||
<string name="storage_tag_required">مطلوب</string>
|
||||
<string name="storage_tag_not_used">غير مستخدم</string>
|
||||
<string name="storage_back">التخزين</string>
|
||||
<string name="storage_games_with_data">%1$s مع بياناتها</string>
|
||||
<string name="storage_sort_largest">الأكبر</string>
|
||||
<string name="storage_sort_least_played">الأقل لعبًا</string>
|
||||
<string name="storage_sort_sd">على بطاقة SD</string>
|
||||
<string name="storage_no_games">لا توجد ألعاب هنا.</string>
|
||||
<string name="storage_played">لعبت %1$s</string>
|
||||
<string name="storage_never_played">لم تُلعب قط</string>
|
||||
<string name="storage_game_parts">اللعبة %1$s · البيانات %2$s</string>
|
||||
<string name="storage_uninstall">إلغاء التثبيت</string>
|
||||
<string name="storage_games_note">إلغاء التثبيت يفتح نافذة Steam ليُسجّل Steam العملية. لنقل لعبة إلى بطاقة SD استخدم Steam › الإعدادات › التخزين.</string>
|
||||
<string name="storage_uninstall_title">إلغاء تثبيت %1$s؟</string>
|
||||
<string name="storage_uninstall_body">يفتح Steam ويطلب التأكيد. هذا يحرر %1$s الخاصة باللعبة. تبقى البادئة حتى تنظّف البقايا، وتبقى حفظات Steam Cloud في حسابك.</string>
|
||||
<string name="storage_open_steam">فتح Steam</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s غير مستخدم</string>
|
||||
<string name="storage_remove_unused">حذف غير المستخدم · %1$s</string>
|
||||
<string name="storage_in_use">قيد الاستخدام</string>
|
||||
<string name="storage_not_used">غير مستخدم</string>
|
||||
<string name="storage_remove">حذف</string>
|
||||
<string name="storage_remove_title">حذف %1$s؟</string>
|
||||
<string name="storage_remove_body">يحرر %1$s. سينزّله Steam من جديد إذا احتاجته لعبة.</string>
|
||||
<string name="storage_tool_fex_runtime">يلزم لألعاب Linux x86 (FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">يلزم لإصدارات Proton لـ ARM64</string>
|
||||
<string name="storage_tool_redist">تثبيتات VC++ وDirectX التي تحتاجها الألعاب</string>
|
||||
<string name="storage_tool_fex">يشغّل ألعاب Linux x86</string>
|
||||
<string name="storage_tool_steam">أداة Steam</string>
|
||||
<string name="storage_tool_old_runtime">يشغّل DroidDeck الألعاب دون بيئة التشغيل هذه</string>
|
||||
<string name="storage_tool_x86_proton">Proton x86، ولا يستطيع DroidDeck تشغيله</string>
|
||||
<string name="storage_tool_custom_default">الافتراضي لكل الألعاب</string>
|
||||
<string name="storage_tool_custom_unused">لا تستخدمه أي لعبة</string>
|
||||
<string name="storage_tool_custom_unknown">لا يمكن قراءة إعدادات Steam، لذا يُحتفظ به</string>
|
||||
<string name="storage_cleanup_busy">انتظر انتهاء تنظيف التخزين</string>
|
||||
<string name="storage_low_title">المتاح %1$s فقط</string>
|
||||
<string name="storage_low_body">يحتاج Steam مساحة للتحديثات وذاكرة الشيدرات. تحت 10 GB قد تفشل تحديثات الألعاب في المنتصف.</string>
|
||||
<string name="storage_low_safe">يمكن تحرير %1$s بأمان.</string>
|
||||
<string name="storage_free_up">تحرير مساحة</string>
|
||||
<string name="storage_play_anyway">اللعب على أي حال</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d مثبَّتة</item>
|
||||
<item quantity="other">%d مثبَّتة</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d على بطاقة SD</item>
|
||||
<item quantity="other">%d على بطاقة SD</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">%d أداة</item>
|
||||
<item quantity="other">%d أدوات</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">مجلدات بادئة فارغة وذاكرة شيدرات من %d لعبة</item>
|
||||
<item quantity="other">مجلدات بادئة فارغة وذاكرة شيدرات من %d لعبة</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">بادئة %d لعبة تحتوي على ملفات، منها حفظات محلية. انسخها أولًا إن كنت تحتاجها.</item>
|
||||
<item quantity="other">بادئات %d لعبة تحتوي على ملفات، منها حفظات محلية. انسخها أولًا إن كنت تحتاجها.</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">%d لعبة</item>
|
||||
<item quantity="other">%d لعبة</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">Proton الافتراضي في DroidDeck</string>
|
||||
<string name="storage_tool_proton">خيار Proton 11 في DroidDeck</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">تستخدمه %d لعبة</item>
|
||||
<item quantity="other">تستخدمه %d لعبة</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">المكوّنات</string>
|
||||
<string name="storage_components">المكوّنات</string>
|
||||
<string name="storage_item_components">مكوّنات غير مستخدمة</string>
|
||||
<string name="storage_component_original">الأصل %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">يحرر %1$s. يعيد فقط إصدار Proton غير المثبَّت الآن.</string>
|
||||
<string name="storage_leftovers_unchecked">لم يُتحقق: لا يمكن قراءة بيان تطبيق Steam، لذا تبقى بادئات الألعاب المحذوفة حتى يصبح ذلك ممكنًا.</string>
|
||||
<string name="storage_failed">فشل: %1$s</string>
|
||||
<string name="storage_desktop_shared">%1$s مشتركة</string>
|
||||
<string name="storage_data_installed">الألعاب المثبَّتة</string>
|
||||
<string name="storage_data_parts">البادئة %1$s · الشيدرات %2$s · الورشة %3$s</string>
|
||||
<string name="storage_clear_shaders">مسح الشيدرات</string>
|
||||
<string name="storage_data_leftovers">ألعاب محذوفة</string>
|
||||
<string name="storage_steam_app">تطبيق Steam %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">البادئة تحتوي على ملفات، ربما حفظات محلية</string>
|
||||
<string name="storage_leftover_confirm">يمسح بادئة Wine وذاكرة الشيدرات (%1$s). انسخ الحفظات التي تحتاجها أولًا؛ تبقى حفظات Steam Cloud في حسابك.</string>
|
||||
<string name="storage_data_other">محفوظة</string>
|
||||
<string name="storage_data_other_title">ألعاب خارج Steam وبادئة Steam المشتركة</string>
|
||||
<string name="storage_data_other_detail">بادئات ألعاب أُضيفت من المتاجر أو يدويًا</string>
|
||||
<string name="storage_components_packages">حزم منزّلة</string>
|
||||
<string name="storage_components_originals">أصول محفوظة</string>
|
||||
<string name="storage_components_none">لا حزم مكوّنات منزّلة.</string>
|
||||
<string name="storage_component_in_use">مختارة لـ Proton أو لعبة</string>
|
||||
<string name="storage_component_unused">غير مختارة في أي مكان</string>
|
||||
<string name="storage_original_kept">يعيد Proton المثبَّت</string>
|
||||
<string name="storage_original_old">إصدار Proton غير مثبَّت الآن</string>
|
||||
<string name="storage_tag_in_use">قيد الاستخدام</string>
|
||||
<string name="storage_components_note">تبقى المكوّنات المستخدمة مقفلة. اخترها من المكوّنات؛ وما تحذفه هنا يمكن تنزيله هناك من جديد.</string>
|
||||
<string name="storage_component_confirm">يحرر %1$s. الحزم التي استوردتها بنفسك لا يمكن تنزيلها من جديد.</string>
|
||||
<string name="storage_desktop_detail">%1$s لتطبيقات Flatpak وما تشترك فيه</string>
|
||||
<string name="storage_desktop_apps_title">التطبيقات</string>
|
||||
<string name="storage_desktop_none">لا توجد تطبيقات Flatpak مثبَّتة.</string>
|
||||
<string name="storage_desktop_shared_title">مشتركة بين التطبيقات</string>
|
||||
<string name="storage_desktop_runtimes">بيئات التشغيل</string>
|
||||
<string name="storage_desktop_runtimes_detail">المكتبات التي تعمل عليها التطبيقات؛ غير المستخدمة تزول مع حذف غير المستخدم</string>
|
||||
<string name="storage_desktop_cache">تخزين التنزيل</string>
|
||||
<string name="storage_desktop_cache_detail">مخزن الكائنات والذاكرة المؤقتة المنزّلة في Flatpak</string>
|
||||
<string name="storage_desktop_prune">حذف غير المستخدم</string>
|
||||
<string name="storage_desktop_prune_body">يحذف بيئات التشغيل التي لا يحتاجها أي تطبيق مثبَّت وينظّف مخزن كائنات Flatpak. تبقى التطبيقات المثبَّتة تعمل.</string>
|
||||
<string name="storage_flatpak_uninstall_body">يحذف التطبيق (%1$s) وبيئات التشغيل التي لا يستخدمها غيره. تبقى بياناته في مجلدك الرئيسي.</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">%d حزمة</item>
|
||||
<item quantity="other">%d حزم</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">%d تطبيق</item>
|
||||
<item quantity="other">%d تطبيقات</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">فيديو الإقلاع</string>
|
||||
<string name="boot_movie_video">الفيديو</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d إطار/ث</string>
|
||||
<string name="boot_movie_note">معدل 60 إطارًا/ث أكثر سلاسة لكنه يستهلك المعالج أكثر أثناء بدء Steam. فيديو البدء المختار في إعدادات Steam نفسها له الأولوية.</string>
|
||||
<string name="boot_movie_sound">الصوت</string>
|
||||
<string name="boot_movie_custom">فيديو خاص بك</string>
|
||||
<string name="boot_movie_custom_hint">MP4 أو MOV أو MKV أو WebM. تُحوَّل الصيغ الأخرى والملفات الأكبر من %1$d ميغابايت إلى WebM بدقة 720p على الجهاز، حتى %2$d ثانية.</string>
|
||||
<string name="boot_movie_import">استيراد</string>
|
||||
<string name="boot_movie_remove">إزالة</string>
|
||||
<string name="boot_movie_pick">اختر فيديو الإقلاع</string>
|
||||
<string name="boot_movie_imported">أصبح %1$s فيديو الإقلاع</string>
|
||||
<string name="boot_movie_imported_silent">أصبح %1$s فيديو الإقلاع، بلا صوت</string>
|
||||
<string name="boot_movie_too_long">يجب ألا تتجاوز مدة الفيديو المطلوب تحويله %1$d ثانية</string>
|
||||
<string name="boot_movie_converting">جارٍ التحويل %1$d%%</string>
|
||||
<string name="boot_movie_converting_started">جارٍ تحويل %1$s لـ Steam. أبقِ DroidDeck مفتوحًا.</string>
|
||||
<string name="boot_movie_unsupported">لا يستطيع هذا الجهاز فك ترميز هذا الفيديو.</string>
|
||||
<string name="boot_movie_import_failed">تعذّر استيراد الفيديو</string>
|
||||
</resources>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -278,6 +278,9 @@
|
||||
<string name="mode_suspend_steam_note">Автоматично поставя на пауза и продължава, когато се покаже отново. Системно подготвя Steam за заспиване първо. Ръчно изчаква „Продължи“. Никога продължава да работи.</string>
|
||||
<string name="mode_background_downloads">Продължаване на изтеглянията</string>
|
||||
<string name="mode_background_downloads_hint">Отлага фоновото спиране, докато Steam активно изтегля.</string>
|
||||
<string name="mode_zram">zRAM компресия</string>
|
||||
<string name="mode_zram_hint">Докато играта работи, паметта на Steam клиента се компресира в zram на устройството, така че играта получава повече RAM. Важи при следващото стартиране на Steam.</string>
|
||||
<string name="mode_zram_unsupported">Недостъпно: устройството няма zram или swap, или ядрото е по-старо от 5.4.</string>
|
||||
<string name="mode_suspend_note">Автоматично поставя на пауза във фонов режим и продължава при показване. Ръчно изчаква „Продължи“. Никога продължава да работи.</string>
|
||||
<string name="mode_startup">Стартиране</string>
|
||||
<string name="mode_steam_startup">Стартиране на Steam заедно с DroidDeck</string>
|
||||
@@ -575,6 +578,10 @@
|
||||
<string name="drawer_cas">Изостряне (CAS)</string>
|
||||
<string name="drawer_cas_level">Ниво на изостряне</string>
|
||||
<string name="drawer_fake_hdr">Имитация на HDR</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">Стартиране на SteamVR</string>
|
||||
<string name="steamvr_checking">Проверка на SteamVR…</string>
|
||||
<string name="drawer_deband">Премахване на цветови ленти</string>
|
||||
<string name="drawer_deband_strength">Сила на премахването на цветови ленти</string>
|
||||
<string name="drawer_brightness">Яркост</string>
|
||||
@@ -592,6 +599,11 @@
|
||||
<string name="drawer_texture_sharpness_note">Отрицателно отместване на mip нивото: по-остри текстури, леко трептене. Автоматично съответства на степента, до която сесията е увеличена спрямо екрана.</string>
|
||||
<string name="drawer_touch_auto">Автоматично (%1$s)</string>
|
||||
<string name="drawer_keyboard">Клавиатура</string>
|
||||
<string name="drawer_latency">Закъснение</string>
|
||||
<string name="drawer_low_latency_controller">Контролер с ниско закъснение</string>
|
||||
<string name="drawer_low_latency_controller_hint">Контролерът докладва при 1000 Hz вместо 250 Hz. Малко повече CPU.</string>
|
||||
<string name="drawer_low_latency_touch">Докосване с ниско закъснение</string>
|
||||
<string name="drawer_low_latency_touch_hint">Докосванията пропускат групирането на кадри; управлението с докосване в Steam работи при 1000 Hz. Малко повече CPU.</string>
|
||||
<string name="drawer_pc_keyboard">Клавиатура за компютър</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">Втори екран</string>
|
||||
@@ -683,6 +695,14 @@
|
||||
<string name="setup_saved_logs">Запазени дневници на сесията</string>
|
||||
<string name="setup_saved_logs_hint">Запазва най-новите %1$d</string>
|
||||
<string name="setup_clear_logs">Изчистване на дневниците</string>
|
||||
<string name="setup_reset_settings">Нулиране на всички настройки</string>
|
||||
<string name="setup_reset_settings_hint">Връща всяка настройка към стойността по подразбиране</string>
|
||||
<string name="setup_reset_settings_button">Нулиране</string>
|
||||
<string name="setup_reset_settings_title">Нулиране на всички настройки?</string>
|
||||
<string name="setup_reset_settings_body">Настройките за картина, производителност, контролер, тема, език и лаунчър се връщат към стандартните. Игрите, входът в Steam, профилите в магазините, изтеглените драйвери и папките за съхранение остават.</string>
|
||||
<string name="setup_reset_settings_profiles">Нулирай и профилите на игрите (Proton, FEX, компоненти и среда за всяка игра, както и средата на профила По подразбиране)</string>
|
||||
<string name="setup_reset_settings_session_running">Спри сесията, преди да нулираш настройките.</string>
|
||||
<string name="setup_reset_settings_partial">Настройките са нулирани, но не всички. Подробностите са в логовете.</string>
|
||||
<string name="setup_offline">Офлайн режим</string>
|
||||
<string name="setup_sign_in_first">Първо влезте в Steam</string>
|
||||
<string name="setup_launcher">Лаунчър</string>
|
||||
@@ -843,7 +863,7 @@
|
||||
<string name="gate_eyebrow">Steam</string>
|
||||
<string name="gate_lede">Android спира приложения, които стартират много фонови процеси, а Steam стартира десетки. Докато това не се промени, игрите се затварят без грешка.</string>
|
||||
<string name="gate_fix_for_me">Поправи го вместо мен</string>
|
||||
<string name="gate_toggle_steps">Включете „Disable child process restrictions“, след което се върнете. Оставете Опции за програмисти включени, иначе превключвателят се изключва отново.</string>
|
||||
<string name="gate_toggle_steps">Включете „Деактивиране на ограниченията за дъщерните процеси“, след което се върнете. Оставете Опции за програмисти включени, иначе превключвателят се изключва отново.</string>
|
||||
<string name="gate_open_dev">Отваряне на Опции за програмисти</string>
|
||||
<string name="gate_no_toggle">Тази версия на Android няма превключвател за това, затова DroidDeck го променя чрез безжично отстраняване на грешки. Отнема около минута, не е нужен компютър.</string>
|
||||
<string name="gate_needs_wifi">Безжичното отстраняване на грешки изисква Wi-Fi. Работи с всяка мрежа; не е необходим интернет.</string>
|
||||
@@ -1009,7 +1029,6 @@
|
||||
<string name="gpu_set_with_runtime">Задава се заедно с драйвера за изпълнение: %1$s включва и двете.</string>
|
||||
<string name="gpu_runtime_hint">Използва се от игрите и работния плот.</string>
|
||||
<string name="gpu_runtime_default">Стандартен за изпълнение</string>
|
||||
<string name="gpu_display_hint">Изобразява кадрите на екрана. Прилага се след рестартиране на DroidDeck.</string>
|
||||
<string name="gpu_auto_by_gpu">Автоматично – избрано според GPU</string>
|
||||
<string name="gpu_manage">Изтегляне, импортиране или изтриване…</string>
|
||||
<string name="gpu_in_use_caps">В УПОТРЕБА</string>
|
||||
@@ -1239,10 +1258,16 @@
|
||||
<string name="drivers_downloading_pair">Изтегляне на %1$s %2$s…</string>
|
||||
<string name="drivers_updated_to">Актуализирано до %1$s %2$s.</string>
|
||||
<string name="drivers_updated_to_restart">Актуализирано до %1$s %2$s. Драйверът за дисплея се прилага след рестартиране на DroidDeck.</string>
|
||||
<string name="drivers_auto_waits_for_session">Актуализациите на драйверите изчакват спирането на сесията.</string>
|
||||
<string name="drivers_auto_waits_for_network">%1$s %2$s ще се изтегли, след като излезеш от мобилни данни и пестене на батерия.</string>
|
||||
<string name="drivers_skipping_bad">%1$s %2$s не стартира сесия тук, затова Автоматично остава на драйверите, които го направиха.</string>
|
||||
<string name="drivers_rolled_back">%1$s не стартира сесия два пъти, затова Автоматично се върна на драйверите, които работеха.</string>
|
||||
<string name="drivers_switched_to">Превключено към %1$s %2$s.</string>
|
||||
<string name="drivers_switched_to_restart">Превключено към %1$s %2$s. Драйверът за дисплея се прилага след рестартиране на DroidDeck.</string>
|
||||
<string name="drivers_using">Използва се %1$s %2$s.</string>
|
||||
<string name="drivers_using_restart">Използва се %1$s %2$s. Драйверът за дисплея се прилага след рестартиране на DroidDeck.</string>
|
||||
<string name="drivers_using_quest">Използват се включеният драйвер за картина на Quest и средата по подразбиране.</string>
|
||||
<string name="drivers_using_quest_restart">Използват се включеният драйвер за картина на Quest и средата по подразбиране. Драйверът за картина важи след рестарт на DroidDeck.</string>
|
||||
<string name="drivers_not_a_driver">Не е ZIP с драйвер: нито драйвер на AdrenoTools, нито -Linux Turnip, нито пакет Android + Linux</string>
|
||||
<string name="drivers_import_failed">Импортирането се провали: %1$s</string>
|
||||
<string name="drivers_imported_unnamed">Импортиран драйвер</string>
|
||||
@@ -1664,7 +1689,7 @@
|
||||
<string name="adbpair_done_text">Докосни, за да се върнеш в DroidDeck. Вече можеш да изключиш безжичното отстраняване на грешки.</string>
|
||||
<string name="adbpair_title_failed">Настройката на безжичното отстраняване на грешки спря</string>
|
||||
|
||||
<string name="selfinst_http">Изтеглянето отговори с HTTP %1$d</string>
|
||||
<string name="selfinst_download_failed">Изтеглянето не успя</string>
|
||||
<string name="selfinst_size_mismatch">Размерът на изтеглянето не съответстваше на публикуваното APK и беше отхвърлен - опитай отново</string>
|
||||
<string name="selfinst_checksum_mismatch">Изтеглянето не съответстваше на контролната си сума и беше отхвърлено - опитай отново</string>
|
||||
<string name="selfinst_wrong_package">Актуализацията е за Android пакета %1$s, а не за %2$s</string>
|
||||
@@ -1762,7 +1787,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">Достигнато е ограничението на GitHub за заявки - опитай отново по-късно</string>
|
||||
<string name="turnip_github_http">GitHub отговори с HTTP %1$d</string>
|
||||
<string name="turnip_download_http">изтеглянето отговори с HTTP %1$d</string>
|
||||
<string name="turnip_download_failed">Изтеглянето не успя</string>
|
||||
<string name="turnip_list_predates_checksums">Този списък с драйвери е отпреди въвеждането на контролни суми - обнови го и опитай отново</string>
|
||||
<string name="turnip_checksum_mismatch">Несъответствие в контролната сума - изтеглянето беше отхвърлено</string>
|
||||
<string name="turnip_not_adrenotools">Не е драйвер за AdrenoTools: %1$s</string>
|
||||
@@ -1799,7 +1824,7 @@
|
||||
<string name="phantom_instructions_fix">%1$s Ако това не е възможно, свържи устройството с компютър с ADB и изпълни командата отдолу. Върни се тук и провери отново.</string>
|
||||
<string name="phantom_instructions_off">Сесиите на Steam могат да стартират.</string>
|
||||
<string name="phantom_instructions_not_applicable">Тази версия на Android не използва това ограничение.</string>
|
||||
<string name="phantom_fix_toggle">Включи „Disable child process restrictions“ в Опции за програмисти и остави Опции за програмисти включени след това.</string>
|
||||
<string name="phantom_fix_toggle">Включи „Деактивиране на ограниченията за дъщерните процеси“ в Опции за програмисти и остави Опции за програмисти включени след това.</string>
|
||||
<string name="phantom_fix_no_toggle">Тази версия на Android няма превключвател за това. Използвай безжично отстраняване на грешки, за да го промениш от това устройство.</string>
|
||||
<string name="upd_block_no_apk">Тази версия няма изтегляне за това копие на DroidDeck.</string>
|
||||
<string name="upd_block_version_code">Android няма да я инсталира върху тази версия, защото нейният versionCode %1$d е по-нисък от инсталираното %2$d.</string>
|
||||
@@ -2031,6 +2056,9 @@
|
||||
<string name="stores_install_title">Инсталация на %1$s:</string>
|
||||
<string name="stores_target_internal">Вътрешна памет</string>
|
||||
<string name="stores_target_free">%1$s свободни</string>
|
||||
<string name="stores_target_free_unknown">Свободното място е неизвестно</string>
|
||||
<string name="stores_storage_insufficient">Недостатъчно свободно място: нужно е %1$s, свободни са само %2$s</string>
|
||||
<string name="stores_storage_low">Недостатъчно място (свободни %1$s, нужни %2$s)</string>
|
||||
<string name="stores_notification_channel">Изтегляния от магазина</string>
|
||||
<string name="stores_notification_channel_hint">Показва напредъка на изтеглянето на игри и ги държи активни на заден фон</string>
|
||||
<string name="stores_notification_one">Изтегляне</string>
|
||||
@@ -2066,6 +2094,21 @@
|
||||
<string name="edit_other_exe">Друг exe…</string>
|
||||
<string name="edit_remove">Премахни</string>
|
||||
<string name="art_cover">Обложка</string>
|
||||
<string name="art_wide_cover">Широка обложка</string>
|
||||
<string name="art_adjust_logo">Настройка на логото</string>
|
||||
<string name="logo_pos_default">По подразбиране</string>
|
||||
<string name="logo_pos_upper_left">Горе вляво</string>
|
||||
<string name="logo_pos_upper_center">Горе в средата</string>
|
||||
<string name="logo_pos_center">В средата</string>
|
||||
<string name="logo_pos_bottom_left">Долу вляво</string>
|
||||
<string name="logo_pos_bottom_center">Долу в средата</string>
|
||||
<string name="logo_pos_position">Позиция</string>
|
||||
<string name="logo_pos_size">Размер</string>
|
||||
<string name="logo_pos_save">Запази</string>
|
||||
<string name="logo_pos_reset">Обратно по подразбиране</string>
|
||||
<string name="logo_pos_hint">Рисува се върху фона на играта, както прави Steam. Точната позиция може да се различава с няколко пиксела.</string>
|
||||
<string name="logo_pos_no_logo">Тази игра още няма лого. Избери такова в Лого, за да го видиш тук.</string>
|
||||
<string name="logo_pos_failed">Позицията на логото не можа да се запази. Опитай отново.</string>
|
||||
<string name="art_background">Фон</string>
|
||||
<string name="art_logo">Лого</string>
|
||||
<string name="art_icon">Икона</string>
|
||||
@@ -2078,6 +2121,17 @@
|
||||
<string name="art_pick_image">Избери изображение…</string>
|
||||
<string name="art_pick_title">Избери изображение: %1$s</string>
|
||||
<string name="art_reset">Връщане към автоматично</string>
|
||||
<string name="art_search_sgdb">Търсене в SteamGridDB…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB: %1$s</string>
|
||||
<string name="art_sgdb_search_label">Търсене на игра</string>
|
||||
<string name="art_sgdb_other_games">Други резултати</string>
|
||||
<string name="art_sgdb_none">Няма резултати.</string>
|
||||
<string name="art_sgdb_no_key">SteamGridDB изисква API ключ. Добави своя от Настройки › Лаунчър.</string>
|
||||
<string name="art_sgdb_bad_key">SteamGridDB не прие API ключа. Провери го от Настройки › Лаунчър.</string>
|
||||
<string name="art_sgdb_limit">SteamGridDB ограничава заявките в момента. Опитай отново след минута.</string>
|
||||
<string name="art_sgdb_offline">SteamGridDB не е достъпен. Провери връзката.</string>
|
||||
<string name="art_sgdb_server">Нещо се обърка в SteamGridDB. Опитай по-късно.</string>
|
||||
<string name="art_sgdb_no_game">SteamGridDB няма резултати за тази игра. Опитай да я потърсиш.</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="one">%1$d игра добавена</item>
|
||||
<item quantity="other">%1$d игри добавени</item>
|
||||
@@ -2105,6 +2159,45 @@
|
||||
<string name="gpu_bar_auto">Автоматично · %1$s</string>
|
||||
<string name="gpu_separately">Драйверите за среда и екран поотделно</string>
|
||||
<string name="gpu_use_title">Използвай %1$s?</string>
|
||||
<string name="gpu_mode_title">Как се избират драйверите</string>
|
||||
<string name="gpu_mode_auto_detail">Най-добрата двойка за този GPU, винаги актуална</string>
|
||||
<string name="gpu_mode_pair">Съвпадаща двойка</string>
|
||||
<string name="gpu_mode_pair_detail">Избери двойка; задава двата драйвера</string>
|
||||
<string name="gpu_mode_mix">Свободно смесване</string>
|
||||
<string name="gpu_mode_mix_detail">Избери картина и среда поотделно</string>
|
||||
<string name="gpu_mode_pair_short">Двойка</string>
|
||||
<string name="gpu_mode_mix_short">Смесено</string>
|
||||
<string name="gpu_src_auto">АВТО</string>
|
||||
<string name="gpu_src_pair">ДВОЙКА</string>
|
||||
<string name="gpu_src_bundle">ПАКЕТ</string>
|
||||
<string name="gpu_src_chosen">ИЗБРАН</string>
|
||||
<string name="gpu_src_by_gpu">СПОРЕД GPU</string>
|
||||
<string name="gpu_src_default">СТАНДАРТ</string>
|
||||
<string name="gpu_display_live">Ризува сега</string>
|
||||
<string name="gpu_display_pending">Нужен е рестарт · дотогава рисува %1$s</string>
|
||||
<string name="gpu_runtime_live">Активна</string>
|
||||
<string name="gpu_runtime_pending">Следваща сесия · тази остава с %1$s</string>
|
||||
<string name="gpu_display_next">Ризува екрана. Важи от следващата сесия.</string>
|
||||
<string name="gpu_previous_driver">предишният драйвер</string>
|
||||
<string name="gpu_restart_banner">Рестартирай, за да смениш драйвера за картина.</string>
|
||||
<string name="gpu_restart_blocked">Спри текущата сесия и рестартирай DroidDeck, за да смениш драйвера за картина.</string>
|
||||
<string name="gpu_restart_now">Рестарт сега</string>
|
||||
<string name="gpu_restart_later">По-късно</string>
|
||||
<string name="gpu_auto_title">Автоматично избра тази двойка за твоя %1$s</string>
|
||||
<string name="gpu_auto_builtin_title">Автоматично използва вградените драйвери за твоя %1$s</string>
|
||||
<string name="gpu_auto_reason">Избрано за семейството %1$s. Новите версии се инсталират на заден план.</string>
|
||||
<string name="gpu_pair_title">Избери двойка</string>
|
||||
<string name="gpu_pair_hint">Двойката задава едновременно драйвера за картина и драйвера за среда.</string>
|
||||
<string name="gpu_mix_title">Избери всеки драйвер</string>
|
||||
<string name="gpu_mix_hint">Пакетът запълва и двете места. Промените в картината важат след рестарт, промените в средата от следващата сесия.</string>
|
||||
<string name="gpu_change_display">Смяна на драйвера за картина</string>
|
||||
<string name="gpu_change_runtime">Смяна на драйвера за среда</string>
|
||||
<string name="gpu_autos_pick">ИЗБОР НА АВТОМАТИЧНО</string>
|
||||
<string name="gpu_installed">Инсталиран</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · рестартирай за прилагане</string>
|
||||
<string name="gpu_chip_restart">%1$s · нужен е рестарт</string>
|
||||
<string name="comp_this_device">Това устройство</string>
|
||||
<string name="proton_remove_title">Премахване на %1$s?</string>
|
||||
<string name="setup_tab_diagnostics">Диагностика</string>
|
||||
<string name="setup_ready">Готово за игра</string>
|
||||
@@ -2116,6 +2209,14 @@
|
||||
<string name="setup_group_look">Визия</string>
|
||||
<string name="setup_group_device">Това устройство</string>
|
||||
<string name="setup_group_art">Визия на библиотеката</string>
|
||||
<string name="setup_group_download">Изтегляне</string>
|
||||
<string name="setup_download_source">Източник</string>
|
||||
<string name="setup_download_source_auto">Автоматично</string>
|
||||
<string name="setup_download_source_direct">Директно</string>
|
||||
<string name="setup_download_source_mirror">Огледало</string>
|
||||
<string name="setup_download_source_custom">Собствен префикс</string>
|
||||
<string name="setup_download_mirror">Сайт</string>
|
||||
<string name="setup_download_prefix">Префикс</string>
|
||||
<string name="flathub_eyebrow">ПРИЛОЖЕНИЯ ЗА LINUX</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">Приложения за десктоп на Linux, отваряни от Desktop.</string>
|
||||
@@ -2223,12 +2324,9 @@
|
||||
<string name="cardboard_recenter_button">Центриране</string>
|
||||
<string name="cardboard_steamvr">SteamVR (експериментално)</string>
|
||||
<string name="cardboard_steamvr_install">Инсталиране</string>
|
||||
<string name="cardboard_steamvr_install_hint">Изтегля SteamVR за ARM64 на Valve (571 MB).</string>
|
||||
<string name="cardboard_steamvr_installing">Изтегляне и разархивиране на SteamVR…</string>
|
||||
<string name="cardboard_steamvr_start">Стартиране</string>
|
||||
<string name="cardboard_steamvr_ready_hint">Стартира SteamVR с главата ви като шлем; картината превключва на 3D един до друг.</string>
|
||||
<string name="cardboard_steamvr_stop">Спиране</string>
|
||||
<string name="cardboard_steamvr_running_hint">SteamVR работи.</string>
|
||||
<string name="cardboard_steamvr_unavailable">Не е налично в тази сесия.</string>
|
||||
<string name="cardboard_steamvr_failed">SteamVR не отговори; проверете лога на сесията.</string>
|
||||
<string name="drawer_component_downloads">Изтегляне на компоненти</string>
|
||||
@@ -2242,4 +2340,195 @@
|
||||
<string name="stores_hltb_completionist">Пълно</string>
|
||||
<string name="stores_hltb_hours">%1$s ч</string>
|
||||
<string name="stores_hltb_minutes">%1$d мин</string>
|
||||
<string name="display_picture_position">Позиция на картината</string>
|
||||
<string name="display_position_top">Горе</string>
|
||||
<string name="display_position_bottom">Долу</string>
|
||||
<string name="display_position_center">В средата (по подразбиране)</string>
|
||||
<string name="setup_tab_storage">Съхранение</string>
|
||||
<string name="storage_measuring">Измерване на съхранението…</string>
|
||||
<string name="storage_measuring_hint">Първият път отнема няколко секунди.</string>
|
||||
<string name="storage_internal">Вътрешно съхранение</string>
|
||||
<string name="storage_used_of">използвани %1$s от %2$s</string>
|
||||
<string name="storage_free">свободни %1$s</string>
|
||||
<string name="storage_droiddeck_uses">DroidDeck използва %1$s. Измерено на %2$s.</string>
|
||||
<string name="storage_rescanning">измерване отново…</string>
|
||||
<string name="storage_other_apps">Други приложения за Android</string>
|
||||
<string name="storage_safe_now">Може да се освободи сега</string>
|
||||
<string name="storage_safe_hint">Неизползвани среди, остатъци, стари логове</string>
|
||||
<string name="storage_clean_up">Почистване…</string>
|
||||
<string name="storage_session_running">Спри сесията, за да почистиш или изтриеш нещо. Размерите остават видими.</string>
|
||||
<string name="storage_where">Къде отива</string>
|
||||
<string name="storage_games">Игри от Steam</string>
|
||||
<string name="storage_game_data">Данни на игрите</string>
|
||||
<string name="storage_game_data_hint">Префикси %1$s · workshop %2$s · шейдъри %3$s</string>
|
||||
<string name="storage_tools">Proton и среди</string>
|
||||
<string name="storage_tools_unused">%1$s неизползвани</string>
|
||||
<string name="storage_stores">GOG, Epic и други магазини</string>
|
||||
<string name="storage_stores_hint">Премахни ги от Магазини</string>
|
||||
<string name="storage_desktop">Приложения за работен плот</string>
|
||||
<string name="storage_desktop_hint">Приложения Flatpak и среди</string>
|
||||
<string name="storage_system">Система Linux и Steam клиент</string>
|
||||
<string name="storage_system_hint">Нужни, за да работи каквото и да е</string>
|
||||
<string name="storage_app_data">Данни на DroidDeck</string>
|
||||
<string name="storage_app_data_hint">Логове от стари сесии %1$s · кеш · драйвери</string>
|
||||
<string name="storage_legend_games">Игри</string>
|
||||
<string name="storage_legend_data">Данни на игрите</string>
|
||||
<string name="storage_legend_tools">Proton и среди</string>
|
||||
<string name="storage_legend_stores">Други магазини</string>
|
||||
<string name="storage_legend_desktop">Приложения за работен плот</string>
|
||||
<string name="storage_legend_system">Система Linux</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">Почистване</string>
|
||||
<string name="storage_clean_up_lede">Нищо тук не засяга инсталирани игри или текущи изтегляния.</string>
|
||||
<string name="storage_nothing_to_clean">В момента няма какво да се почисти.</string>
|
||||
<string name="storage_selected">Избрано: %1$s · %2$s → %3$s ще се освободят</string>
|
||||
<string name="storage_cleaning">Почистване…</string>
|
||||
<string name="storage_free_button">Освободи %1$s</string>
|
||||
<string name="storage_freed">Освободени %1$s.</string>
|
||||
<string name="storage_item_runtimes">Неизползвани среди на Steam и Proton</string>
|
||||
<string name="storage_item_leftovers">Остатъци от изтрити игри</string>
|
||||
<string name="storage_item_leftovers_unsure">Остатъци, които може да съдържат записи</string>
|
||||
<string name="storage_item_logs">Логове от стари сесии</string>
|
||||
<string name="storage_item_logs_detail">Логовете на текущата сесия остават</string>
|
||||
<string name="storage_item_custom_detail">Никоя игра не го използва. Инсталирай го отново от Компоненти, ако ти потрябва.</string>
|
||||
<string name="storage_item_shaders">Кеш на шейдърите за инсталираните игри</string>
|
||||
<string name="storage_item_shaders_detail">Изгражда се отново по време на игра. Първите минути на всяка игра отново ще накъсват.</string>
|
||||
<string name="storage_tag_safe">Безопасно</string>
|
||||
<string name="storage_tag_unused">Неизползвано</string>
|
||||
<string name="storage_tag_stutter">Накъсване</string>
|
||||
<string name="storage_tag_saves">Провери записите</string>
|
||||
<string name="storage_tag_default">По подразбиране</string>
|
||||
<string name="storage_tag_required">Нужно</string>
|
||||
<string name="storage_tag_not_used">Неизползвано</string>
|
||||
<string name="storage_back">Съхранение</string>
|
||||
<string name="storage_games_with_data">%1$s с данните им</string>
|
||||
<string name="storage_sort_largest">Най-големи</string>
|
||||
<string name="storage_sort_least_played">Най-малко играна</string>
|
||||
<string name="storage_sort_sd">На SD карта</string>
|
||||
<string name="storage_no_games">Тук няма игри.</string>
|
||||
<string name="storage_played">играна %1$s</string>
|
||||
<string name="storage_never_played">Никога не е играна</string>
|
||||
<string name="storage_game_parts">Игра %1$s · данни %2$s</string>
|
||||
<string name="storage_uninstall">Деинсталиране</string>
|
||||
<string name="storage_games_note">Деинсталирането отваря прозореца на Steam, за да го отчете Steam. За да преместиш игра на SD картата, използвай Steam › Настройки › Съхранение.</string>
|
||||
<string name="storage_uninstall_title">Деинсталиране на %1$s?</string>
|
||||
<string name="storage_uninstall_body">Steam се отваря и иска потвърждение. Това освобождава %1$s на играта. Префиксът остава, докато не почистиш остатъците, а записите в Steam Cloud остават в профила ти.</string>
|
||||
<string name="storage_open_steam">Отвори Steam</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s неизползвани</string>
|
||||
<string name="storage_remove_unused">Премахни неизползваните · %1$s</string>
|
||||
<string name="storage_in_use">Използва се</string>
|
||||
<string name="storage_not_used">Неизползвано</string>
|
||||
<string name="storage_remove">Премахни</string>
|
||||
<string name="storage_remove_title">Премахване на %1$s?</string>
|
||||
<string name="storage_remove_body">Освобождава %1$s. Steam ще го изтегли отново, ако игра го поиска.</string>
|
||||
<string name="storage_tool_fex_runtime">Нужна за игри за Linux x86 (FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">Нужна за Proton за ARM64</string>
|
||||
<string name="storage_tool_redist">Инсталации на VC++ и DirectX, нужни на игрите</string>
|
||||
<string name="storage_tool_fex">Стартира игри за Linux x86</string>
|
||||
<string name="storage_tool_steam">Инструмент на Steam</string>
|
||||
<string name="storage_tool_old_runtime">DroidDeck стартира игри без тази среда</string>
|
||||
<string name="storage_tool_x86_proton">Proton x86, който DroidDeck не може да стартира</string>
|
||||
<string name="storage_tool_custom_default">По подразбиране за всички игри</string>
|
||||
<string name="storage_tool_custom_unused">Никоя игра не го използва</string>
|
||||
<string name="storage_tool_custom_unknown">Настройките на Steam не могат да се прочетат, затова се запазва</string>
|
||||
<string name="storage_cleanup_busy">Изчакай почистването на съхранението да завърши</string>
|
||||
<string name="storage_low_title">Свободни са само %1$s</string>
|
||||
<string name="storage_low_body">Steam се нуждае от място за актуализации и кеш на шейдърите. Под 10 GB актуализациите на игрите може да се провалят по средата.</string>
|
||||
<string name="storage_low_safe">%1$s могат да се освободят безопасно.</string>
|
||||
<string name="storage_free_up">Освободи място</string>
|
||||
<string name="storage_play_anyway">Играй въпреки това</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d инсталирана</item>
|
||||
<item quantity="other">%d инсталирани</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d на SD карта</item>
|
||||
<item quantity="other">%d на SD карта</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">%d инструмент</item>
|
||||
<item quantity="other">%d инструмента</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">Празни папки за префикс и кеш на шейдърите от %d игра</item>
|
||||
<item quantity="other">Празни папки за префикс и кеш на шейдърите от %d игри</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">Префиксът на %d игра още съдържа файлове, включително локални записи. Първо направи копие, ако ти трябват.</item>
|
||||
<item quantity="other">Префиксите на %d игри още съдържат файлове, включително локални записи. Първо направи копие, ако ти трябват.</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">%d игра</item>
|
||||
<item quantity="other">%d игри</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">Proton по подразбиране на DroidDeck</string>
|
||||
<string name="storage_tool_proton">Опция Proton 11 на DroidDeck</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">Използва се от %d игра</item>
|
||||
<item quantity="other">Използва се от %d игри</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">Компоненти</string>
|
||||
<string name="storage_components">Компоненти</string>
|
||||
<string name="storage_item_components">Неизползвани компоненти</string>
|
||||
<string name="storage_component_original">Оригинал %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">Освобождава %1$s. Връща само версия на Proton, която вече не е инсталирана.</string>
|
||||
<string name="storage_leftovers_unchecked">Непроверено: манифестът на приложение на Steam не може да се прочете, затова префиксите на изтритите игри остават, докато това стане възможно.</string>
|
||||
<string name="storage_failed">Неуспешно: %1$s</string>
|
||||
<string name="storage_desktop_shared">%1$s споделени</string>
|
||||
<string name="storage_data_installed">Инсталирани игри</string>
|
||||
<string name="storage_data_parts">Префикс %1$s · шейдъри %2$s · workshop %3$s</string>
|
||||
<string name="storage_clear_shaders">Изчисти шейдърите</string>
|
||||
<string name="storage_data_leftovers">Изтрити игри</string>
|
||||
<string name="storage_steam_app">Приложение Steam %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">Префиксът още съдържа файлове, може би локални записи</string>
|
||||
<string name="storage_leftover_confirm">Изчиства Wine префикса и кеша на шейдърите (%1$s). Първо направи копие на записите, които ти трябват; записите в Steam Cloud остават в профила ти.</string>
|
||||
<string name="storage_data_other">Запазени</string>
|
||||
<string name="storage_data_other_title">Игри извън Steam и споделен Steam префикс</string>
|
||||
<string name="storage_data_other_detail">Префикси на игри, добавени през магазини или ръчно</string>
|
||||
<string name="storage_components_packages">Изтеглени пакети</string>
|
||||
<string name="storage_components_originals">Запазени оригинали</string>
|
||||
<string name="storage_components_none">Няма изтеглени пакети с компоненти.</string>
|
||||
<string name="storage_component_in_use">Избран за Proton или игра</string>
|
||||
<string name="storage_component_unused">Не е избран никъде</string>
|
||||
<string name="storage_original_kept">Връща инсталирания Proton</string>
|
||||
<string name="storage_original_old">Версия на Proton, която вече не е инсталирана</string>
|
||||
<string name="storage_tag_in_use">Използва се</string>
|
||||
<string name="storage_components_note">Използваните компоненти остават заключени. Избирай ги от Компоненти; това, което премахнеш тук, може да се изтегли оттам отново.</string>
|
||||
<string name="storage_component_confirm">Освобождава %1$s. Пакетите, които сам си внесъл, не могат да се изтеглят отново.</string>
|
||||
<string name="storage_desktop_detail">%1$s за приложения Flatpak и каквото споделят</string>
|
||||
<string name="storage_desktop_apps_title">Приложения</string>
|
||||
<string name="storage_desktop_none">Няма инсталирани приложения Flatpak.</string>
|
||||
<string name="storage_desktop_shared_title">Споделени от приложенията</string>
|
||||
<string name="storage_desktop_runtimes">Среди</string>
|
||||
<string name="storage_desktop_runtimes_detail">Библиотеките, на които работят приложенията; неизползваните се махат с Премахни неизползваните</string>
|
||||
<string name="storage_desktop_cache">Хранилище за изтегляния</string>
|
||||
<string name="storage_desktop_cache_detail">Обектно хранилище и изтеглен кеш на Flatpak</string>
|
||||
<string name="storage_desktop_prune">Премахни неизползваните</string>
|
||||
<string name="storage_desktop_prune_body">Премахва среди, които никое инсталирано приложение не използва, и почиства обектното хранилище на Flatpak. Инсталираните приложения продължават да работят.</string>
|
||||
<string name="storage_flatpak_uninstall_body">Премахва приложението (%1$s) и средите, които никой друг не използва. Данните в домашната ти папка остават.</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">%d пакет</item>
|
||||
<item quantity="other">%d пакета</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">%d приложение</item>
|
||||
<item quantity="other">%d приложения</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">Начално видео</string>
|
||||
<string name="boot_movie_video">Видео</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d кадъра/с</string>
|
||||
<string name="boot_movie_note">60 кадъра/с изглежда по-гладко, но натоварва повече процесора, докато Steam се стартира. Начално видео, избрано в настройките на Steam, има предимство.</string>
|
||||
<string name="boot_movie_sound">Звук</string>
|
||||
<string name="boot_movie_custom">Собствено видео</string>
|
||||
<string name="boot_movie_custom_hint">MP4, MOV, MKV или WebM. Други формати и файлове над %1$d MB се конвертират на устройството до WebM 720p, до %2$d секунди.</string>
|
||||
<string name="boot_movie_import">Импортиране</string>
|
||||
<string name="boot_movie_remove">Премахване</string>
|
||||
<string name="boot_movie_pick">Изберете начално видео</string>
|
||||
<string name="boot_movie_imported">%1$s вече е началното видео</string>
|
||||
<string name="boot_movie_imported_silent">%1$s вече е началното видео, без звук</string>
|
||||
<string name="boot_movie_too_long">Видеата за конвертиране трябва да са до %1$d секунди</string>
|
||||
<string name="boot_movie_converting">Конвертиране %1$d%%</string>
|
||||
<string name="boot_movie_converting_started">%1$s се конвертира за Steam. Не затваряйте DroidDeck.</string>
|
||||
<string name="boot_movie_unsupported">Устройството не може да декодира това видео.</string>
|
||||
<string name="boot_movie_import_failed">Видеото не можа да бъде импортирано</string>
|
||||
</resources>
|
||||
@@ -278,6 +278,9 @@
|
||||
<string name="mode_suspend_steam_note">Automaticky pozastaví relaci a obnoví ji při zobrazení. Nativně nejprve připraví Steam na spánek. Manuálně čeká na Obnovit. Nikdy běh nezastavuje.</string>
|
||||
<string name="mode_background_downloads">Nechat stahování běžet</string>
|
||||
<string name="mode_background_downloads_hint">Oddálí pozastavení na pozadí, pokud Steam aktivně stahuje.</string>
|
||||
<string name="mode_zram">Komprese zRAM</string>
|
||||
<string name="mode_zram_hint">Během hraní se paměť klienta Steam komprimuje do zram zařízení, takže hra má víc RAM. Platí při příštím spuštění Steamu.</string>
|
||||
<string name="mode_zram_unsupported">Není k dispozici: zařízení nemá zram ani swap, nebo je jádro starší než 5.4.</string>
|
||||
<string name="mode_suspend_note">Automaticky pozastaví na pozadí a obnoví při zobrazení. Manuálně čeká na Obnovit. Nikdy běh nezastavuje.</string>
|
||||
<string name="mode_startup">Spouštění</string>
|
||||
<string name="mode_steam_startup">Spustit Steam s DroidDeck</string>
|
||||
@@ -579,6 +582,10 @@
|
||||
<string name="drawer_cas">Zaostření (CAS)</string>
|
||||
<string name="drawer_cas_level">Úroveň zaostření</string>
|
||||
<string name="drawer_fake_hdr">Falešné HDR</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">Spustit SteamVR</string>
|
||||
<string name="steamvr_checking">Kontrola SteamVR…</string>
|
||||
<string name="drawer_deband">Odstranění pruhování</string>
|
||||
<string name="drawer_deband_strength">Síla odstranění pruhování</string>
|
||||
<string name="drawer_brightness">Jas</string>
|
||||
@@ -596,6 +603,11 @@
|
||||
<string name="drawer_texture_sharpness_note">Záporný mip bias: ostřejší textury, trochu mihotání. Automaticky odpovídá tomu, jak moc je relace zvětšena na obrazovku.</string>
|
||||
<string name="drawer_touch_auto">Automaticky (%1$s)</string>
|
||||
<string name="drawer_keyboard">Klávesnice</string>
|
||||
<string name="drawer_latency">Zpoždění</string>
|
||||
<string name="drawer_low_latency_controller">Ovladač s nízkým zpožděním</string>
|
||||
<string name="drawer_low_latency_controller_hint">Ovladač hlásí na 1000 Hz místo 250 Hz. O něco víc CPU.</string>
|
||||
<string name="drawer_low_latency_touch">Dotek s nízkým zpožděním</string>
|
||||
<string name="drawer_low_latency_touch_hint">Doteky přeskakují dávkování snímků, takže ovládání dotykem ve Steamu běží na 1000 Hz. O něco víc CPU.</string>
|
||||
<string name="drawer_pc_keyboard">Klávesnice PC</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">Druhá obrazovka</string>
|
||||
@@ -689,6 +701,14 @@
|
||||
<string name="setup_saved_logs">Uložené protokoly relací</string>
|
||||
<string name="setup_saved_logs_hint">Uchovává nejnovějších %1$d</string>
|
||||
<string name="setup_clear_logs">Vymazat protokoly</string>
|
||||
<string name="setup_reset_settings">Obnovit všechna nastavení</string>
|
||||
<string name="setup_reset_settings_hint">Vrátí každé nastavení na výchozí hodnotu</string>
|
||||
<string name="setup_reset_settings_button">Obnovit</string>
|
||||
<string name="setup_reset_settings_title">Obnovit všechna nastavení?</string>
|
||||
<string name="setup_reset_settings_body">Nastavení obrazu, výkonu, ovladače, motivu, jazyka a spouštěče se vrátí na výchozí hodnoty. Hry, přihlášení ke Steamu, účty obchodů, stažené ovladače a složky úložiště zůstanou.</string>
|
||||
<string name="setup_reset_settings_profiles">Obnovit i profily her (Proton, FEX, komponenty a prostředí pro každou hru a prostředí profilu Výchozí)</string>
|
||||
<string name="setup_reset_settings_session_running">Před obnovením nastavení zastav relaci.</string>
|
||||
<string name="setup_reset_settings_partial">Nastavení byla obnovena, ale ne všechna. Podrobnosti jsou v logech.</string>
|
||||
<string name="setup_offline">Režim offline</string>
|
||||
<string name="setup_sign_in_first">Nejprve se přihlaste ke Steamu</string>
|
||||
<string name="setup_launcher">Spouštěč</string>
|
||||
@@ -855,7 +875,7 @@
|
||||
<string name="gate_eyebrow">Steam</string>
|
||||
<string name="gate_lede">Android zastavuje aplikace, které spouštějí mnoho procesů na pozadí, a Steam jich spouští desítky. Dokud se to nezmění, hry se zavírají bez chybové zprávy.</string>
|
||||
<string name="gate_fix_for_me">Oprav to za mě</string>
|
||||
<string name="gate_toggle_steps">Zapněte „Disable child process restrictions“ a vraťte se zpět. Nechte Možnosti pro vývojáře zapnuté, jinak se přepínač znovu vypne.</string>
|
||||
<string name="gate_toggle_steps">Zapněte „Deaktivace omezení podřízených procesů“ a vraťte se zpět. Nechte Možnosti pro vývojáře zapnuté, jinak se přepínač znovu vypne.</string>
|
||||
<string name="gate_open_dev">Otevřít Možnosti pro vývojáře</string>
|
||||
<string name="gate_no_toggle">Tato verze Androidu pro to nemá přepínač, proto to DroidDeck změní přes Bezdrátové ladění. Zabere to asi minutu, počítač není potřeba.</string>
|
||||
<string name="gate_needs_wifi">Bezdrátové ladění potřebuje Wi-Fi. Funguje jakákoli síť, internet není nutný.</string>
|
||||
@@ -1021,7 +1041,6 @@
|
||||
<string name="gpu_set_with_runtime">Nastaveno společně s ovladačem runtime: %1$s obsahuje oba.</string>
|
||||
<string name="gpu_runtime_hint">Používají hry a desktop.</string>
|
||||
<string name="gpu_runtime_default">Výchozí runtime</string>
|
||||
<string name="gpu_display_hint">Vykresluje snímky na obrazovku. Použije se po restartu DroidDecku.</string>
|
||||
<string name="gpu_auto_by_gpu">Automaticky - podle GPU</string>
|
||||
<string name="gpu_manage">Stáhnout, importovat nebo smazat…</string>
|
||||
<string name="gpu_in_use_caps">POUŽÍVÁ SE</string>
|
||||
@@ -1259,10 +1278,16 @@
|
||||
<string name="drivers_downloading_pair">Stahování %1$s %2$s…</string>
|
||||
<string name="drivers_updated_to">Aktualizováno na %1$s %2$s.</string>
|
||||
<string name="drivers_updated_to_restart">Aktualizováno na %1$s %2$s. Ovladač zobrazení se použije po restartu DroidDecku.</string>
|
||||
<string name="drivers_auto_waits_for_session">Aktualizace ovladačů počkají, až se relace zastaví.</string>
|
||||
<string name="drivers_auto_waits_for_network">%1$s %2$s se stáhne, jakmile opustíš mobilní data a spořič baterie.</string>
|
||||
<string name="drivers_skipping_bad">%1$s %2$s tady nespustil relaci, takže Automaticky zůstává u ovladačů, které ano.</string>
|
||||
<string name="drivers_rolled_back">%1$s dvakrát nespustil relaci, takže Automaticky se vrátil k ovladačům, které fungovaly.</string>
|
||||
<string name="drivers_switched_to">Přepnuto na %1$s %2$s.</string>
|
||||
<string name="drivers_switched_to_restart">Přepnuto na %1$s %2$s. Ovladač zobrazení se použije po restartu DroidDecku.</string>
|
||||
<string name="drivers_using">Používá se %1$s %2$s.</string>
|
||||
<string name="drivers_using_restart">Používá se %1$s %2$s. Ovladač zobrazení se použije po restartu DroidDecku.</string>
|
||||
<string name="drivers_using_quest">Používá se přibalený obrazový ovladač pro Quest a výchozí prostředí.</string>
|
||||
<string name="drivers_using_quest_restart">Používá se přibalený obrazový ovladač pro Quest a výchozí prostředí. Obrazový ovladač platí po restartu DroidDeck.</string>
|
||||
<string name="drivers_not_a_driver">Toto není ZIP s ovladačem: ani ovladač AdrenoTools, ani Turnip -Linux, ani balíček Android + Linux</string>
|
||||
<string name="drivers_import_failed">Import se nezdařil: %1$s</string>
|
||||
<string name="drivers_imported_unnamed">Importovaný ovladač</string>
|
||||
@@ -1706,7 +1731,7 @@
|
||||
<string name="adbpair_done_text">Klepnutím se vrátíte do DroidDecku. Bezdrátové ladění teď můžete vypnout.</string>
|
||||
<string name="adbpair_title_failed">Nastavení bezdrátového ladění bylo zastaveno</string>
|
||||
|
||||
<string name="selfinst_http">Stahování odpovědělo HTTP %1$d</string>
|
||||
<string name="selfinst_download_failed">Stahování selhalo</string>
|
||||
<string name="selfinst_size_mismatch">Velikost stahování neodpovídala zveřejněnému APK a bylo zahozeno - zkuste to znovu</string>
|
||||
<string name="selfinst_checksum_mismatch">Stahování neodpovídalo svému kontrolnímu součtu a bylo zahozeno - zkuste to znovu</string>
|
||||
<string name="selfinst_wrong_package">Aktualizace je pro balíček Androidu %1$s, ne %2$s</string>
|
||||
@@ -1804,7 +1829,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">Byl dosažen limit požadavků GitHubu - zkuste to později</string>
|
||||
<string name="turnip_github_http">GitHub odpověděl HTTP %1$d</string>
|
||||
<string name="turnip_download_http">stahování odpovědělo HTTP %1$d</string>
|
||||
<string name="turnip_download_failed">Stahování selhalo</string>
|
||||
<string name="turnip_list_predates_checksums">Tento seznam ovladačů předchází kontrolním součtům - obnovte ho a zkuste to znovu</string>
|
||||
<string name="turnip_checksum_mismatch">Neshoda kontrolního součtu - stahování bylo zahozeno</string>
|
||||
<string name="turnip_not_adrenotools">Není ovladač AdrenoTools: %1$s</string>
|
||||
@@ -1841,7 +1866,7 @@
|
||||
<string name="phantom_instructions_fix">%1$s Pokud to není možné, připojte toto zařízení k počítači s ADB a spusťte příkaz níže. Vraťte se sem a zkontrolujte to znovu.</string>
|
||||
<string name="phantom_instructions_off">Relace Steamu se mohou spouštět.</string>
|
||||
<string name="phantom_instructions_not_applicable">Tato verze Androidu tento limit nepoužívá.</string>
|
||||
<string name="phantom_fix_toggle">Zapněte „Zakázat omezení procesů potomků“ v Možnostech pro vývojáře a pak je nechte zapnuté.</string>
|
||||
<string name="phantom_fix_toggle">Zapněte „Deaktivace omezení podřízených procesů“ v Možnostech pro vývojáře a pak je nechte zapnuté.</string>
|
||||
<string name="phantom_fix_no_toggle">Tato verze Androidu pro to nemá přepínač. Použijte bezdrátové ladění a změňte to z tohoto zařízení.</string>
|
||||
<string name="upd_block_no_apk">Toto sestavení nemá stažení pro tuto kopii DroidDecku.</string>
|
||||
<string name="upd_block_version_code">Android to nenainstaluje přes toto sestavení, protože jeho versionCode %1$d je nižší než nainstalovaný %2$d.</string>
|
||||
@@ -2073,6 +2098,9 @@
|
||||
<string name="stores_install_title">Instalace %1$s:</string>
|
||||
<string name="stores_target_internal">Interní úložiště</string>
|
||||
<string name="stores_target_free">%1$s volných</string>
|
||||
<string name="stores_target_free_unknown">Volné místo neznámé</string>
|
||||
<string name="stores_storage_insufficient">Málo volného místa: potřeba %1$s, volně jen %2$s</string>
|
||||
<string name="stores_storage_low">Málo místa (volně %1$s, potřeba %2$s)</string>
|
||||
<string name="stores_notification_channel">Stahování z obchodů</string>
|
||||
<string name="stores_notification_channel_hint">Zobrazuje průběh stahování her a udržuje stahování aktivní na pozadí</string>
|
||||
<string name="stores_notification_one">Stahování</string>
|
||||
@@ -2108,6 +2136,21 @@
|
||||
<string name="edit_other_exe">Jiný exe…</string>
|
||||
<string name="edit_remove">Odstranit</string>
|
||||
<string name="art_cover">Obálka</string>
|
||||
<string name="art_wide_cover">Široká obálka</string>
|
||||
<string name="art_adjust_logo">Upravit logo</string>
|
||||
<string name="logo_pos_default">Výchozí</string>
|
||||
<string name="logo_pos_upper_left">Vlevo nahoře</string>
|
||||
<string name="logo_pos_upper_center">Nahoře uprostřed</string>
|
||||
<string name="logo_pos_center">Uprostřed</string>
|
||||
<string name="logo_pos_bottom_left">Vlevo dole</string>
|
||||
<string name="logo_pos_bottom_center">Dole uprostřed</string>
|
||||
<string name="logo_pos_position">Poloha</string>
|
||||
<string name="logo_pos_size">Velikost</string>
|
||||
<string name="logo_pos_save">Uložit</string>
|
||||
<string name="logo_pos_reset">Zpět na výchozí</string>
|
||||
<string name="logo_pos_hint">Kreslí se přes pozadí hry, jako to dělá Steam. Přesná poloha se může lišit o pár pixelů.</string>
|
||||
<string name="logo_pos_no_logo">Tato hra zatím nemá logo. Vyber ho v Logo, ať se tady zobrazí.</string>
|
||||
<string name="logo_pos_failed">Polohu loga se nepodařilo uložit. Zkus to znovu.</string>
|
||||
<string name="art_background">Pozadí</string>
|
||||
<string name="art_logo">Logo</string>
|
||||
<string name="art_icon">Ikona</string>
|
||||
@@ -2120,6 +2163,17 @@
|
||||
<string name="art_pick_image">Vybrat obrázek…</string>
|
||||
<string name="art_pick_title">Vyberte obrázek: %1$s</string>
|
||||
<string name="art_reset">Obnovit na automatické</string>
|
||||
<string name="art_search_sgdb">Hledat na SteamGridDB…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB: %1$s</string>
|
||||
<string name="art_sgdb_search_label">Hledat hru</string>
|
||||
<string name="art_sgdb_other_games">Další výsledky</string>
|
||||
<string name="art_sgdb_none">Žádné výsledky.</string>
|
||||
<string name="art_sgdb_no_key">SteamGridDB vyžaduje API klíč. Přidej svůj v Nastavení › Spouštěč.</string>
|
||||
<string name="art_sgdb_bad_key">SteamGridDB nepřijal API klíč. Zkontroluj ho v Nastavení › Spouštěč.</string>
|
||||
<string name="art_sgdb_limit">SteamGridDB teď omezuje požadavky. Zkus to za minutu.</string>
|
||||
<string name="art_sgdb_offline">SteamGridDB není dostupný. Zkontroluj připojení.</string>
|
||||
<string name="art_sgdb_server">Na straně SteamGridDB se něco pokazilo. Zkus to později.</string>
|
||||
<string name="art_sgdb_no_game">SteamGridDB nemá pro tuto hru výsledky. Zkus ji vyhledat.</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="one">%1$d hra přidána</item>
|
||||
<item quantity="few">%1$d hry přidány</item>
|
||||
@@ -2151,6 +2205,45 @@
|
||||
<string name="gpu_bar_auto">Automaticky · %1$s</string>
|
||||
<string name="gpu_separately">Ovladače runtime a displeje samostatně</string>
|
||||
<string name="gpu_use_title">Použít %1$s?</string>
|
||||
<string name="gpu_mode_title">Jak se vybírají ovladače</string>
|
||||
<string name="gpu_mode_auto_detail">Nejlepší dvojice pro tento GPU, vždy aktuální</string>
|
||||
<string name="gpu_mode_pair">Spárovaná dvojice</string>
|
||||
<string name="gpu_mode_pair_detail">Vyber dvojici; nastaví oba ovladače</string>
|
||||
<string name="gpu_mode_mix">Volné kombinování</string>
|
||||
<string name="gpu_mode_mix_detail">Vyber obraz a prostředí zvlášť</string>
|
||||
<string name="gpu_mode_pair_short">Dvojice</string>
|
||||
<string name="gpu_mode_mix_short">Smíšené</string>
|
||||
<string name="gpu_src_auto">AUTO</string>
|
||||
<string name="gpu_src_pair">DVOJICE</string>
|
||||
<string name="gpu_src_bundle">BALÍK</string>
|
||||
<string name="gpu_src_chosen">VYBRÁNO</string>
|
||||
<string name="gpu_src_by_gpu">PODLE GPU</string>
|
||||
<string name="gpu_src_default">VÝCHOZÍ</string>
|
||||
<string name="gpu_display_live">Kreslí teď</string>
|
||||
<string name="gpu_display_pending">Čeká na restart · do té doby kreslí %1$s</string>
|
||||
<string name="gpu_runtime_live">Aktivní</string>
|
||||
<string name="gpu_runtime_pending">Příští relace · tahle zůstává u %1$s</string>
|
||||
<string name="gpu_display_next">Kreslí obrazovku. Platí od příští relace.</string>
|
||||
<string name="gpu_previous_driver">předchozí ovladač</string>
|
||||
<string name="gpu_restart_banner">Restartuj, ať se obrazový ovladač změní.</string>
|
||||
<string name="gpu_restart_blocked">Zastav aktuální relaci a restartuj DroidDeck, ať se obrazový ovladač změní.</string>
|
||||
<string name="gpu_restart_now">Restartovat</string>
|
||||
<string name="gpu_restart_later">Později</string>
|
||||
<string name="gpu_auto_title">Automatika vybrala tuto dvojici pro tvůj %1$s</string>
|
||||
<string name="gpu_auto_builtin_title">Automatika používá vestavěné ovladače pro tvůj %1$s</string>
|
||||
<string name="gpu_auto_reason">Vybráno pro rodinu %1$s. Nové verze se instalují na pozadí.</string>
|
||||
<string name="gpu_pair_title">Vyber dvojici</string>
|
||||
<string name="gpu_pair_hint">Dvojice nastaví obrazový a runtime ovladač společně.</string>
|
||||
<string name="gpu_mix_title">Vyber každý ovladač</string>
|
||||
<string name="gpu_mix_hint">Balík zaplní obě místa. Změny obrazu platí po restartu, změny prostředí od příští relace.</string>
|
||||
<string name="gpu_change_display">Změnit obrazový ovladač</string>
|
||||
<string name="gpu_change_runtime">Změnit runtime ovladač</string>
|
||||
<string name="gpu_autos_pick">VOLBA AUTOMATIKY</string>
|
||||
<string name="gpu_installed">Nainstalováno</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · restart pro použití</string>
|
||||
<string name="gpu_chip_restart">%1$s · čeká na restart</string>
|
||||
<string name="comp_this_device">Toto zařízení</string>
|
||||
<string name="proton_remove_title">Odstranit %1$s?</string>
|
||||
<string name="setup_tab_diagnostics">Diagnostika</string>
|
||||
<string name="setup_ready">Připraveno ke hraní</string>
|
||||
@@ -2162,6 +2255,14 @@
|
||||
<string name="setup_group_look">Vzhled</string>
|
||||
<string name="setup_group_device">Toto zařízení</string>
|
||||
<string name="setup_group_art">Grafika knihovny</string>
|
||||
<string name="setup_group_download">Stahování</string>
|
||||
<string name="setup_download_source">Zdroj</string>
|
||||
<string name="setup_download_source_auto">Automaticky</string>
|
||||
<string name="setup_download_source_direct">Přímo</string>
|
||||
<string name="setup_download_source_mirror">Zrcadlo</string>
|
||||
<string name="setup_download_source_custom">Vlastní prefix</string>
|
||||
<string name="setup_download_mirror">Web</string>
|
||||
<string name="setup_download_prefix">Prefix</string>
|
||||
<string name="flathub_eyebrow">LINUXOVÉ APLIKACE</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">Desktopové aplikace pro Linuxový desktop, otevírané z Desktopu.</string>
|
||||
@@ -2273,12 +2374,9 @@
|
||||
<string name="cardboard_recenter_button">Vycentrovat</string>
|
||||
<string name="cardboard_steamvr">SteamVR (experimentální)</string>
|
||||
<string name="cardboard_steamvr_install">Instalovat</string>
|
||||
<string name="cardboard_steamvr_install_hint">Stáhne SteamVR pro ARM64 od Valve (571 MB).</string>
|
||||
<string name="cardboard_steamvr_installing">Stahování a rozbalování SteamVR…</string>
|
||||
<string name="cardboard_steamvr_start">Spustit</string>
|
||||
<string name="cardboard_steamvr_ready_hint">Spustí SteamVR s vaší hlavou jako headsetem; obraz se přepne na 3D vedle sebe.</string>
|
||||
<string name="cardboard_steamvr_stop">Zastavit</string>
|
||||
<string name="cardboard_steamvr_running_hint">SteamVR běží.</string>
|
||||
<string name="cardboard_steamvr_unavailable">V této relaci není k dispozici.</string>
|
||||
<string name="cardboard_steamvr_failed">SteamVR neodpovídá; zkontrolujte protokol relace.</string>
|
||||
<string name="drawer_component_downloads">Stáhnout komponenty</string>
|
||||
@@ -2292,4 +2390,213 @@
|
||||
<string name="stores_hltb_completionist">Kompletní</string>
|
||||
<string name="stores_hltb_hours">%1$s h</string>
|
||||
<string name="stores_hltb_minutes">%1$d min</string>
|
||||
<string name="display_picture_position">Poloha obrázku</string>
|
||||
<string name="display_position_top">Nahoře</string>
|
||||
<string name="display_position_bottom">Dole</string>
|
||||
<string name="display_position_center">Uprostřed (výchozí)</string>
|
||||
<string name="setup_tab_storage">Úložiště</string>
|
||||
<string name="storage_measuring">Měření úložiště…</string>
|
||||
<string name="storage_measuring_hint">Poprvé to trvá pár sekund.</string>
|
||||
<string name="storage_internal">Interní úložiště</string>
|
||||
<string name="storage_used_of">využito %1$s z %2$s</string>
|
||||
<string name="storage_free">volně %1$s</string>
|
||||
<string name="storage_droiddeck_uses">DroidDeck využívá %1$s. Změřeno %2$s.</string>
|
||||
<string name="storage_rescanning">měření znovu…</string>
|
||||
<string name="storage_other_apps">Ostatní aplikace pro Android</string>
|
||||
<string name="storage_safe_now">Lze uvolnit hned</string>
|
||||
<string name="storage_safe_hint">Nevyužitá prostředí, zbytky, staré logy</string>
|
||||
<string name="storage_clean_up">Čištění…</string>
|
||||
<string name="storage_session_running">Zastav relaci, ať můžeš uklidit nebo něco smazat. Velikosti zůstávají vidět.</string>
|
||||
<string name="storage_where">Kam to jde</string>
|
||||
<string name="storage_games">Hry ze Steamu</string>
|
||||
<string name="storage_game_data">Data her</string>
|
||||
<string name="storage_game_data_hint">Prefixy %1$s · workshop %2$s · shadery %3$s</string>
|
||||
<string name="storage_tools">Proton a prostředí</string>
|
||||
<string name="storage_tools_unused">%1$s nevyužito</string>
|
||||
<string name="storage_stores">GOG, Epic a další obchody</string>
|
||||
<string name="storage_stores_hint">Odeber je v Obchodech</string>
|
||||
<string name="storage_desktop">Aplikace pro plochu</string>
|
||||
<string name="storage_desktop_hint">Aplikace Flatpak a prostředí</string>
|
||||
<string name="storage_system">Systém Linux a klient Steam</string>
|
||||
<string name="storage_system_hint">Potřebné, aby vůbec něco běželo</string>
|
||||
<string name="storage_app_data">Data DroidDeck</string>
|
||||
<string name="storage_app_data_hint">Logy starých relací %1$s · mezipaměť · ovladače</string>
|
||||
<string name="storage_legend_games">Hry</string>
|
||||
<string name="storage_legend_data">Data her</string>
|
||||
<string name="storage_legend_tools">Proton a prostředí</string>
|
||||
<string name="storage_legend_stores">Další obchody</string>
|
||||
<string name="storage_legend_desktop">Aplikace pro plochu</string>
|
||||
<string name="storage_legend_system">Systém Linux</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">Uklidit</string>
|
||||
<string name="storage_clean_up_lede">Nic tady se nedotkne nainstalovaných her ani probíhajících stahování.</string>
|
||||
<string name="storage_nothing_to_clean">Teď není co uklidit.</string>
|
||||
<string name="storage_selected">Vybráno: %1$s · %2$s → uvolní se %3$s</string>
|
||||
<string name="storage_cleaning">Čištění…</string>
|
||||
<string name="storage_free_button">Uvolnit %1$s</string>
|
||||
<string name="storage_freed">Uvolněno %1$s.</string>
|
||||
<string name="storage_item_runtimes">Nevyužitá prostředí Steamu a Proton</string>
|
||||
<string name="storage_item_leftovers">Zbytky po smazaných hrách</string>
|
||||
<string name="storage_item_leftovers_unsure">Zbytky, které mohou obsahovat uložené pozice</string>
|
||||
<string name="storage_item_logs">Logy starých relací</string>
|
||||
<string name="storage_item_logs_detail">Logy aktuální relace zůstávají</string>
|
||||
<string name="storage_item_custom_detail">Žádná hra ho nepoužívá. Pokud ho budeš potřebovat, nainstaluj ho znovu v Komponentách.</string>
|
||||
<string name="storage_item_shaders">Mezipaměť shaderů nainstalovaných her</string>
|
||||
<string name="storage_item_shaders_detail">Během hraní se znovu postaví. První minuty každé hry budou zase škubat.</string>
|
||||
<string name="storage_tag_safe">Bezpečné</string>
|
||||
<string name="storage_tag_unused">Nevyužité</string>
|
||||
<string name="storage_tag_stutter">Škubání</string>
|
||||
<string name="storage_tag_saves">Zkontroluj uložené pozice</string>
|
||||
<string name="storage_tag_default">Výchozí</string>
|
||||
<string name="storage_tag_required">Potřebné</string>
|
||||
<string name="storage_tag_not_used">Nevyužité</string>
|
||||
<string name="storage_back">Úložiště</string>
|
||||
<string name="storage_games_with_data">%1$s s jejich daty</string>
|
||||
<string name="storage_sort_largest">Největší</string>
|
||||
<string name="storage_sort_least_played">Nejméně hrané</string>
|
||||
<string name="storage_sort_sd">Na SD kartě</string>
|
||||
<string name="storage_no_games">Nejsou tu žádné hry.</string>
|
||||
<string name="storage_played">hráno %1$s</string>
|
||||
<string name="storage_never_played">Nikdy nehráno</string>
|
||||
<string name="storage_game_parts">Hra %1$s · data %2$s</string>
|
||||
<string name="storage_uninstall">Odinstalovat</string>
|
||||
<string name="storage_games_note">Odinstalování otevře okno Steamu, aby to Steam zaznamenal. Pro přesun hry na SD kartu použij Steam › Nastavení › Úložiště.</string>
|
||||
<string name="storage_uninstall_title">Odinstalovat %1$s?</string>
|
||||
<string name="storage_uninstall_body">Steam se otevře a požádá o potvrzení. Tím se uvolní %1$s hry. Prefix zůstane, dokud neuklidíš zbytky, a uložené pozice ve Steam Cloud zůstanou na účtu.</string>
|
||||
<string name="storage_open_steam">Otevřít Steam</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s nevyužito</string>
|
||||
<string name="storage_remove_unused">Odebrat nevyužité · %1$s</string>
|
||||
<string name="storage_in_use">Používá se</string>
|
||||
<string name="storage_not_used">Nevyužité</string>
|
||||
<string name="storage_remove">Odebrat</string>
|
||||
<string name="storage_remove_title">Odebrat %1$s?</string>
|
||||
<string name="storage_remove_body">Uvolní %1$s. Steam to znovu stáhne, pokud to nějaká hra bude potřebovat.</string>
|
||||
<string name="storage_tool_fex_runtime">Potřebné pro hry pro Linux x86 (FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">Potřebné pro Proton pro ARM64</string>
|
||||
<string name="storage_tool_redist">Instalace VC++ a DirectX, které hry potřebují</string>
|
||||
<string name="storage_tool_fex">Spouští hry pro Linux x86</string>
|
||||
<string name="storage_tool_steam">Nástroj Steamu</string>
|
||||
<string name="storage_tool_old_runtime">DroidDeck spouští hry bez tohoto prostředí</string>
|
||||
<string name="storage_tool_x86_proton">Proton x86, který DroidDeck nedokáže spustit</string>
|
||||
<string name="storage_tool_custom_default">Výchozí pro všechny hry</string>
|
||||
<string name="storage_tool_custom_unused">Žádná hra ho nepoužívá</string>
|
||||
<string name="storage_tool_custom_unknown">Nastavení Steamu nejde přečíst, takže zůstává</string>
|
||||
<string name="storage_cleanup_busy">Počkej, až se dokončí čištění úložiště</string>
|
||||
<string name="storage_low_title">Volně jen %1$s</string>
|
||||
<string name="storage_low_body">Steam potřebuje místo na aktualizace a mezipaměť shaderů. Pod 10 GB mohou aktualizace her selhat v polovině.</string>
|
||||
<string name="storage_low_safe">%1$s lze bezpečně uvolnit.</string>
|
||||
<string name="storage_free_up">Uvolnit místo</string>
|
||||
<string name="storage_play_anyway">Přesto hrát</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d nainstalovaná</item>
|
||||
<item quantity="other">%d nainstalovaných</item>
|
||||
<item quantity="few">%d nainstalované</item>
|
||||
<item quantity="many">%d nainstalované</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d na SD kartě</item>
|
||||
<item quantity="other">%d na SD kartě</item>
|
||||
<item quantity="few">%d na SD kartě</item>
|
||||
<item quantity="many">%d na SD kartě</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">%d nástroj</item>
|
||||
<item quantity="other">%d nástrojů</item>
|
||||
<item quantity="few">%d nástroje</item>
|
||||
<item quantity="many">%d nástroje</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">Prázdné složky prefixu a mezipaměť shaderů z %d hry</item>
|
||||
<item quantity="other">Prázdné složky prefixu a mezipaměť shaderů z %d her</item>
|
||||
<item quantity="few">Prázdné složky prefixu a mezipaměť shaderů ze %d her</item>
|
||||
<item quantity="many">Prázdné složky prefixu a mezipaměť shaderů z %d her</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">Prefix %d hry stále obsahuje soubory, včetně místních uložených pozic. Pokud je potřebuješ, nejdřív si udělej kopii.</item>
|
||||
<item quantity="other">Prefixy %d her stále obsahují soubory, včetně místních uložených pozic. Pokud je potřebuješ, nejdřív si udělej kopii.</item>
|
||||
<item quantity="few">Prefixy %d her stále obsahují soubory, včetně místních uložených pozic. Pokud je potřebuješ, nejdřív si udělej kopii.</item>
|
||||
<item quantity="many">Prefixy %d her stále obsahují soubory, včetně místních uložených pozic. Pokud je potřebuješ, nejdřív si udělej kopii.</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">%d hra</item>
|
||||
<item quantity="other">%d her</item>
|
||||
<item quantity="few">%d hry</item>
|
||||
<item quantity="many">%d hry</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">Výchozí Proton DroidDeck</string>
|
||||
<string name="storage_tool_proton">Volba Proton 11 v DroidDeck</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">Používá %d hra</item>
|
||||
<item quantity="other">Používá %d her</item>
|
||||
<item quantity="few">Používají %d hry</item>
|
||||
<item quantity="many">Používá %d hry</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">Komponenty</string>
|
||||
<string name="storage_components">Komponenty</string>
|
||||
<string name="storage_item_components">Nevyužité komponenty</string>
|
||||
<string name="storage_component_original">Originál %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">Uvolní %1$s. Vrací jen verzi Protonu, která už není nainstalovaná.</string>
|
||||
<string name="storage_leftovers_unchecked">Nezkontrolováno: manifest aplikace Steamu nejde přečíst, takže prefixy smazaných her zůstanou, dokud to nepůjde.</string>
|
||||
<string name="storage_failed">Selhalo: %1$s</string>
|
||||
<string name="storage_desktop_shared">%1$s sdíleno</string>
|
||||
<string name="storage_data_installed">Nainstalované hry</string>
|
||||
<string name="storage_data_parts">Prefix %1$s · shadery %2$s · workshop %3$s</string>
|
||||
<string name="storage_clear_shaders">Vymazat shadery</string>
|
||||
<string name="storage_data_leftovers">Smazané hry</string>
|
||||
<string name="storage_steam_app">Aplikace Steam %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">Prefix stále obsahuje soubory, možná místní uložené pozice</string>
|
||||
<string name="storage_leftover_confirm">Vymaže Wine prefix a mezipaměť shaderů (%1$s). Nejdřív si udělej kopii uložených pozic, které potřebuješ; uložené pozice ve Steam Cloud zůstanou na účtu.</string>
|
||||
<string name="storage_data_other">Zachováno</string>
|
||||
<string name="storage_data_other_title">Hry mimo Steam a sdílený prefix Steamu</string>
|
||||
<string name="storage_data_other_detail">Prefixy her přidaných přes obchody nebo ručně</string>
|
||||
<string name="storage_components_packages">Stažené balíky</string>
|
||||
<string name="storage_components_originals">Zachované originály</string>
|
||||
<string name="storage_components_none">Nejsou stažené žádné balíky komponent.</string>
|
||||
<string name="storage_component_in_use">Vybráno pro Proton nebo hru</string>
|
||||
<string name="storage_component_unused">Nikde nevybráno</string>
|
||||
<string name="storage_original_kept">Vrací nainstalovaný Proton</string>
|
||||
<string name="storage_original_old">Verze Protonu, která už není nainstalovaná</string>
|
||||
<string name="storage_tag_in_use">Používá se</string>
|
||||
<string name="storage_components_note">Používané komponenty zůstávají zamčené. Vybírej je v Komponentách; co tady odebereš, tam můžeš znovu stáhnout.</string>
|
||||
<string name="storage_component_confirm">Uvolní %1$s. Balíky, které jsi importoval sám, už znovu stáhnout nelze.</string>
|
||||
<string name="storage_desktop_detail">%1$s pro aplikace Flatpak a co sdílejí</string>
|
||||
<string name="storage_desktop_apps_title">Aplikace</string>
|
||||
<string name="storage_desktop_none">Nejsou nainstalované žádné aplikace Flatpak.</string>
|
||||
<string name="storage_desktop_shared_title">Sdílené aplikacemi</string>
|
||||
<string name="storage_desktop_runtimes">Prostředí</string>
|
||||
<string name="storage_desktop_runtimes_detail">Knihovny, na kterých aplikace běží; nevyužité zmizí přes Odebrat nevyužité</string>
|
||||
<string name="storage_desktop_cache">Úložiště stahování</string>
|
||||
<string name="storage_desktop_cache_detail">Objektové úložiště a stažená mezipaměť Flatpaku</string>
|
||||
<string name="storage_desktop_prune">Odebrat nevyužité</string>
|
||||
<string name="storage_desktop_prune_body">Odebere prostředí, která žádná nainstalovaná aplikace nepotřebuje, a uklidí objektové úložiště Flatpaku. Nainstalované aplikace dál fungují.</string>
|
||||
<string name="storage_flatpak_uninstall_body">Odebere aplikaci (%1$s) a prostředí, která nikdo jiný nepoužívá. Data v domovské složce zůstanou.</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">%d balík</item>
|
||||
<item quantity="other">%d balíků</item>
|
||||
<item quantity="few">%d balíky</item>
|
||||
<item quantity="many">%d balíku</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">%d aplikace</item>
|
||||
<item quantity="other">%d aplikací</item>
|
||||
<item quantity="few">%d aplikace</item>
|
||||
<item quantity="many">%d aplikace</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">Úvodní video</string>
|
||||
<string name="boot_movie_video">Video</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d fps</string>
|
||||
<string name="boot_movie_note">60 fps vypadá plynuleji, ale při spouštění Steamu víc zatěžuje procesor. Úvodní video vybrané přímo v nastavení Steamu má přednost.</string>
|
||||
<string name="boot_movie_sound">Zvuk</string>
|
||||
<string name="boot_movie_custom">Vlastní video</string>
|
||||
<string name="boot_movie_custom_hint">MP4, MOV, MKV nebo WebM. Jiné formáty a soubory nad %1$d MB se v zařízení převedou na WebM 720p, nejvýše %2$d sekund.</string>
|
||||
<string name="boot_movie_import">Importovat</string>
|
||||
<string name="boot_movie_remove">Odebrat</string>
|
||||
<string name="boot_movie_pick">Vyberte úvodní video</string>
|
||||
<string name="boot_movie_imported">%1$s je nyní úvodní video</string>
|
||||
<string name="boot_movie_imported_silent">%1$s je nyní úvodní video, bez zvuku</string>
|
||||
<string name="boot_movie_too_long">Videa k převodu mohou mít nejvýše %1$d sekund</string>
|
||||
<string name="boot_movie_converting">Převádění %1$d %%</string>
|
||||
<string name="boot_movie_converting_started">Převádí se %1$s pro Steam. Nechte DroidDeck otevřený.</string>
|
||||
<string name="boot_movie_unsupported">Toto zařízení video nedokáže dekódovat.</string>
|
||||
<string name="boot_movie_import_failed">Video se nepodařilo importovat</string>
|
||||
</resources>
|
||||
@@ -278,6 +278,9 @@
|
||||
<string name="mode_suspend_steam_note">Auto sætter på pause og genoptager, når den vises. Native forbereder Steam til dvale først. Manuel venter på Genoptag. Aldrig fortsætter med at køre.</string>
|
||||
<string name="mode_background_downloads">Fortsæt downloads</string>
|
||||
<string name="mode_background_downloads_hint">Udskyder baggrundspause, mens Steam aktivt downloader.</string>
|
||||
<string name="mode_zram">zRAM-komprimering</string>
|
||||
<string name="mode_zram_hint">Mens et spil kører, komprimeres Steam-klientens hukommelse til enhedens zram, så spillet får mere RAM. Gælder næste gang Steam starter.</string>
|
||||
<string name="mode_zram_unsupported">Ikke tilgængeligt: enheden har ingen zram eller swap, eller kernen er ældre end 5.4.</string>
|
||||
<string name="mode_suspend_note">Auto sætter på pause i baggrunden og genoptager, når den vises. Manuel venter på Genoptag. Aldrig fortsætter med at køre.</string>
|
||||
<string name="mode_startup">Opstart</string>
|
||||
<string name="mode_steam_startup">Start Steam med DroidDeck</string>
|
||||
@@ -575,6 +578,10 @@
|
||||
<string name="drawer_cas">Skarphed (CAS)</string>
|
||||
<string name="drawer_cas_level">Skarphedsniveau</string>
|
||||
<string name="drawer_fake_hdr">Falsk HDR</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">Start SteamVR</string>
|
||||
<string name="steamvr_checking">Tjekker SteamVR…</string>
|
||||
<string name="drawer_deband">Debanding</string>
|
||||
<string name="drawer_deband_strength">Debanding-styrke</string>
|
||||
<string name="drawer_brightness">Lysstyrke</string>
|
||||
@@ -592,6 +599,11 @@
|
||||
<string name="drawer_texture_sharpness_note">En negativ mip-bias: skarpere teksturer, lidt flimmer. Auto følger, hvor meget sessionen forstørres til skærmen.</string>
|
||||
<string name="drawer_touch_auto">Auto (%1$s)</string>
|
||||
<string name="drawer_keyboard">Tastatur</string>
|
||||
<string name="drawer_latency">Forsinkelse</string>
|
||||
<string name="drawer_low_latency_controller">Controller med lav forsinkelse</string>
|
||||
<string name="drawer_low_latency_controller_hint">Controlleren rapporterer ved 1000 Hz i stedet for 250 Hz. Lidt mere CPU.</string>
|
||||
<string name="drawer_low_latency_touch">Berøring med lav forsinkelse</string>
|
||||
<string name="drawer_low_latency_touch_hint">Berøringer springer billedsamling over; Steams berøringsstyring kører ved 1000 Hz. Lidt mere CPU.</string>
|
||||
<string name="drawer_pc_keyboard">PC-tastatur</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">Anden skærm</string>
|
||||
@@ -683,6 +695,14 @@
|
||||
<string name="setup_saved_logs">Gemte sessionslogs</string>
|
||||
<string name="setup_saved_logs_hint">Gemmer de nyeste %1$d</string>
|
||||
<string name="setup_clear_logs">Ryd logs</string>
|
||||
<string name="setup_reset_settings">Nulstil alle indstillinger</string>
|
||||
<string name="setup_reset_settings_hint">Sæt hver indstilling tilbage til standardværdien</string>
|
||||
<string name="setup_reset_settings_button">Nulstil</string>
|
||||
<string name="setup_reset_settings_title">Nulstil alle indstillinger?</string>
|
||||
<string name="setup_reset_settings_body">Indstillinger for billede, ydeevne, controller, tema, sprog og launcher går tilbage til standard. Dine spil, Steam-login, butikskonti, hentede drivere og lagringsmapper bevares.</string>
|
||||
<string name="setup_reset_settings_profiles">Nulstil også spilprofiler (Proton, FEX, komponenter og kørselsmiljø pr. spil samt kørselsmiljøet for Standard-profilen)</string>
|
||||
<string name="setup_reset_settings_session_running">Stop sessionen, før du nulstiller indstillinger.</string>
|
||||
<string name="setup_reset_settings_partial">Indstillingerne blev nulstillet, men ikke alle. Detaljerne står i loggen.</string>
|
||||
<string name="setup_offline">Offlinetilstand</string>
|
||||
<string name="setup_sign_in_first">Log ind på Steam først</string>
|
||||
<string name="setup_launcher">Launcher</string>
|
||||
@@ -843,7 +863,7 @@
|
||||
<string name="gate_eyebrow">Steam</string>
|
||||
<string name="gate_lede">Android lukker apps, der starter mange baggrundsprocesser, og Steam starter dusinvis. Indtil dette ændres, lukker spil uden en fejlmeddelelse.</string>
|
||||
<string name="gate_fix_for_me">Ret det for mig</string>
|
||||
<string name="gate_toggle_steps">Slå “Deaktiver begrænsninger for underprocesser” til, og kom så tilbage. Lad Udviklerindstillinger stå til, ellers slås kontakten fra igen.</string>
|
||||
<string name="gate_toggle_steps">Slå “Deaktiver begrænsningerne for underprocesser” til, og kom så tilbage. Lad Udviklerindstillinger stå til, ellers slås kontakten fra igen.</string>
|
||||
<string name="gate_open_dev">Åbn Udviklerindstillinger</string>
|
||||
<string name="gate_no_toggle">Denne Android-version har ingen kontakt til det, så DroidDeck ændrer det via Trådløs fejlretning. Tager omkring et minut, ingen computer nødvendig.</string>
|
||||
<string name="gate_needs_wifi">Trådløs fejlretning kræver Wi-Fi. Alle netværk fungerer; det kræver ikke internet.</string>
|
||||
@@ -1009,7 +1029,6 @@
|
||||
<string name="gpu_set_with_runtime">Angives sammen med runtimedriveren: %1$s dækker begge.</string>
|
||||
<string name="gpu_runtime_hint">Bruges af spil og skrivebordet.</string>
|
||||
<string name="gpu_runtime_default">Runtime-standard</string>
|
||||
<string name="gpu_display_hint">Tegner billeder til skærmen. Gælder, når DroidDeck genstarter.</string>
|
||||
<string name="gpu_auto_by_gpu">Auto – valgt efter GPU</string>
|
||||
<string name="gpu_manage">Download, importer eller slet…</string>
|
||||
<string name="gpu_in_use_caps">I BRUG</string>
|
||||
@@ -1239,10 +1258,16 @@
|
||||
<string name="drivers_downloading_pair">Downloader %1$s %2$s…</string>
|
||||
<string name="drivers_updated_to">Opdateret til %1$s %2$s.</string>
|
||||
<string name="drivers_updated_to_restart">Opdateret til %1$s %2$s. Skærmdriveren træder i kraft, når DroidDeck genstarter.</string>
|
||||
<string name="drivers_auto_waits_for_session">Driveropdateringer venter, til sessionen stoppes.</string>
|
||||
<string name="drivers_auto_waits_for_network">%1$s %2$s hentes, når du forlader mobildata og batterisparetilstand.</string>
|
||||
<string name="drivers_skipping_bad">%1$s %2$s startede ingen session her, så Automatisk holder sig til de drivere, der gjorde.</string>
|
||||
<string name="drivers_rolled_back">%1$s startede ingen session to gange, så Automatisk gik tilbage til de drivere, der virkede.</string>
|
||||
<string name="drivers_switched_to">Skiftet til %1$s %2$s.</string>
|
||||
<string name="drivers_switched_to_restart">Skiftet til %1$s %2$s. Skærmdriveren træder i kraft, når DroidDeck genstarter.</string>
|
||||
<string name="drivers_using">Bruger %1$s %2$s.</string>
|
||||
<string name="drivers_using_restart">Bruger %1$s %2$s. Skærmdriveren træder i kraft, når DroidDeck genstarter.</string>
|
||||
<string name="drivers_using_quest">Den medfølgende billeddriver til Quest og standardkørselsmiljøet bruges.</string>
|
||||
<string name="drivers_using_quest_restart">Den medfølgende billeddriver til Quest og standardkørselsmiljøet bruges. Billeddriveren gælder efter en genstart af DroidDeck.</string>
|
||||
<string name="drivers_not_a_driver">Ikke en driver-zip: hverken en AdrenoTools-driver, en -Linux-Turnip eller en Android + Linux-bundle</string>
|
||||
<string name="drivers_import_failed">Import mislykkedes: %1$s</string>
|
||||
<string name="drivers_imported_unnamed">Importeret driver</string>
|
||||
@@ -1664,7 +1689,7 @@
|
||||
<string name="adbpair_done_text">Tryk for at gå tilbage til DroidDeck. Du kan slukke Trådløs fejlretning nu.</string>
|
||||
<string name="adbpair_title_failed">Opsætning af Trådløs fejlretning stoppet</string>
|
||||
|
||||
<string name="selfinst_http">Downloadet svarede HTTP %1$d</string>
|
||||
<string name="selfinst_download_failed">Hentningen mislykkedes</string>
|
||||
<string name="selfinst_size_mismatch">Downloadets størrelse passede ikke med den udgivne APK og blev kasseret - forsøg igen</string>
|
||||
<string name="selfinst_checksum_mismatch">Downloadet matchede ikke sin tjeksum og blev kasseret - forsøg igen</string>
|
||||
<string name="selfinst_wrong_package">Opdateringen er til Android-pakken %1$s, ikke %2$s</string>
|
||||
@@ -1762,7 +1787,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">GitHubs hastighedsgrænse blev nået - forsøg igen senere</string>
|
||||
<string name="turnip_github_http">GitHub svarede HTTP %1$d</string>
|
||||
<string name="turnip_download_http">downloadet svarede HTTP %1$d</string>
|
||||
<string name="turnip_download_failed">Hentningen mislykkedes</string>
|
||||
<string name="turnip_list_predates_checksums">Denne driverliste er ældre end tjeksummer - opdater den, og forsøg igen</string>
|
||||
<string name="turnip_checksum_mismatch">Tjeksum stemmer ikke - downloadet blev kasseret</string>
|
||||
<string name="turnip_not_adrenotools">Ikke en AdrenoTools-driver: %1$s</string>
|
||||
@@ -1799,7 +1824,7 @@
|
||||
<string name="phantom_instructions_fix">%1$s Hvis det ikke er muligt, så forbind enheden til en computer med ADB, og køre kommandoen nedenfor. Kom tilbage hertil, og tjek igen.</string>
|
||||
<string name="phantom_instructions_off">Steam-sessioner kan starte.</string>
|
||||
<string name="phantom_instructions_not_applicable">Denne Android-version bruger ikke denne grænse.</string>
|
||||
<string name="phantom_fix_toggle">Tænd „Disable child process restrictions“ i Udvikleremuligheder, og lad Udvikleremuligheder være tændt bagefter.</string>
|
||||
<string name="phantom_fix_toggle">Tænd „Deaktiver begrænsningerne for underprocesser“ i Udvikleremuligheder, og lad Udvikleremuligheder være tændt bagefter.</string>
|
||||
<string name="phantom_fix_no_toggle">Denne Android-version har ingen kontakt til det. Brug Trådløs fejlretning til at ændre det fra denne enhed.</string>
|
||||
<string name="upd_block_no_apk">Denne version har ingen download til denne kopi af DroidDeck.</string>
|
||||
<string name="upd_block_version_code">Android installerer den ikke over denne version, fordi dens versionCode %1$d er under den installerede %2$d.</string>
|
||||
@@ -2031,6 +2056,9 @@
|
||||
<string name="stores_install_title">Installer %1$s:</string>
|
||||
<string name="stores_target_internal">Intern</string>
|
||||
<string name="stores_target_free">%1$s fri</string>
|
||||
<string name="stores_target_free_unknown">Ledig plads ukendt</string>
|
||||
<string name="stores_storage_insufficient">For lidt ledig plads: der kræves %1$s, kun %2$s ledig</string>
|
||||
<string name="stores_storage_low">For lidt plads (%1$s ledig, %2$s kræves)</string>
|
||||
<string name="stores_notification_channel">Butiksdownloads</string>
|
||||
<string name="stores_notification_channel_hint">Viser fremskridt for spil-downloads og holder downloads kørende i baggrunden</string>
|
||||
<string name="stores_notification_one">Downloader</string>
|
||||
@@ -2066,6 +2094,21 @@
|
||||
<string name="edit_other_exe">Anden exe…</string>
|
||||
<string name="edit_remove">Fjern</string>
|
||||
<string name="art_cover">Omslag</string>
|
||||
<string name="art_wide_cover">Bredt omslag</string>
|
||||
<string name="art_adjust_logo">Tilpas logo</string>
|
||||
<string name="logo_pos_default">Standard</string>
|
||||
<string name="logo_pos_upper_left">Øverst til venstre</string>
|
||||
<string name="logo_pos_upper_center">Øverst i midten</string>
|
||||
<string name="logo_pos_center">Midten</string>
|
||||
<string name="logo_pos_bottom_left">Nederst til venstre</string>
|
||||
<string name="logo_pos_bottom_center">Nederst i midten</string>
|
||||
<string name="logo_pos_position">Placering</string>
|
||||
<string name="logo_pos_size">Størrelse</string>
|
||||
<string name="logo_pos_save">Gem</string>
|
||||
<string name="logo_pos_reset">Tilbage til standard</string>
|
||||
<string name="logo_pos_hint">Tegnes oven på spillets baggrund, som Steam gør. Den præcise placering kan afvige et par pixels.</string>
|
||||
<string name="logo_pos_no_logo">Dette spil har endnu ikke et logo. Vælg et under Logo for at se det her.</string>
|
||||
<string name="logo_pos_failed">Logopladeringen kunne ikke gemmes. Prøv igen.</string>
|
||||
<string name="art_background">Baggrund</string>
|
||||
<string name="art_logo">Logo</string>
|
||||
<string name="art_icon">Ikon</string>
|
||||
@@ -2078,6 +2121,17 @@
|
||||
<string name="art_pick_image">Vælg billede…</string>
|
||||
<string name="art_pick_title">Vælg et billede: %1$s</string>
|
||||
<string name="art_reset">Nulstil til automatisk</string>
|
||||
<string name="art_search_sgdb">Søg på SteamGridDB…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB: %1$s</string>
|
||||
<string name="art_sgdb_search_label">Søg et spil</string>
|
||||
<string name="art_sgdb_other_games">Andre resultater</string>
|
||||
<string name="art_sgdb_none">Ingen resultater.</string>
|
||||
<string name="art_sgdb_no_key">SteamGridDB kræver en API-nøgle. Tilføj din under Indstillinger › Launcher.</string>
|
||||
<string name="art_sgdb_bad_key">SteamGridDB accepterede ikke API-nøglen. Tjek den under Indstillinger › Launcher.</string>
|
||||
<string name="art_sgdb_limit">SteamGridDB begrænser antallet af forespørgsler lige nu. Prøv igen om et minut.</string>
|
||||
<string name="art_sgdb_offline">SteamGridDB kan ikke nås. Tjek forbindelsen.</string>
|
||||
<string name="art_sgdb_server">Noget gik galt hos SteamGridDB. Prøv igen senere.</string>
|
||||
<string name="art_sgdb_no_game">SteamGridDB har ingen resultater for dette spil. Prøv at søge på det.</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="one">%1$d spil tilføjet</item>
|
||||
<item quantity="other">%1$d spil tilføjet</item>
|
||||
@@ -2105,6 +2159,45 @@
|
||||
<string name="gpu_bar_auto">Auto · %1$s</string>
|
||||
<string name="gpu_separately">Runtime- og skærmdrivere separat</string>
|
||||
<string name="gpu_use_title">Brug %1$s?</string>
|
||||
<string name="gpu_mode_title">Sådan vælges drivere</string>
|
||||
<string name="gpu_mode_auto_detail">Det bedste par til denne GPU, altid opdateret</string>
|
||||
<string name="gpu_mode_pair">Matchet par</string>
|
||||
<string name="gpu_mode_pair_detail">Vælg et par; sætter begge drivere</string>
|
||||
<string name="gpu_mode_mix">Fri blanding</string>
|
||||
<string name="gpu_mode_mix_detail">Vælg billede og kørselsmiljø hver for sig</string>
|
||||
<string name="gpu_mode_pair_short">Par</string>
|
||||
<string name="gpu_mode_mix_short">Blandet</string>
|
||||
<string name="gpu_src_auto">AUTO</string>
|
||||
<string name="gpu_src_pair">PAR</string>
|
||||
<string name="gpu_src_bundle">PAKKE</string>
|
||||
<string name="gpu_src_chosen">VALGT</string>
|
||||
<string name="gpu_src_by_gpu">PR. GPU</string>
|
||||
<string name="gpu_src_default">STANDARD</string>
|
||||
<string name="gpu_display_live">Tegner nu</string>
|
||||
<string name="gpu_display_pending">Kræver genstart · indtil da tegner %1$s</string>
|
||||
<string name="gpu_runtime_live">Aktiv</string>
|
||||
<string name="gpu_runtime_pending">Næste session · denne beholder %1$s</string>
|
||||
<string name="gpu_display_next">Tegner skærmen. Gælder fra næste session.</string>
|
||||
<string name="gpu_previous_driver">den forrige driver</string>
|
||||
<string name="gpu_restart_banner">Genstart for at skifte billeddriver.</string>
|
||||
<string name="gpu_restart_blocked">Stop den aktuelle session og genstart DroidDeck for at skifte billeddriver.</string>
|
||||
<string name="gpu_restart_now">Genstart nu</string>
|
||||
<string name="gpu_restart_later">Senere</string>
|
||||
<string name="gpu_auto_title">Automatisk valgte dette par til din %1$s</string>
|
||||
<string name="gpu_auto_builtin_title">Automatisk bruger de indbyggede drivere til din %1$s</string>
|
||||
<string name="gpu_auto_reason">Valgt til %1$s-familien. Nye versioner installeres i baggrunden.</string>
|
||||
<string name="gpu_pair_title">Vælg et par</string>
|
||||
<string name="gpu_pair_hint">Et par sætter billeddriveren og runtimedriveren sammen.</string>
|
||||
<string name="gpu_mix_title">Vælg hver driver</string>
|
||||
<string name="gpu_mix_hint">En pakke udfylder begge pladser. Billedændringer gælder efter en genstart, kørselsændringer fra næste session.</string>
|
||||
<string name="gpu_change_display">Skift billeddriver</string>
|
||||
<string name="gpu_change_runtime">Skift runtimedriver</string>
|
||||
<string name="gpu_autos_pick">AUTOMATISK VALG</string>
|
||||
<string name="gpu_installed">Installeret</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · genstart for at anvende</string>
|
||||
<string name="gpu_chip_restart">%1$s · kræver genstart</string>
|
||||
<string name="comp_this_device">Denne enhed</string>
|
||||
<string name="proton_remove_title">Fjern %1$s?</string>
|
||||
<string name="setup_tab_diagnostics">Diagnostik</string>
|
||||
<string name="setup_ready">Klar til at spille</string>
|
||||
@@ -2116,6 +2209,14 @@
|
||||
<string name="setup_group_look">Udseende</string>
|
||||
<string name="setup_group_device">Denne enhed</string>
|
||||
<string name="setup_group_art">Biblioteksgrafik</string>
|
||||
<string name="setup_group_download">Hentning</string>
|
||||
<string name="setup_download_source">Kilde</string>
|
||||
<string name="setup_download_source_auto">Automatisk</string>
|
||||
<string name="setup_download_source_direct">Direkte</string>
|
||||
<string name="setup_download_source_mirror">Spejl</string>
|
||||
<string name="setup_download_source_custom">Eget præfiks</string>
|
||||
<string name="setup_download_mirror">Websted</string>
|
||||
<string name="setup_download_prefix">Præfiks</string>
|
||||
<string name="flathub_eyebrow">LINUX-APPS</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">Desktopapps til Linux-desktoppen, åbnet fra Desktop.</string>
|
||||
@@ -2223,12 +2324,9 @@
|
||||
<string name="cardboard_recenter_button">Centrer</string>
|
||||
<string name="cardboard_steamvr">SteamVR (eksperimentel)</string>
|
||||
<string name="cardboard_steamvr_install">Installer</string>
|
||||
<string name="cardboard_steamvr_install_hint">Downloader Valves SteamVR til ARM64 (571 MB).</string>
|
||||
<string name="cardboard_steamvr_installing">Downloader og udpakker SteamVR…</string>
|
||||
<string name="cardboard_steamvr_start">Start</string>
|
||||
<string name="cardboard_steamvr_ready_hint">Kører SteamVR med dit hoved som headset; billedet skifter til side-by-side-3D.</string>
|
||||
<string name="cardboard_steamvr_stop">Stop</string>
|
||||
<string name="cardboard_steamvr_running_hint">SteamVR kører.</string>
|
||||
<string name="cardboard_steamvr_unavailable">Ikke tilgængelig i denne session.</string>
|
||||
<string name="cardboard_steamvr_failed">SteamVR svarede ikke; se sessionsloggen.</string>
|
||||
<string name="drawer_component_downloads">Download komponenter</string>
|
||||
@@ -2242,4 +2340,195 @@
|
||||
<string name="stores_hltb_completionist">Komplettering</string>
|
||||
<string name="stores_hltb_hours">%1$s t</string>
|
||||
<string name="stores_hltb_minutes">%1$d min</string>
|
||||
<string name="display_picture_position">Billedets placering</string>
|
||||
<string name="display_position_top">Øverst</string>
|
||||
<string name="display_position_bottom">Nederst</string>
|
||||
<string name="display_position_center">Midten (standard)</string>
|
||||
<string name="setup_tab_storage">Lager</string>
|
||||
<string name="storage_measuring">Måler lager…</string>
|
||||
<string name="storage_measuring_hint">Første gang tager et par sekunder.</string>
|
||||
<string name="storage_internal">Internt lager</string>
|
||||
<string name="storage_used_of">%1$s af %2$s brugt</string>
|
||||
<string name="storage_free">%1$s ledig</string>
|
||||
<string name="storage_droiddeck_uses">DroidDeck bruger %1$s. Målt %2$s.</string>
|
||||
<string name="storage_rescanning">måler igen…</string>
|
||||
<string name="storage_other_apps">Andre Android-apps</string>
|
||||
<string name="storage_safe_now">Kan frigøres nu</string>
|
||||
<string name="storage_safe_hint">Ubrugte kørselsmiljøer, rester, gamle logfiler</string>
|
||||
<string name="storage_clean_up">Rydder op…</string>
|
||||
<string name="storage_session_running">Stop sessionen for at rydde op eller slette noget. Størrelserne vises stadig.</string>
|
||||
<string name="storage_where">Hvor det ender</string>
|
||||
<string name="storage_games">Steam-spil</string>
|
||||
<string name="storage_game_data">Spildata</string>
|
||||
<string name="storage_game_data_hint">Præfikser %1$s · workshop %2$s · shaders %3$s</string>
|
||||
<string name="storage_tools">Proton og kørselsmiljøer</string>
|
||||
<string name="storage_tools_unused">%1$s ubrugt</string>
|
||||
<string name="storage_stores">GOG, Epic og andre butikker</string>
|
||||
<string name="storage_stores_hint">Fjern dem under Butikker</string>
|
||||
<string name="storage_desktop">Skrivebordsapps</string>
|
||||
<string name="storage_desktop_hint">Flatpak-apps og kørselsmiljøer</string>
|
||||
<string name="storage_system">Linux-systemet og Steam-klienten</string>
|
||||
<string name="storage_system_hint">Nødvendigt for at noget kan køre</string>
|
||||
<string name="storage_app_data">DroidDeck-data</string>
|
||||
<string name="storage_app_data_hint">Logfiler fra gamle sessioner %1$s · cache · drivere</string>
|
||||
<string name="storage_legend_games">Spil</string>
|
||||
<string name="storage_legend_data">Spildata</string>
|
||||
<string name="storage_legend_tools">Proton og kørselsmiljøer</string>
|
||||
<string name="storage_legend_stores">Andre butikker</string>
|
||||
<string name="storage_legend_desktop">Skrivebordsapps</string>
|
||||
<string name="storage_legend_system">Linux-systemet</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">Ryd op</string>
|
||||
<string name="storage_clean_up_lede">Intet her rører installerede spil eller igangværende hentninger.</string>
|
||||
<string name="storage_nothing_to_clean">Der er intet at rydde op lige nu.</string>
|
||||
<string name="storage_selected">Valgt: %1$s · %2$s → %3$s frigøres</string>
|
||||
<string name="storage_cleaning">Rydder op…</string>
|
||||
<string name="storage_free_button">Frigør %1$s</string>
|
||||
<string name="storage_freed">Frigjorde %1$s.</string>
|
||||
<string name="storage_item_runtimes">Ubrugte Steam-kørselsmiljøer og Proton</string>
|
||||
<string name="storage_item_leftovers">Rester efter slettede spil</string>
|
||||
<string name="storage_item_leftovers_unsure">Rester, der kan indeholde gemte spil</string>
|
||||
<string name="storage_item_logs">Logfiler fra gamle sessioner</string>
|
||||
<string name="storage_item_logs_detail">Logfiler fra denne session bevares</string>
|
||||
<string name="storage_item_custom_detail">Ingen spil bruger det. Installer det igen under Komponenter, hvis du får brug for det.</string>
|
||||
<string name="storage_item_shaders">Shadercache for installerede spil</string>
|
||||
<string name="storage_item_shaders_detail">Genopbygges, mens du spiller. De første minutter af hvert spil hakker igen.</string>
|
||||
<string name="storage_tag_safe">Sikkert</string>
|
||||
<string name="storage_tag_unused">Ubrugt</string>
|
||||
<string name="storage_tag_stutter">Hak</string>
|
||||
<string name="storage_tag_saves">Tjek gemte spil</string>
|
||||
<string name="storage_tag_default">Standard</string>
|
||||
<string name="storage_tag_required">Nødvendigt</string>
|
||||
<string name="storage_tag_not_used">Ubrugt</string>
|
||||
<string name="storage_back">Lager</string>
|
||||
<string name="storage_games_with_data">%1$s med deres data</string>
|
||||
<string name="storage_sort_largest">Størst</string>
|
||||
<string name="storage_sort_least_played">Mindst spillet</string>
|
||||
<string name="storage_sort_sd">På SD-kort</string>
|
||||
<string name="storage_no_games">Der er ingen spil her.</string>
|
||||
<string name="storage_played">spillet %1$s</string>
|
||||
<string name="storage_never_played">Aldrig spillet</string>
|
||||
<string name="storage_game_parts">Spil %1$s · data %2$s</string>
|
||||
<string name="storage_uninstall">Afinstaller</string>
|
||||
<string name="storage_games_note">Afinstallation åbner Steam-vinduet, så Steam registrerer det. For at flytte et spil til SD-kortet bruger du Steam › Indstillinger › Lager.</string>
|
||||
<string name="storage_uninstall_title">Afinstaller %1$s?</string>
|
||||
<string name="storage_uninstall_body">Steam åbnes og beder om bekræftelse. Det frigør spillets %1$s. Præfikset bliver, til du rydder resterne op, og Steam Cloud-gemte spil bliver på kontoen.</string>
|
||||
<string name="storage_open_steam">Åbn Steam</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s ubrugt</string>
|
||||
<string name="storage_remove_unused">Fjern ubrugte · %1$s</string>
|
||||
<string name="storage_in_use">I brug</string>
|
||||
<string name="storage_not_used">Ubrugt</string>
|
||||
<string name="storage_remove">Fjern</string>
|
||||
<string name="storage_remove_title">Fjern %1$s?</string>
|
||||
<string name="storage_remove_body">Frigør %1$s. Steam henter det igen, hvis et spil får brug for det.</string>
|
||||
<string name="storage_tool_fex_runtime">Nødvendigt for Linux x86-spil (FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">Nødvendigt for ARM64-Proton</string>
|
||||
<string name="storage_tool_redist">VC++- og DirectX-installationer, som spil kræver</string>
|
||||
<string name="storage_tool_fex">Kører Linux x86-spil</string>
|
||||
<string name="storage_tool_steam">Steam-værktøj</string>
|
||||
<string name="storage_tool_old_runtime">DroidDeck kører spil uden dette kørselsmiljø</string>
|
||||
<string name="storage_tool_x86_proton">Proton x86, som DroidDeck ikke kan køre</string>
|
||||
<string name="storage_tool_custom_default">Standard for alle spil</string>
|
||||
<string name="storage_tool_custom_unused">Ingen spil bruger det</string>
|
||||
<string name="storage_tool_custom_unknown">Steam-indstillingerne kan ikke læses, så det bevares</string>
|
||||
<string name="storage_cleanup_busy">Vent, til lageroprydningen er færdig</string>
|
||||
<string name="storage_low_title">Kun %1$s ledig</string>
|
||||
<string name="storage_low_body">Steam har brug for plads til opdateringer og shadercache. Under 10 GB kan spilopdateringer mislykkes halvvejs.</string>
|
||||
<string name="storage_low_safe">%1$s kan frigøres sikkert.</string>
|
||||
<string name="storage_free_up">Frigør plads</string>
|
||||
<string name="storage_play_anyway">Spil alligevel</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d installeret</item>
|
||||
<item quantity="other">%d installerede</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d på SD-kort</item>
|
||||
<item quantity="other">%d på SD-kort</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">%d værktøj</item>
|
||||
<item quantity="other">%d værktøjer</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">Tomme præfiksmapper og shadercache fra %d spil</item>
|
||||
<item quantity="other">Tomme præfiksmapper og shadercache fra %d spil</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">Præfikset for %d spil indeholder stadig filer, bl.a. lokale gemte spil. Lav en kopi først, hvis du har brug for dem.</item>
|
||||
<item quantity="other">Præfikserne for %d spil indeholder stadig filer, bl.a. lokale gemte spil. Lav en kopi først, hvis du har brug for dem.</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">%d spil</item>
|
||||
<item quantity="other">%d spil</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">DroidDecks standard-Proton</string>
|
||||
<string name="storage_tool_proton">DroidDecks Proton 11-mulighed</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">Bruges af %d spil</item>
|
||||
<item quantity="other">Bruges af %d spil</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">Komponenter</string>
|
||||
<string name="storage_components">Komponenter</string>
|
||||
<string name="storage_item_components">Ubrugte komponenter</string>
|
||||
<string name="storage_component_original">Original %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">Frigør %1$s. Læg kun en Proton-version tilbage, der ikke længere er installeret.</string>
|
||||
<string name="storage_leftovers_unchecked">Ikke tjekket: en Steam-apps manifest kan ikke læses, så præfikser for slettede spil bliver, til det kan.</string>
|
||||
<string name="storage_failed">Mislykkedes: %1$s</string>
|
||||
<string name="storage_desktop_shared">%1$s delt</string>
|
||||
<string name="storage_data_installed">Installerede spil</string>
|
||||
<string name="storage_data_parts">Præfiks %1$s · shaders %2$s · workshop %3$s</string>
|
||||
<string name="storage_clear_shaders">Ryd shaders</string>
|
||||
<string name="storage_data_leftovers">Slettede spil</string>
|
||||
<string name="storage_steam_app">Steam-app %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">Præfikset indeholder stadig filer, måske lokale gemte spil</string>
|
||||
<string name="storage_leftover_confirm">Rydder Wine-præfikset og shadercachen (%1$s). Lav først en kopi af de gemte spil, du har brug for; Steam Cloud-gemte spil bliver på kontoen.</string>
|
||||
<string name="storage_data_other">Bevares</string>
|
||||
<string name="storage_data_other_title">Ikke-Steam-spil og delt Steam-præfiks</string>
|
||||
<string name="storage_data_other_detail">Præfikser for spil tilføjet via butikker eller manuelt</string>
|
||||
<string name="storage_components_packages">Hentede pakker</string>
|
||||
<string name="storage_components_originals">Bevarede originaler</string>
|
||||
<string name="storage_components_none">Ingen komponentpakker hentet.</string>
|
||||
<string name="storage_component_in_use">Valgt til en Proton eller et spil</string>
|
||||
<string name="storage_component_unused">Ikke valgt nogen steder</string>
|
||||
<string name="storage_original_kept">Lægger den installerede Proton tilbage</string>
|
||||
<string name="storage_original_old">Proton-version, der ikke længere er installeret</string>
|
||||
<string name="storage_tag_in_use">I brug</string>
|
||||
<string name="storage_components_note">Komponenter i brug er låst. Vælg dem under Komponenter; det, du fjerner her, kan hentes igen der.</string>
|
||||
<string name="storage_component_confirm">Frigør %1$s. Pakker, du selv har importeret, kan ikke hentes igen.</string>
|
||||
<string name="storage_desktop_detail">%1$s til Flatpak-apps og det, de deler</string>
|
||||
<string name="storage_desktop_apps_title">Apps</string>
|
||||
<string name="storage_desktop_none">Ingen Flatpak-apps installeret.</string>
|
||||
<string name="storage_desktop_shared_title">Deles af appene</string>
|
||||
<string name="storage_desktop_runtimes">Kørselsmiljøer</string>
|
||||
<string name="storage_desktop_runtimes_detail">Bibliotekerne, appene kører på; ubrugte forsvinder med Fjern ubrugte</string>
|
||||
<string name="storage_desktop_cache">Hentningslager</string>
|
||||
<string name="storage_desktop_cache_detail">Flatpaks objektlager og hentede cache</string>
|
||||
<string name="storage_desktop_prune">Fjern ubrugte</string>
|
||||
<string name="storage_desktop_prune_body">Fjerner kørselsmiljøer, som ingen installeret app har brug for, og rydder op i Flatpaks objektlager. Installerede apps virker stadig.</string>
|
||||
<string name="storage_flatpak_uninstall_body">Fjerner appen (%1$s) og de kørselsmiljøer, ingen andre bruger. Data i din hjemmemappe bevares.</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">%d pakke</item>
|
||||
<item quantity="other">%d pakker</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">%d app</item>
|
||||
<item quantity="other">%d apps</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">Opstartsvideo</string>
|
||||
<string name="boot_movie_video">Video</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d fps</string>
|
||||
<string name="boot_movie_note">60 fps ser mere flydende ud, men bruger mere CPU, mens Steam starter. En opstartsvideo valgt i Steams egne indstillinger har forrang.</string>
|
||||
<string name="boot_movie_sound">Lyd</string>
|
||||
<string name="boot_movie_custom">Din egen video</string>
|
||||
<string name="boot_movie_custom_hint">MP4, MOV, MKV eller WebM. Andre formater og filer over %1$d MB konverteres til 720p-WebM på enheden, op til %2$d sekunder.</string>
|
||||
<string name="boot_movie_import">Importér</string>
|
||||
<string name="boot_movie_remove">Fjern</string>
|
||||
<string name="boot_movie_pick">Vælg en opstartsvideo</string>
|
||||
<string name="boot_movie_imported">%1$s er nu opstartsvideoen</string>
|
||||
<string name="boot_movie_imported_silent">%1$s er nu opstartsvideoen, uden lyd</string>
|
||||
<string name="boot_movie_too_long">Videoer, der skal konverteres, må højst vare %1$d sekunder</string>
|
||||
<string name="boot_movie_converting">Konverterer %1$d %%</string>
|
||||
<string name="boot_movie_converting_started">Konverterer %1$s til Steam. Hold DroidDeck åben.</string>
|
||||
<string name="boot_movie_unsupported">Enheden kan ikke afkode videoen.</string>
|
||||
<string name="boot_movie_import_failed">Kunne ikke importere videoen</string>
|
||||
</resources>
|
||||
@@ -575,6 +575,10 @@
|
||||
<string name="drawer_cas">Schärfen (CAS)</string>
|
||||
<string name="drawer_cas_level">Schärfegrad</string>
|
||||
<string name="drawer_fake_hdr">Fake-HDR</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">SteamVR starten</string>
|
||||
<string name="steamvr_checking">SteamVR wird geprüft…</string>
|
||||
<string name="drawer_deband">Debanding</string>
|
||||
<string name="drawer_deband_strength">Debanding-Stärke</string>
|
||||
<string name="drawer_brightness">Helligkeit</string>
|
||||
@@ -592,6 +596,11 @@
|
||||
<string name="drawer_texture_sharpness_note">Ein negativer Mip-Bias: schärfere Texturen, etwas Flimmern. Auto richtet sich danach, wie stark die Sitzung auf den Bildschirm vergrößert wird.</string>
|
||||
<string name="drawer_touch_auto">Auto (%1$s)</string>
|
||||
<string name="drawer_keyboard">Tastatur</string>
|
||||
<string name="drawer_latency">Latenz</string>
|
||||
<string name="drawer_low_latency_controller">Controller mit geringer Latenz</string>
|
||||
<string name="drawer_low_latency_controller_hint">Der Controller meldet mit 1000 Hz statt 250 Hz. Die CPU-Last ist etwas höher.</string>
|
||||
<string name="drawer_low_latency_touch">Touch mit geringer Latenz</string>
|
||||
<string name="drawer_low_latency_touch_hint">Touch überspringt das Zusammenfassen von Frames, Steam-Touch meldet mit 1000 Hz. Die CPU-Last ist etwas höher.</string>
|
||||
<string name="drawer_pc_keyboard">PC-Tastatur</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">Zweiter Bildschirm</string>
|
||||
@@ -843,7 +852,7 @@
|
||||
<string name="gate_eyebrow">Steam</string>
|
||||
<string name="gate_lede">Android stoppt Apps, die viele Hintergrundprozesse starten, und Steam startet Dutzende. Solange das so bleibt, werden Spiele ohne Fehlermeldung geschlossen.</string>
|
||||
<string name="gate_fix_for_me">Für mich beheben</string>
|
||||
<string name="gate_toggle_steps">Aktiviere „Einschränkungen für untergeordnete Prozesse deaktivieren“ und kehre dann zurück. Lass die Entwickleroptionen aktiviert, sonst geht der Schalter wieder aus.</string>
|
||||
<string name="gate_toggle_steps">Aktiviere „Beschränkungen für Unterprozesse deaktivieren“ und kehre dann zurück. Lass die Entwickleroptionen aktiviert, sonst geht der Schalter wieder aus.</string>
|
||||
<string name="gate_open_dev">Entwickleroptionen öffnen</string>
|
||||
<string name="gate_no_toggle">Diese Android-Version hat dafür keinen Schalter, deshalb ändert DroidDeck es über „Debugging über WLAN“. Dauert etwa eine Minute, kein Computer nötig.</string>
|
||||
<string name="gate_needs_wifi">„Debugging über WLAN“ braucht eine Wi-Fi-Verbindung. Jedes Netzwerk geht; Internet ist nicht nötig.</string>
|
||||
@@ -1009,7 +1018,6 @@
|
||||
<string name="gpu_set_with_runtime">Zusammen mit dem Laufzeittreiber festgelegt: %1$s enthält beide.</string>
|
||||
<string name="gpu_runtime_hint">Wird von Spielen und dem Desktop genutzt.</string>
|
||||
<string name="gpu_runtime_default">Laufzeit-Standard</string>
|
||||
<string name="gpu_display_hint">Bringt die Frames auf den Bildschirm. Gilt nach einem Neustart von DroidDeck.</string>
|
||||
<string name="gpu_auto_by_gpu">Auto - nach GPU gewählt</string>
|
||||
<string name="gpu_manage">Herunterladen, importieren oder löschen…</string>
|
||||
<string name="gpu_in_use_caps">IN VERWENDUNG</string>
|
||||
@@ -1239,10 +1247,16 @@
|
||||
<string name="drivers_downloading_pair">%1$s %2$s wird heruntergeladen…</string>
|
||||
<string name="drivers_updated_to">Auf %1$s %2$s aktualisiert.</string>
|
||||
<string name="drivers_updated_to_restart">Auf %1$s %2$s aktualisiert. Der Anzeigetreiber gilt nach einem Neustart von DroidDeck.</string>
|
||||
<string name="drivers_auto_waits_for_session">Treiber-Updates werden nach dem Ende der Sitzung ausgeführt.</string>
|
||||
<string name="drivers_auto_waits_for_network">Sobald mobile Daten und Energiesparmodus aus sind, wird %1$s %2$s geladen.</string>
|
||||
<string name="drivers_skipping_bad">%1$s %2$s konnte auf diesem Gerät keine Sitzung starten, deshalb bleibt der Automatikmodus beim funktionierenden Treiber.</string>
|
||||
<string name="drivers_rolled_back">%1$s konnte zweimal keine Sitzung starten, deshalb ist der Automatikmodus zum funktionierenden Treiber zurückgekehrt.</string>
|
||||
<string name="drivers_switched_to">Zu %1$s %2$s gewechselt.</string>
|
||||
<string name="drivers_switched_to_restart">Zu %1$s %2$s gewechselt. Der Anzeigetreiber gilt nach einem Neustart von DroidDeck.</string>
|
||||
<string name="drivers_using">%1$s %2$s wird verwendet.</string>
|
||||
<string name="drivers_using_restart">%1$s %2$s wird verwendet. Der Anzeigetreiber gilt nach einem Neustart von DroidDeck.</string>
|
||||
<string name="drivers_using_quest">Nutzt den eingebauten Quest-Anzeigetreiber und die Laufzeit-Standardwerte.</string>
|
||||
<string name="drivers_using_quest_restart">Nutzt den eingebauten Quest-Anzeigetreiber und die Laufzeit-Standardwerte. Der Anzeigetreiber greift nach einem Neustart von DroidDeck.</string>
|
||||
<string name="drivers_not_a_driver">Keine Treiber-ZIP: weder ein AdrenoTools-Treiber noch ein -Linux-Turnip noch ein Android + Linux-Bundle</string>
|
||||
<string name="drivers_import_failed">Import fehlgeschlagen: %1$s</string>
|
||||
<string name="drivers_imported_unnamed">Importierter Treiber</string>
|
||||
@@ -1664,7 +1678,7 @@
|
||||
<string name="adbpair_done_text">Tippe, um zu DroidDeck zurückzukehren. Du kannst Debugging über WLAN jetzt deaktivieren.</string>
|
||||
<string name="adbpair_title_failed">Einrichtung von Debugging über WLAN abgebrochen</string>
|
||||
|
||||
<string name="selfinst_http">Der Download antwortete mit HTTP %1$d</string>
|
||||
<string name="selfinst_download_failed">Download fehlgeschlagen</string>
|
||||
<string name="selfinst_size_mismatch">Der Download hatte nicht die Größe der veröffentlichten APK und wurde verworfen - versuche es erneut</string>
|
||||
<string name="selfinst_checksum_mismatch">Der Download stimmte nicht mit seiner Prüfsumme überein und wurde verworfen - versuche es erneut</string>
|
||||
<string name="selfinst_wrong_package">Das Update ist für das Android-Paket %1$s, nicht für %2$s</string>
|
||||
@@ -1762,7 +1776,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">Das Rate-Limit von GitHub wurde erreicht - versuche es später erneut</string>
|
||||
<string name="turnip_github_http">GitHub antwortete mit HTTP %1$d</string>
|
||||
<string name="turnip_download_http">Download antwortete mit HTTP %1$d</string>
|
||||
<string name="turnip_download_failed">Download fehlgeschlagen</string>
|
||||
<string name="turnip_list_predates_checksums">Diese Treiberliste stammt aus der Zeit vor Prüfsummen - aktualisiere sie und versuche es erneut</string>
|
||||
<string name="turnip_checksum_mismatch">Prüfsumme stimmt nicht überein - der Download wurde verworfen</string>
|
||||
<string name="turnip_not_adrenotools">Kein AdrenoTools-Treiber: %1$s</string>
|
||||
@@ -1799,7 +1813,7 @@
|
||||
<string name="phantom_instructions_fix">%1$s Falls das nicht möglich ist, verbinde dieses Gerät per ADB mit einem Computer und führe den Befehl unten aus. Kehre dann hierher zurück und prüfe erneut.</string>
|
||||
<string name="phantom_instructions_off">Steam-Sitzungen können starten.</string>
|
||||
<string name="phantom_instructions_not_applicable">Diese Android-Version nutzt dieses Limit nicht.</string>
|
||||
<string name="phantom_fix_toggle">Aktiviere „Einschränkungen für untergeordnete Prozesse deaktivieren“ in den Entwickleroptionen und lass die Entwickleroptionen danach aktiviert.</string>
|
||||
<string name="phantom_fix_toggle">Aktiviere „Beschränkungen für Unterprozesse deaktivieren“ in den Entwickleroptionen und lass die Entwickleroptionen danach aktiviert.</string>
|
||||
<string name="phantom_fix_no_toggle">Diese Android-Version hat dafür keinen Schalter. Nutze Debugging über WLAN, um es auf diesem Gerät zu ändern.</string>
|
||||
<string name="upd_block_no_apk">Dieser Build hat keinen Download für diese DroidDeck-Installation.</string>
|
||||
<string name="upd_block_version_code">Android installiert ihn nicht über diesen Build, weil sein versionCode %1$d unter dem installierten %2$d liegt.</string>
|
||||
@@ -2066,6 +2080,21 @@
|
||||
<string name="edit_other_exe">Andere EXE…</string>
|
||||
<string name="edit_remove">Entfernen</string>
|
||||
<string name="art_cover">Cover</string>
|
||||
<string name="art_wide_cover">Breites Cover</string>
|
||||
<string name="art_adjust_logo">Logo anpassen</string>
|
||||
<string name="logo_pos_default">Standard</string>
|
||||
<string name="logo_pos_upper_left">Oben links</string>
|
||||
<string name="logo_pos_upper_center">Oben mittig</string>
|
||||
<string name="logo_pos_center">Mitte</string>
|
||||
<string name="logo_pos_bottom_left">Unten links</string>
|
||||
<string name="logo_pos_bottom_center">Unten mittig</string>
|
||||
<string name="logo_pos_position">Position</string>
|
||||
<string name="logo_pos_size">Größe</string>
|
||||
<string name="logo_pos_save">Speichern</string>
|
||||
<string name="logo_pos_reset">Auf Standard zurücksetzen</string>
|
||||
<string name="logo_pos_hint">Wird wie bei Steam über dem Spielehintergrund gezeichnet. Die Position kann um einige Pixel abweichen.</string>
|
||||
<string name="logo_pos_no_logo">Für dieses Spiel gibt es noch kein Logo. Wähle unter Logo eines aus, um es hier zu sehen.</string>
|
||||
<string name="logo_pos_failed">Die Logoposition ließ sich nicht speichern. Versuche es erneut.</string>
|
||||
<string name="art_background">Hintergrund</string>
|
||||
<string name="art_logo">Logo</string>
|
||||
<string name="art_icon">Symbol</string>
|
||||
@@ -2078,6 +2107,17 @@
|
||||
<string name="art_pick_image">Bild auswählen…</string>
|
||||
<string name="art_pick_title">Bild auswählen: %1$s</string>
|
||||
<string name="art_reset">Auf automatisch zurücksetzen</string>
|
||||
<string name="art_search_sgdb">SteamGridDB durchsuchen…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB: %1$s</string>
|
||||
<string name="art_sgdb_search_label">Spiel suchen</string>
|
||||
<string name="art_sgdb_other_games">Weitere Treffer</string>
|
||||
<string name="art_sgdb_none">Nichts gefunden.</string>
|
||||
<string name="art_sgdb_no_key">SteamGridDB braucht einen API-Schlüssel. Füge ihn unter Einstellungen › Launcher hinzu.</string>
|
||||
<string name="art_sgdb_bad_key">SteamGridDB hat den API-Schlüssel nicht akzeptiert. Prüfe ihn unter Einstellungen › Launcher.</string>
|
||||
<string name="art_sgdb_limit">SteamGridDB begrenzt gerade die Anfragen. Versuche es später erneut.</string>
|
||||
<string name="art_sgdb_offline">Keine Verbindung zu SteamGridDB. Prüfe die Netzwerkverbindung.</string>
|
||||
<string name="art_sgdb_server">Bei SteamGridDB ist ein Problem aufgetreten. Versuche es später erneut.</string>
|
||||
<string name="art_sgdb_no_game">SteamGridDB hat keinen Treffer für dieses Spiel. Versuche es mit der Suche.</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="one">%1$d Spiel hinzugefügt</item>
|
||||
<item quantity="other">%1$d Spiele hinzugefügt</item>
|
||||
@@ -2105,6 +2145,45 @@
|
||||
<string name="gpu_bar_auto">Auto · %1$s</string>
|
||||
<string name="gpu_separately">Laufzeit- und Anzeigetreiber getrennt</string>
|
||||
<string name="gpu_use_title">%1$s verwenden?</string>
|
||||
<string name="gpu_mode_title">So werden Treiber gewählt</string>
|
||||
<string name="gpu_mode_auto_detail">Wählt für diese GPU automatisch die beste Kombination und hält sie aktuell</string>
|
||||
<string name="gpu_mode_pair">Paar</string>
|
||||
<string name="gpu_mode_pair_detail">Eine Kombination wählen, die beide Treiber gemeinsam festlegt</string>
|
||||
<string name="gpu_mode_mix">Frei kombinieren</string>
|
||||
<string name="gpu_mode_mix_detail">Anzeigetreiber und Laufzeittreiber getrennt wählen</string>
|
||||
<string name="gpu_mode_pair_short">Paar</string>
|
||||
<string name="gpu_mode_mix_short">Mix</string>
|
||||
<string name="gpu_src_auto">Auto</string>
|
||||
<string name="gpu_src_pair">Paar</string>
|
||||
<string name="gpu_src_bundle">Integriert</string>
|
||||
<string name="gpu_src_chosen">Gewählt</string>
|
||||
<string name="gpu_src_by_gpu">Nach GPU</string>
|
||||
<string name="gpu_src_default">Standard</string>
|
||||
<string name="gpu_display_live">Zeichnet</string>
|
||||
<string name="gpu_display_pending">Neustart ausstehend · zuletzt von %1$s gezeichnet</string>
|
||||
<string name="gpu_runtime_live">Läuft</string>
|
||||
<string name="gpu_runtime_pending">Gilt ab der nächsten Sitzung · diese Sitzung nutzt %1$s</string>
|
||||
<string name="gpu_display_next">Zeichnet den Bildschirm. Gilt ab der nächsten Sitzung.</string>
|
||||
<string name="gpu_previous_driver">Vorheriger Treiber</string>
|
||||
<string name="gpu_restart_banner">Neu starten, um den Anzeigetreiber zu wechseln.</string>
|
||||
<string name="gpu_restart_blocked">Beende zuerst die laufende Sitzung und starte dann DroidDeck neu, um den Anzeigetreiber zu wechseln.</string>
|
||||
<string name="gpu_restart_now">Jetzt neu starten</string>
|
||||
<string name="gpu_restart_later">Später</string>
|
||||
<string name="gpu_auto_title">Diese Kombination wurde automatisch für deine %1$s gewählt</string>
|
||||
<string name="gpu_auto_builtin_title">Der Automatikmodus nutzt für deine %1$s den eingebauten Treiber</string>
|
||||
<string name="gpu_auto_reason">Für die %1$s-Serie gewählt. Neuere Versionen werden im Hintergrund installiert.</string>
|
||||
<string name="gpu_pair_title">Kombination wählen</string>
|
||||
<string name="gpu_pair_hint">Eine Kombination legt Anzeigetreiber und Laufzeittreiber gemeinsam fest.</string>
|
||||
<string name="gpu_mix_title">Treiber getrennt wählen</string>
|
||||
<string name="gpu_mix_hint">Ein integriertes Paket füllt beides. Änderungen am Anzeigetreiber greifen nach einem Neustart, der Laufzeittreiber ab der nächsten Sitzung.</string>
|
||||
<string name="gpu_change_display">Anzeigetreiber wechseln</string>
|
||||
<string name="gpu_change_runtime">Laufzeittreiber wechseln</string>
|
||||
<string name="gpu_autos_pick">Automatisch wählen</string>
|
||||
<string name="gpu_installed">Installiert</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · gilt nach Neustart</string>
|
||||
<string name="gpu_chip_restart">%1$s · Neustart ausstehend</string>
|
||||
<string name="comp_this_device">Dieses Gerät</string>
|
||||
<string name="proton_remove_title">%1$s entfernen?</string>
|
||||
<string name="setup_tab_diagnostics">Diagnose</string>
|
||||
<string name="setup_ready">Bereit zum Spielen</string>
|
||||
@@ -2116,6 +2195,14 @@
|
||||
<string name="setup_group_look">Aussehen</string>
|
||||
<string name="setup_group_device">Dieses Gerät</string>
|
||||
<string name="setup_group_art">Bibliotheksgrafiken</string>
|
||||
<string name="setup_group_download">Downloads</string>
|
||||
<string name="setup_download_source">Quelle</string>
|
||||
<string name="setup_download_source_auto">Automatisch</string>
|
||||
<string name="setup_download_source_direct">Direkt</string>
|
||||
<string name="setup_download_source_mirror">Spiegel</string>
|
||||
<string name="setup_download_source_custom">Eigener Präfix</string>
|
||||
<string name="setup_download_mirror">Seite</string>
|
||||
<string name="setup_download_prefix">Präfix</string>
|
||||
<string name="flathub_eyebrow">LINUX-APPS</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">Desktop-Apps für den Linux-Desktop, geöffnet über Desktop.</string>
|
||||
@@ -2223,12 +2310,9 @@
|
||||
<string name="cardboard_recenter_button">Zentrieren</string>
|
||||
<string name="cardboard_steamvr">SteamVR (experimentell)</string>
|
||||
<string name="cardboard_steamvr_install">Installieren</string>
|
||||
<string name="cardboard_steamvr_install_hint">Lädt Valves SteamVR für ARM64 herunter (571 MB).</string>
|
||||
<string name="cardboard_steamvr_installing">SteamVR wird heruntergeladen und entpackt …</string>
|
||||
<string name="cardboard_steamvr_start">Starten</string>
|
||||
<string name="cardboard_steamvr_ready_hint">Startet SteamVR mit deinem Kopf als Headset; das Bild wechselt zu Side-by-Side-3D.</string>
|
||||
<string name="cardboard_steamvr_stop">Beenden</string>
|
||||
<string name="cardboard_steamvr_running_hint">SteamVR läuft.</string>
|
||||
<string name="cardboard_steamvr_unavailable">In dieser Sitzung nicht verfügbar.</string>
|
||||
<string name="cardboard_steamvr_failed">SteamVR hat nicht reagiert; Sitzungsprotokoll prüfen.</string>
|
||||
<string name="drawer_component_downloads">Komponenten herunterladen</string>
|
||||
@@ -2256,4 +2340,195 @@
|
||||
<string name="stores_storage_insufficient">Nicht genug freier Speicher: %1$s benötigt, nur %2$s frei</string>
|
||||
<string name="stores_storage_low">Zu wenig Speicher (%1$s frei, %2$s benötigt)</string>
|
||||
<string name="stores_target_free_unknown">Freier Speicher unbekannt</string>
|
||||
<string name="display_picture_position">Bildposition</string>
|
||||
<string name="display_position_top">Oben</string>
|
||||
<string name="display_position_bottom">Unten</string>
|
||||
<string name="display_position_center">Mitte (Standard)</string>
|
||||
<string name="setup_tab_storage">Speicher</string>
|
||||
<string name="storage_measuring">Speicher wird erfasst…</string>
|
||||
<string name="storage_measuring_hint">Die erste Erfassung dauert ein paar Sekunden.</string>
|
||||
<string name="storage_internal">Interner Speicher</string>
|
||||
<string name="storage_used_of">%1$s von %2$s belegt</string>
|
||||
<string name="storage_free">%1$s frei</string>
|
||||
<string name="storage_droiddeck_uses">DroidDeck belegt %1$s. Erfassung vom %2$s.</string>
|
||||
<string name="storage_rescanning">Wird neu erfasst…</string>
|
||||
<string name="storage_other_apps">Andere Android-Apps</string>
|
||||
<string name="storage_safe_now">Jetzt sicher freigebbar</string>
|
||||
<string name="storage_safe_hint">Ungenutzte Laufzeiten, Reste, alte Logs</string>
|
||||
<string name="storage_clean_up">Aufräumen…</string>
|
||||
<string name="storage_session_running">Beende die Sitzung, um aufzuräumen oder zu entfernen. Die Größen werden weiterhin angezeigt.</string>
|
||||
<string name="storage_where">Belegung</string>
|
||||
<string name="storage_games">Steam-Spiele</string>
|
||||
<string name="storage_game_data">Spieldaten</string>
|
||||
<string name="storage_game_data_hint">Präfixe %1$s · Workshop %2$s · Shader %3$s</string>
|
||||
<string name="storage_tools">Proton und Laufzeiten</string>
|
||||
<string name="storage_tools_unused">%1$s ungenutzt</string>
|
||||
<string name="storage_stores">GOG, Epic und andere Stores</string>
|
||||
<string name="storage_stores_hint">Im Store deinstallieren</string>
|
||||
<string name="storage_desktop">Desktop-Apps</string>
|
||||
<string name="storage_desktop_hint">Flatpak-Apps und Laufzeiten</string>
|
||||
<string name="storage_system">Linux-System und Steam-Client</string>
|
||||
<string name="storage_system_hint">Nötig, um irgendetwas auszuführen</string>
|
||||
<string name="storage_app_data">DroidDeck-Daten</string>
|
||||
<string name="storage_app_data_hint">Alte Sitzungslogs %1$s · Cache · Treiber</string>
|
||||
<string name="storage_legend_games">Spiele</string>
|
||||
<string name="storage_legend_data">Spieldaten</string>
|
||||
<string name="storage_legend_tools">Proton und Laufzeiten</string>
|
||||
<string name="storage_legend_stores">Andere Stores</string>
|
||||
<string name="storage_legend_desktop">Desktop-Apps</string>
|
||||
<string name="storage_legend_system">Linux-System</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">Aufräumen</string>
|
||||
<string name="storage_clean_up_lede">Installierte Spiele und laufende Downloads bleiben unberührt.</string>
|
||||
<string name="storage_nothing_to_clean">Es gibt gerade nichts aufzuräumen.</string>
|
||||
<string name="storage_selected">Gewählt: %1$s · %2$s → %3$s frei</string>
|
||||
<string name="storage_cleaning">Wird aufgeräumt…</string>
|
||||
<string name="storage_free_button">%1$s freigeben</string>
|
||||
<string name="storage_freed">%1$s freigegeben.</string>
|
||||
<string name="storage_item_runtimes">Ungenutzte Steam-Laufzeiten und Proton</string>
|
||||
<string name="storage_item_leftovers">Reste deinstallierter Spiele</string>
|
||||
<string name="storage_item_leftovers_unsure">Reste, die Speicherstände enthalten können</string>
|
||||
<string name="storage_item_logs">Alte Sitzungslogs</string>
|
||||
<string name="storage_item_logs_detail">Logs der aktuellen Sitzung bleiben erhalten</string>
|
||||
<string name="storage_item_custom_detail">Kein Spiel nutzt es. Installiere es zum Nutzen unter Komponenten neu.</string>
|
||||
<string name="storage_item_shaders">Shader-Cache installierter Spiele</string>
|
||||
<string name="storage_item_shaders_detail">Wird beim Spielen neu erzeugt. Die ersten Minuten jedes Spiels ruckeln erneut.</string>
|
||||
<string name="storage_tag_safe">Sicher</string>
|
||||
<string name="storage_tag_unused">Ungenutzt</string>
|
||||
<string name="storage_tag_stutter">Ruckeln</string>
|
||||
<string name="storage_tag_saves">Speicherstände prüfen</string>
|
||||
<string name="storage_tag_default">Standard</string>
|
||||
<string name="storage_tag_required">Nötig</string>
|
||||
<string name="storage_tag_not_used">Ungenutzt</string>
|
||||
<string name="storage_back">Speicher</string>
|
||||
<string name="storage_games_with_data">%1$s mit Daten</string>
|
||||
<string name="storage_sort_largest">Größte</string>
|
||||
<string name="storage_sort_least_played">Am seltensten gespielt</string>
|
||||
<string name="storage_sort_sd">Auf SD-Karte</string>
|
||||
<string name="storage_no_games">Hier sind keine Spiele.</string>
|
||||
<string name="storage_played">Gespielt %1$s</string>
|
||||
<string name="storage_never_played">Nie gespielt</string>
|
||||
<string name="storage_game_parts">Spiel %1$s · Daten %2$s</string>
|
||||
<string name="storage_uninstall">Deinstallieren</string>
|
||||
<string name="storage_games_note">Die Deinstallation öffnet den Dialog von Steam, damit Steam es mitbekommt. Um ein Spiel auf die SD-Karte zu verschieben, nutze Steam › Einstellungen › Speicher.</string>
|
||||
<string name="storage_uninstall_title">%1$s deinstallieren?</string>
|
||||
<string name="storage_uninstall_body">Steam öffnet sich und fragt nach. Dadurch werden %1$s des Spiels frei. Das Präfix bleibt bis zum Aufräumen der Reste, Steam-Cloud-Speicherstände bleiben im Konto.</string>
|
||||
<string name="storage_open_steam">Steam öffnen</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s ungenutzt</string>
|
||||
<string name="storage_remove_unused">Ungenutztes entfernen · %1$s</string>
|
||||
<string name="storage_in_use">In Benutzung</string>
|
||||
<string name="storage_not_used">Ungenutzt</string>
|
||||
<string name="storage_remove">Entfernen</string>
|
||||
<string name="storage_remove_title">%1$s entfernen?</string>
|
||||
<string name="storage_remove_body">Gibt %1$s frei. Braucht später ein Spiel es, lädt Steam es erneut.</string>
|
||||
<string name="storage_tool_fex_runtime">Für Linux-x86-Spiele nötig (FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">Für ARM64-Proton nötig</string>
|
||||
<string name="storage_tool_redist">VC++- und DirectX-Installationsprogramme, die Spiele anfordern</string>
|
||||
<string name="storage_tool_fex">Linux-x86-Spiele ausführen</string>
|
||||
<string name="storage_tool_steam">Steam-Werkzeuge</string>
|
||||
<string name="storage_tool_old_runtime">DroidDeck führt Spiele ohne diese Laufzeit aus</string>
|
||||
<string name="storage_tool_x86_proton">x86-Proton, das DroidDeck nicht ausführen kann</string>
|
||||
<string name="storage_tool_custom_default">Standard für alle Spiele</string>
|
||||
<string name="storage_tool_custom_unused">Kein Spiel nutzt es</string>
|
||||
<string name="storage_tool_custom_unknown">Steam-Einstellungen nicht lesbar, bleibt daher erhalten</string>
|
||||
<string name="storage_cleanup_busy">Warte, bis das Aufräumen des Speichers fertig ist</string>
|
||||
<string name="storage_low_title">Nur noch %1$s frei</string>
|
||||
<string name="storage_low_body">Steam braucht Platz für Updates und Shader-Cache. Unter 10 GB können Spiel-Updates mittendrin fehlschlagen.</string>
|
||||
<string name="storage_low_safe">%1$s lässt sich sicher freigeben.</string>
|
||||
<string name="storage_free_up">Platz schaffen</string>
|
||||
<string name="storage_play_anyway">Trotzdem spielen</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d installiert</item>
|
||||
<item quantity="other">%d installiert</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d auf SD-Karte</item>
|
||||
<item quantity="other">%d auf SD-Karte</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">%d Werkzeuge</item>
|
||||
<item quantity="other">%d Werkzeuge</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">Leere Präfixordner und Shader-Caches von %d Spielen</item>
|
||||
<item quantity="other">Leere Präfixordner und Shader-Caches von %d Spielen</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">Präfixe von %d Spielen enthalten noch Dateien, darunter möglicherweise lokale Speicherstände. Sichere sie vorher, wenn du sie behalten willst.</item>
|
||||
<item quantity="other">Präfixe von %d Spielen enthalten noch Dateien, darunter möglicherweise lokale Speicherstände. Sichere sie vorher, wenn du sie behalten willst.</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">%d Spiel</item>
|
||||
<item quantity="other">%d Spiele</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">Standard-Proton von DroidDeck</string>
|
||||
<string name="storage_tool_proton">Proton-11-Option von DroidDeck</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">Von %d Spiel genutzt</item>
|
||||
<item quantity="other">Von %d Spielen genutzt</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">Komponenten</string>
|
||||
<string name="storage_components">Komponenten</string>
|
||||
<string name="storage_item_components">Ungenutzte Komponenten</string>
|
||||
<string name="storage_component_original">Original %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">Gibt %1$s frei. Stellt nur eine Proton-Version wieder her, die nicht mehr installiert ist.</string>
|
||||
<string name="storage_leftovers_unchecked">Nicht geprüft: Ein Steam-App-Manifest war nicht lesbar, deshalb bleiben Präfixe deinstallierter Spiele erhalten, bis es lesbar ist.</string>
|
||||
<string name="storage_failed">Nicht abgeschlossen: %1$s</string>
|
||||
<string name="storage_desktop_shared">Gemeinsam %1$s</string>
|
||||
<string name="storage_data_installed">Installierte Spiele</string>
|
||||
<string name="storage_data_parts">Präfix %1$s · Shader %2$s · Workshop %3$s</string>
|
||||
<string name="storage_clear_shaders">Shader löschen</string>
|
||||
<string name="storage_data_leftovers">Deinstallierte Spiele</string>
|
||||
<string name="storage_steam_app">Steam-App %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">Das Präfix enthält noch Dateien, darunter möglicherweise lokale Speicherstände</string>
|
||||
<string name="storage_leftover_confirm">Löscht Wine-Präfix und Shader-Cache (%1$s). Sichere vorher benötigte Speicherstände; Steam-Cloud-Speicherstände bleiben im Konto.</string>
|
||||
<string name="storage_data_other">Behalten</string>
|
||||
<string name="storage_data_other_title">Gemeinsame Präfixe von Nicht-Steam-Spielen und Steam</string>
|
||||
<string name="storage_data_other_detail">Präfixe von Spielen aus Stores oder manuell hinzugefügter Spiele</string>
|
||||
<string name="storage_components_packages">Geladene Pakete</string>
|
||||
<string name="storage_components_originals">Gesicherte Originale</string>
|
||||
<string name="storage_components_none">Keine geladenen Komponentenpakete.</string>
|
||||
<string name="storage_component_in_use">Für ein Proton oder ein Spiel gewählt</string>
|
||||
<string name="storage_component_unused">Nirgends gewählt</string>
|
||||
<string name="storage_original_kept">Installiertes Proton wiederherstellen</string>
|
||||
<string name="storage_original_old">Proton-Version, die nicht mehr installiert ist</string>
|
||||
<string name="storage_tag_in_use">In Benutzung</string>
|
||||
<string name="storage_components_note">Komponenten in Benutzung sind gesperrt. Wähle sie unter Komponenten; was du hier entfernst, lässt sich dort erneut laden.</string>
|
||||
<string name="storage_component_confirm">Gibt %1$s frei. Selbst importierte Pakete lassen sich nicht erneut laden.</string>
|
||||
<string name="storage_desktop_detail">%1$s für Flatpak-Apps und ihre gemeinsamen Inhalte</string>
|
||||
<string name="storage_desktop_apps_title">Apps</string>
|
||||
<string name="storage_desktop_none">Keine Flatpak-Apps installiert.</string>
|
||||
<string name="storage_desktop_shared_title">Gemeinsam genutzt</string>
|
||||
<string name="storage_desktop_runtimes">Laufzeiten</string>
|
||||
<string name="storage_desktop_runtimes_detail">Bibliotheken, die Apps brauchen; ungenutzte räumt „Ungenutztes entfernen“ mit auf</string>
|
||||
<string name="storage_desktop_cache">Download-Repository</string>
|
||||
<string name="storage_desktop_cache_detail">Objekt-Repository und zwischengespeicherte Downloads von Flatpak</string>
|
||||
<string name="storage_desktop_prune">Ungenutztes entfernen</string>
|
||||
<string name="storage_desktop_prune_body">Entfernt Laufzeiten, die keine installierte App braucht, und räumt das Objekt-Repository von Flatpak auf. Installierte Apps laufen weiter.</string>
|
||||
<string name="storage_flatpak_uninstall_body">Entfernt die App (%1$s) und Laufzeiten, die keine andere App braucht. Daten im Home-Verzeichnis bleiben erhalten.</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">%d Paket</item>
|
||||
<item quantity="other">%d Pakete</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">%d App</item>
|
||||
<item quantity="other">%d Apps</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">Bootvideo</string>
|
||||
<string name="boot_movie_video">Video</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d fps</string>
|
||||
<string name="boot_movie_note">60 fps wirkt flüssiger, braucht aber mehr CPU, während Steam startet. Ein in Steam selbst gewähltes Startvideo hat Vorrang.</string>
|
||||
<string name="boot_movie_sound">Ton</string>
|
||||
<string name="boot_movie_custom">Eigenes Video</string>
|
||||
<string name="boot_movie_custom_hint">MP4, MOV, MKV oder WebM. Andere Formate und Dateien über %1$d MB werden auf dem Gerät in 720p-WebM umgewandelt, bis %2$d Sekunden.</string>
|
||||
<string name="boot_movie_import">Importieren</string>
|
||||
<string name="boot_movie_remove">Entfernen</string>
|
||||
<string name="boot_movie_pick">Bootvideo auswählen</string>
|
||||
<string name="boot_movie_imported">%1$s ist jetzt das Bootvideo</string>
|
||||
<string name="boot_movie_imported_silent">%1$s ist jetzt das Bootvideo, ohne Ton</string>
|
||||
<string name="boot_movie_too_long">Videos, die umgewandelt werden, dürfen höchstens %1$d Sekunden lang sein</string>
|
||||
<string name="boot_movie_converting">Umwandeln %1$d %%</string>
|
||||
<string name="boot_movie_converting_started">%1$s wird für Steam umgewandelt. DroidDeck geöffnet lassen.</string>
|
||||
<string name="boot_movie_unsupported">Dieses Gerät kann das Video nicht dekodieren.</string>
|
||||
<string name="boot_movie_import_failed">Das Video konnte nicht importiert werden</string>
|
||||
</resources>
|
||||
@@ -278,6 +278,9 @@
|
||||
<string name="mode_suspend_steam_note">Το Αυτόματο θέτει σε παύση και συνεχίζει όταν εμφανίζεται. Το Εγγενής προετοιμάζει πρώτα το Steam για αδρανοποίηση. Το Μη αυτόματο περιμένει το Συνέχιση. Το Ποτέ συνεχίζει να εκτελείται.</string>
|
||||
<string name="mode_background_downloads">Διατήρηση των λήψεων σε εξέλιξη</string>
|
||||
<string name="mode_background_downloads_hint">Καθυστερεί την αναστολή παρασκηνίου όσο το Steam κάνει ενεργά λήψη.</string>
|
||||
<string name="mode_zram">Συμπίεση zRAM</string>
|
||||
<string name="mode_zram_hint">Όσο τρέχει ένα παιχνίδι, η μνήμη του Steam συμπιέζεται στο zram της συσκευής, ώστε το παιχνίδι να έχει περισσότερη RAM. Ισχύει την επόμενη φορά που θα ξεκινήσει το Steam.</string>
|
||||
<string name="mode_zram_unsupported">Μη διαθέσιμο: η συσκευή δεν έχει zram ή swap, ή ο πυρήνας είναι παλαιότερος από 5.4.</string>
|
||||
<string name="mode_suspend_note">Το Αυτόματο θέτει σε παύση στο παρασκήνιο και συνεχίζει όταν εμφανίζεται. Το Μη αυτόματο περιμένει το Συνέχιση. Το Ποτέ συνεχίζει να εκτελείται.</string>
|
||||
<string name="mode_startup">Εκκίνηση</string>
|
||||
<string name="mode_steam_startup">Εκκίνηση του Steam με το DroidDeck</string>
|
||||
@@ -575,6 +578,10 @@
|
||||
<string name="drawer_cas">Όξυνση (CAS)</string>
|
||||
<string name="drawer_cas_level">Επίπεδο όξυνσης</string>
|
||||
<string name="drawer_fake_hdr">Ψεύτικο HDR</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">Εκκίνηση SteamVR</string>
|
||||
<string name="steamvr_checking">Έλεγχος SteamVR…</string>
|
||||
<string name="drawer_deband">Αφαίρεση λωρίδων</string>
|
||||
<string name="drawer_deband_strength">Ένταση αφαίρεσης λωρίδων</string>
|
||||
<string name="drawer_brightness">Φωτεινότητα</string>
|
||||
@@ -592,6 +599,11 @@
|
||||
<string name="drawer_texture_sharpness_note">Μια αρνητική απόκλιση mip: πιο έντονες υφές, κάποιο τρεμόπαιγμα. Το Αυτόματο ταιριάζει με το πόσο μεγεθύνεται η συνεδρία στην οθόνη.</string>
|
||||
<string name="drawer_touch_auto">Αυτόματο (%1$s)</string>
|
||||
<string name="drawer_keyboard">Πληκτρολόγιο</string>
|
||||
<string name="drawer_latency">Καθυστέρηση</string>
|
||||
<string name="drawer_low_latency_controller">Χειριστήριο χαμηλής καθυστέρησης</string>
|
||||
<string name="drawer_low_latency_controller_hint">Το χειριστήριο αναφέρει στα 1000 Hz αντί για 250 Hz. Λίγο περισσότερο CPU.</string>
|
||||
<string name="drawer_low_latency_touch">Αφή χαμηλής καθυστέρησης</string>
|
||||
<string name="drawer_low_latency_touch_hint">Οι αφές παρακάμπτουν την ομαδοποίηση καρέ, οπότε ο χειρισμός αφής του Steam τρέχει στα 1000 Hz. Λίγο περισσότερο CPU.</string>
|
||||
<string name="drawer_pc_keyboard">Πληκτρολόγιο PC</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">Δεύτερη οθόνη</string>
|
||||
@@ -683,6 +695,14 @@
|
||||
<string name="setup_saved_logs">Αποθηκευμένα αρχεία καταγραφής συνεδρίας</string>
|
||||
<string name="setup_saved_logs_hint">Διατηρεί τα %1$d πιο πρόσφατα</string>
|
||||
<string name="setup_clear_logs">Εκκαθάριση αρχείων καταγραφής</string>
|
||||
<string name="setup_reset_settings">Επαναφορά όλων των ρυθμίσεων</string>
|
||||
<string name="setup_reset_settings_hint">Επαναφέρει κάθε ρύθμιση στην προεπιλεγμένη τιμή</string>
|
||||
<string name="setup_reset_settings_button">Επαναφορά</string>
|
||||
<string name="setup_reset_settings_title">Επαναφορά όλων των ρυθμίσεων;</string>
|
||||
<string name="setup_reset_settings_body">Οι ρυθμίσεις εικόνας, απόδοσης, χειριστηρίου, θέματος, γλώσσας και εκκινητή επιστρέφουν στις προεπιλογές. Τα παιχνίδια, η σύνδεση στο Steam, οι λογαριασμοί καταστημάτων, τα ληφθέντα προγράμματα οδήγησης και οι φάκελοι αποθήκευσης παραμένουν.</string>
|
||||
<string name="setup_reset_settings_profiles">Επαναφορά και των προφίλ παιχνιδιών (Proton, FEX, στοιχεία και περιβάλλον ανά παιχνίδι, καθώς και το περιβάλλον του προφίλ Προεπιλογή)</string>
|
||||
<string name="setup_reset_settings_session_running">Σταμάτησε τη συνεδρία πριν κάνεις επαναφορά ρυθμίσεων.</string>
|
||||
<string name="setup_reset_settings_partial">Οι ρυθμίσεις επαναφέρθηκαν, αλλά όχι όλες. Οι λεπτομέρειες είναι στα αρχεία καταγραφής.</string>
|
||||
<string name="setup_offline">Λειτουργία εκτός σύνδεσης</string>
|
||||
<string name="setup_sign_in_first">Συνδεθείτε πρώτα στο Steam</string>
|
||||
<string name="setup_launcher">Εκκινητής</string>
|
||||
@@ -843,7 +863,7 @@
|
||||
<string name="gate_eyebrow">Steam</string>
|
||||
<string name="gate_lede">Το Android διακόπτει εφαρμογές που ξεκινούν πολλές διαδικασίες παρασκηνίου, και το Steam ξεκινά δεκάδες. Μέχρι να αλλάξει αυτό, τα παιχνίδια κλείνουν χωρίς σφάλμα.</string>
|
||||
<string name="gate_fix_for_me">Διόρθωσέ το για εμένα</string>
|
||||
<string name="gate_toggle_steps">Ενεργοποιήστε «Απενεργοποίηση περιορισμών θυγατρικών διαδικασιών» και επιστρέψτε εδώ. Αφήστε τις Επιλογές προγραμματιστή ενεργές, διαφορετικά ο διακόπτης απενεργοποιείται ξανά.</string>
|
||||
<string name="gate_toggle_steps">Ενεργοποιήστε «Απενεργοπ. περιορισμών σε θυγατρικές διεργασίες» και επιστρέψτε εδώ. Αφήστε τις Επιλογές προγραμματιστή ενεργές, διαφορετικά ο διακόπτης απενεργοποιείται ξανά.</string>
|
||||
<string name="gate_open_dev">Άνοιγμα Επιλογών προγραμματιστή</string>
|
||||
<string name="gate_no_toggle">Αυτή η έκδοση Android δεν έχει διακόπτη για αυτό, οπότε το DroidDeck το αλλάζει μέσω Ασύρματου εντοπισμού σφαλμάτων. Διαρκεί περίπου ένα λεπτό, χωρίς υπολογιστή.</string>
|
||||
<string name="gate_needs_wifi">Ο Ασύρματος εντοπισμός σφαλμάτων απαιτεί Wi-Fi. Οποιοδήποτε δίκτυο λειτουργεί· δεν απαιτείται σύνδεση στο διαδίκτυο.</string>
|
||||
@@ -1009,7 +1029,6 @@
|
||||
<string name="gpu_set_with_runtime">Ορίζεται μαζί με το πρόγραμμα οδήγησης runtime: το %1$s φέρει και τα δύο.</string>
|
||||
<string name="gpu_runtime_hint">Χρησιμοποιείται από παιχνίδια και την επιφάνεια εργασίας.</string>
|
||||
<string name="gpu_runtime_default">Προεπιλογή runtime</string>
|
||||
<string name="gpu_display_hint">Σχεδιάζει καρέ στην οθόνη. Εφαρμόζεται μετά την επανεκκίνηση του DroidDeck.</string>
|
||||
<string name="gpu_auto_by_gpu">Αυτόματο - επιλέγεται από τη GPU</string>
|
||||
<string name="gpu_manage">Λήψη, εισαγωγή ή διαγραφή…</string>
|
||||
<string name="gpu_in_use_caps">ΣΕ ΧΡΗΣΗ</string>
|
||||
@@ -1239,10 +1258,16 @@
|
||||
<string name="drivers_downloading_pair">Λήψη %1$s %2$s…</string>
|
||||
<string name="drivers_updated_to">Ενημερώθηκε σε %1$s %2$s.</string>
|
||||
<string name="drivers_updated_to_restart">Ενημερώθηκε σε %1$s %2$s. Το πρόγραμμα οδήγησης οθόνης εφαρμόζεται μετά την επανεκκίνηση του DroidDeck.</string>
|
||||
<string name="drivers_auto_waits_for_session">Οι ενημερώσεις προγραμμάτων οδήγησης περιμένουν να σταματήσει η συνεδρία.</string>
|
||||
<string name="drivers_auto_waits_for_network">Το %1$s %2$s θα ληφθεί μόλις βγεις από δεδομένα κινητής και εξοικονόμηση μπαταρίας.</string>
|
||||
<string name="drivers_skipping_bad">Το %1$s %2$s δεν ξεκίνησε συνεδρία εδώ, οπότε το Αυτόματο μένει στα προγράμματα οδήγησης που ξεκίνησαν.</string>
|
||||
<string name="drivers_rolled_back">Το %1$s δεν ξεκίνησε συνεδρία δύο φορές, οπότε το Αυτόματο επέστρεψε στα προγράμματα οδήγησης που δούλευαν.</string>
|
||||
<string name="drivers_switched_to">Έγινε αλλαγή σε %1$s %2$s.</string>
|
||||
<string name="drivers_switched_to_restart">Έγινε αλλαγή σε %1$s %2$s. Το πρόγραμμα οδήγησης οθόνης εφαρμόζεται μετά την επανεκκίνηση του DroidDeck.</string>
|
||||
<string name="drivers_using">Χρήση %1$s %2$s.</string>
|
||||
<string name="drivers_using_restart">Χρήση %1$s %2$s. Το πρόγραμμα οδήγησης οθόνης εφαρμόζεται μετά την επανεκκίνηση του DroidDeck.</string>
|
||||
<string name="drivers_using_quest">Χρησιμοποιείται το ενσωματωμένο πρόγραμμα οδήγησης εικόνας για Quest και το προεπιλεγμένο περιβάλλον.</string>
|
||||
<string name="drivers_using_quest_restart">Χρησιμοποιείται το ενσωματωμένο πρόγραμμα οδήγησης εικόνας για Quest και το προεπιλεγμένο περιβάλλον. Το πρόγραμμα οδήγησης εικόνας ισχύει μετά από επανεκκίνηση του DroidDeck.</string>
|
||||
<string name="drivers_not_a_driver">Δεν είναι zip προγράμματος οδήγησης: ούτε πρόγραμμα οδήγησης AdrenoTools, ούτε -Linux Turnip, ούτε πακέτο Android + Linux</string>
|
||||
<string name="drivers_import_failed">Η εισαγωγή απέτυχε: %1$s</string>
|
||||
<string name="drivers_imported_unnamed">Έγινε εισαγωγή προγράμματος οδήγησης</string>
|
||||
@@ -1664,7 +1689,7 @@
|
||||
<string name="adbpair_done_text">Πατήστε για να επιστρέψετε στο DroidDeck. Μπορείτε να απενεργοποιήσετε τον ασύρματο εντοπισμό σφαλμάτων τώρα.</string>
|
||||
<string name="adbpair_title_failed">Η ρύθμιση ασύρματου εντοπισμού σφαλμάτων διακόπηκε</string>
|
||||
|
||||
<string name="selfinst_http">Η λήψη απάντησε με HTTP %1$d</string>
|
||||
<string name="selfinst_download_failed">Η λήψη απέτυχε</string>
|
||||
<string name="selfinst_size_mismatch">Το μέγεθος της λήψης δεν ταίριαζε με το δημοσιευμένο APK και απορρίφθηκε - δοκιμάστε ξανά</string>
|
||||
<string name="selfinst_checksum_mismatch">Η λήψη δεν ταίριαζε με το άθροισμα ελέγχου της και απορρίφθηκε - δοκιμάστε ξανά</string>
|
||||
<string name="selfinst_wrong_package">Η ενημέρωση είναι για το πακέτο Android %1$s, όχι για το %2$s</string>
|
||||
@@ -1762,7 +1787,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">Το όριο ρυθμού του GitHub ξεπεράστηκε - δοκιμάστε ξανά αργότερα</string>
|
||||
<string name="turnip_github_http">Το GitHub απάντησε με HTTP %1$d</string>
|
||||
<string name="turnip_download_http">η λήψη απάντησε με HTTP %1$d</string>
|
||||
<string name="turnip_download_failed">Η λήψη απέτυχε</string>
|
||||
<string name="turnip_list_predates_checksums">Αυτή η λίστα προγραμμάτων οδήγησης προηγείται των αθροισμάτων ελέγχου - ανανεώστε την και δοκιμάστε ξανά</string>
|
||||
<string name="turnip_checksum_mismatch">Αναντιστοιχία αθροίσματος ελέγχου - η λήψη απορρίφθηκε</string>
|
||||
<string name="turnip_not_adrenotools">Δεν είναι πρόγραμμα οδήγησης AdrenoTools: %1$s</string>
|
||||
@@ -1799,7 +1824,7 @@
|
||||
<string name="phantom_instructions_fix">%1$s Αν αυτό δεν είναι δυνατό, συνδέστε αυτή τη συσκευή σε έναν υπολογιστή με ADB και εκτελέστε την παρακάτω εντολή. Επιστρέψτε εδώ και ελέγξτε ξανά.</string>
|
||||
<string name="phantom_instructions_off">Οι συνεδρίες Steam μπορούν να ξεκινήσουν.</string>
|
||||
<string name="phantom_instructions_not_applicable">Αυτή η έκδοση Android δεν χρησιμοποιεί αυτό το όριο.</string>
|
||||
<string name="phantom_fix_toggle">Ενεργοποιήστε την «Απενεργοποίηση περιορισμών θυγατρικών διαδικασιών» στις Επιλογές προγραμματιστή και αφήστε τις Επιλογές προγραμματιστή ενεργές μετά.</string>
|
||||
<string name="phantom_fix_toggle">Ενεργοποιήστε την «Απενεργοπ. περιορισμών σε θυγατρικές διεργασίες» στις Επιλογές προγραμματιστή και αφήστε τις Επιλογές προγραμματιστή ενεργές μετά.</string>
|
||||
<string name="phantom_fix_no_toggle">Αυτή η έκδοση Android δεν έχει διακόπτη για αυτό. Χρησιμοποιήστε τον ασύρματο εντοπισμό σφαλμάτων για να το αλλάξετε από αυτή τη συσκευή.</string>
|
||||
<string name="upd_block_no_apk">Αυτή η έκδοση δεν έχει λήψη για αυτό το αντίγραφο του DroidDeck.</string>
|
||||
<string name="upd_block_version_code">Το Android δεν θα το εγκαταστήσει πάνω από αυτήν την έκδοση επειδή το versionCode %1$d είναι μικρότερο από το εγκατεστημένο %2$d.</string>
|
||||
@@ -2031,6 +2056,9 @@
|
||||
<string name="stores_install_title">Εγκατάσταση %1$s:</string>
|
||||
<string name="stores_target_internal">Εσωτερική</string>
|
||||
<string name="stores_target_free">%1$s ελεύθερα</string>
|
||||
<string name="stores_target_free_unknown">Ο ελεύθερος χώρος είναι άγνωστος</string>
|
||||
<string name="stores_storage_insufficient">Λίγος ελεύθερος χώρος: χρειάζονται %1$s, διαθέσιμα μόνο %2$s</string>
|
||||
<string name="stores_storage_low">Λίγος χώρος (%1$s ελεύθερα, χρειάζονται %2$s)</string>
|
||||
<string name="stores_notification_channel">Λήψεις καταστήματος</string>
|
||||
<string name="stores_notification_channel_hint">Εμφανίζει την πρόοδο λήψης παιχνιδιών και κρατά τις λήψεις ενεργές στο παρασκήνιο</string>
|
||||
<string name="stores_notification_one">Γίνεται λήψη</string>
|
||||
@@ -2066,6 +2094,21 @@
|
||||
<string name="edit_other_exe">Άλλο exe…</string>
|
||||
<string name="edit_remove">Κατάργηση</string>
|
||||
<string name="art_cover">Εξώφυλλο</string>
|
||||
<string name="art_wide_cover">Φαρδύ εξώφυλλο</string>
|
||||
<string name="art_adjust_logo">Προσαρμογή λογότυπου</string>
|
||||
<string name="logo_pos_default">Προεπιλογή</string>
|
||||
<string name="logo_pos_upper_left">Πάνω αριστερά</string>
|
||||
<string name="logo_pos_upper_center">Πάνω κέντρο</string>
|
||||
<string name="logo_pos_center">Κέντρο</string>
|
||||
<string name="logo_pos_bottom_left">Κάτω αριστερά</string>
|
||||
<string name="logo_pos_bottom_center">Κάτω κέντρο</string>
|
||||
<string name="logo_pos_position">Θέση</string>
|
||||
<string name="logo_pos_size">Μέγεθος</string>
|
||||
<string name="logo_pos_save">Αποθήκευση</string>
|
||||
<string name="logo_pos_reset">Επαναφορά στην προεπιλογή</string>
|
||||
<string name="logo_pos_hint">Σχεδιάζεται πάνω από το φόντο του παιχνιδιού, όπως κάνει το Steam. Η ακριβής θέση μπορεί να διαφέρει μερικά pixel.</string>
|
||||
<string name="logo_pos_no_logo">Αυτό το παιχνίδι δεν έχει λογότυπο ακόμη. Διάλεξε ένα στο Λογότυπο για να το δεις εδώ.</string>
|
||||
<string name="logo_pos_failed">Η θέση του λογότυπου δεν αποθηκεύτηκε. Δοκίμασε ξανά.</string>
|
||||
<string name="art_background">Φόντο</string>
|
||||
<string name="art_logo">Λογότυπο</string>
|
||||
<string name="art_icon">Εικονίδιο</string>
|
||||
@@ -2078,6 +2121,17 @@
|
||||
<string name="art_pick_image">Επιλογή εικόνας…</string>
|
||||
<string name="art_pick_title">Επιλέξτε μια εικόνα: %1$s</string>
|
||||
<string name="art_reset">Επαναφορά στο αυτόματο</string>
|
||||
<string name="art_search_sgdb">Αναζήτηση στο SteamGridDB…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB: %1$s</string>
|
||||
<string name="art_sgdb_search_label">Αναζήτηση παιχνιδιού</string>
|
||||
<string name="art_sgdb_other_games">Άλλα αποτελέσματα</string>
|
||||
<string name="art_sgdb_none">Κανένα αποτέλεσμα.</string>
|
||||
<string name="art_sgdb_no_key">Το SteamGridDB χρειάζεται κλειδί API. Πρόσθεσε το δικό σου στις Ρυθμίσεις › Εκκινητής.</string>
|
||||
<string name="art_sgdb_bad_key">Το SteamGridDB δεν δέχτηκε το κλειδί API. Έλεγξέ το στις Ρυθμίσεις › Εκκινητής.</string>
|
||||
<string name="art_sgdb_limit">Το SteamGridDB περιορίζει τα αιτήματα τώρα. Δοκίμασε ξανά σε ένα λεπτό.</string>
|
||||
<string name="art_sgdb_offline">Το SteamGridDB δεν είναι προσβάσιμο. Έλεγξε τη σύνδεση.</string>
|
||||
<string name="art_sgdb_server">Κάτι πήγε στραβά στο SteamGridDB. Δοκίμασε αργότερα.</string>
|
||||
<string name="art_sgdb_no_game">Το SteamGridDB δεν έχει αποτελέσματα για αυτό το παιχνίδι. Δοκίμασε να το αναζητήσεις.</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="one">%1$d παιχνίδι προστέθηκε</item>
|
||||
<item quantity="other">%1$d παιχνίδια προστέθηκαν</item>
|
||||
@@ -2105,6 +2159,45 @@
|
||||
<string name="gpu_bar_auto">Αυτόματο · %1$s</string>
|
||||
<string name="gpu_separately">Προγράμματα οδήγησης runtime και οθόνης ξεχωριστά</string>
|
||||
<string name="gpu_use_title">Χρήση %1$s;</string>
|
||||
<string name="gpu_mode_title">Πώς επιλέγονται τα προγράμματα οδήγησης</string>
|
||||
<string name="gpu_mode_auto_detail">Το καλύτερο ζεύγος για αυτό το GPU, πάντα ενημερωμένο</string>
|
||||
<string name="gpu_mode_pair">Ταιριαστό ζεύγος</string>
|
||||
<string name="gpu_mode_pair_detail">Διάλεξε ζεύγος· ορίζει και τα δύο προγράμματα οδήγησης</string>
|
||||
<string name="gpu_mode_mix">Ελεύθερος συνδυασμός</string>
|
||||
<string name="gpu_mode_mix_detail">Διάλεξε εικόνα και περιβάλλον χωριστά</string>
|
||||
<string name="gpu_mode_pair_short">Ζεύγος</string>
|
||||
<string name="gpu_mode_mix_short">Μικτό</string>
|
||||
<string name="gpu_src_auto">ΑΥΤΟ</string>
|
||||
<string name="gpu_src_pair">ΖΕΥΓΟΣ</string>
|
||||
<string name="gpu_src_bundle">ΠΑΚΕΤΟ</string>
|
||||
<string name="gpu_src_chosen">ΕΠΙΛΕΓΜΕΝΟ</string>
|
||||
<string name="gpu_src_by_gpu">ΑΝΑ GPU</string>
|
||||
<string name="gpu_src_default">ΠΡΟΕΠΙΛΟΓΗ</string>
|
||||
<string name="gpu_display_live">Σχεδιάζει τώρα</string>
|
||||
<string name="gpu_display_pending">Απαιτείται επανεκκίνηση · μέχρι τότε σχεδιάζει το %1$s</string>
|
||||
<string name="gpu_runtime_live">Ενεργό</string>
|
||||
<string name="gpu_runtime_pending">Επόμενη συνεδρία · αυτή κρατά το %1$s</string>
|
||||
<string name="gpu_display_next">Σχεδιάζει την οθόνη. Ισχύει από την επόμενη συνεδρία.</string>
|
||||
<string name="gpu_previous_driver">το προηγούμενο πρόγραμμα οδήγησης</string>
|
||||
<string name="gpu_restart_banner">Κάνε επανεκκίνηση για να αλλάξει το πρόγραμμα οδήγησης εικόνας.</string>
|
||||
<string name="gpu_restart_blocked">Σταμάτησε την τρέχουσα συνεδρία και κάνε επανεκκίνηση το DroidDeck για να αλλάξει το πρόγραμμα οδήγησης εικόνας.</string>
|
||||
<string name="gpu_restart_now">Επανεκκίνηση τώρα</string>
|
||||
<string name="gpu_restart_later">Αργότερα</string>
|
||||
<string name="gpu_auto_title">Το Αυτόματο διάλεξε αυτό το ζεύγος για το %1$s σου</string>
|
||||
<string name="gpu_auto_builtin_title">Το Αυτόματο χρησιμοποιεί τα ενσωματωμένα προγράμματα οδήγησης για το %1$s σου</string>
|
||||
<string name="gpu_auto_reason">Επιλέχθηκε για την οικογένεια %1$s. Νέες εκδόσεις εγκαθίστανται στο παρασκήνιο.</string>
|
||||
<string name="gpu_pair_title">Διάλεξε ζεύγος</string>
|
||||
<string name="gpu_pair_hint">Το ζεύγος ορίζει μαζί το πρόγραμμα οδήγησης εικόνας και runtime.</string>
|
||||
<string name="gpu_mix_title">Διάλεξε κάθε πρόγραμμα οδήγησης</string>
|
||||
<string name="gpu_mix_hint">Το πακέτο γεμίζει και τις δύο θέσεις. Οι αλλαγές εικόνας ισχύουν μετά από επανεκκίνηση, οι αλλαγές περιβάλλοντος από την επόμενη συνεδρία.</string>
|
||||
<string name="gpu_change_display">Αλλαγή προγράμματος οδήγησης εικόνας</string>
|
||||
<string name="gpu_change_runtime">Αλλαγή προγράμματος οδήγησης runtime</string>
|
||||
<string name="gpu_autos_pick">ΕΠΙΛΟΓΗ ΑΥΤΟΜΑΤΟΥ</string>
|
||||
<string name="gpu_installed">Εγκατεστημένο</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · επανεκκίνηση για εφαρμογή</string>
|
||||
<string name="gpu_chip_restart">%1$s · απαιτείται επανεκκίνηση</string>
|
||||
<string name="comp_this_device">Αυτή η συσκευή</string>
|
||||
<string name="proton_remove_title">Κατάργηση %1$s;</string>
|
||||
<string name="setup_tab_diagnostics">Διαγνωστικά</string>
|
||||
<string name="setup_ready">Έτοιμο για παιχνίδι</string>
|
||||
@@ -2116,6 +2209,14 @@
|
||||
<string name="setup_group_look">Εμφάνιση</string>
|
||||
<string name="setup_group_device">Αυτή η συσκευή</string>
|
||||
<string name="setup_group_art">Γραφικά βιβλιοθήκης</string>
|
||||
<string name="setup_group_download">Λήψη</string>
|
||||
<string name="setup_download_source">Πηγή</string>
|
||||
<string name="setup_download_source_auto">Αυτόματη</string>
|
||||
<string name="setup_download_source_direct">Άμεση</string>
|
||||
<string name="setup_download_source_mirror">Καθρέφτης</string>
|
||||
<string name="setup_download_source_custom">Δικό σου prefix</string>
|
||||
<string name="setup_download_mirror">Ιστότοπος</string>
|
||||
<string name="setup_download_prefix">Prefix</string>
|
||||
<string name="flathub_eyebrow">ΕΦΑΡΜΟΓΕΣ LINUX</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">Εφαρμογές επιφάνειας εργασίας για την επιφάνεια εργασίας Linux, ανοίγουν από την Επιφάνεια εργασίας.</string>
|
||||
@@ -2223,12 +2324,9 @@
|
||||
<string name="cardboard_recenter_button">Επανακεντράρισμα</string>
|
||||
<string name="cardboard_steamvr">SteamVR (πειραματικό)</string>
|
||||
<string name="cardboard_steamvr_install">Εγκατάσταση</string>
|
||||
<string name="cardboard_steamvr_install_hint">Κατεβάζει το SteamVR ARM64 της Valve (571 MB).</string>
|
||||
<string name="cardboard_steamvr_installing">Λήψη και αποσυμπίεση του SteamVR…</string>
|
||||
<string name="cardboard_steamvr_start">Έναρξη</string>
|
||||
<string name="cardboard_steamvr_ready_hint">Εκτελεί το SteamVR με το κεφάλι σας ως headset· η εικόνα αλλάζει σε 3D δίπλα-δίπλα.</string>
|
||||
<string name="cardboard_steamvr_stop">Διακοπή</string>
|
||||
<string name="cardboard_steamvr_running_hint">Το SteamVR εκτελείται.</string>
|
||||
<string name="cardboard_steamvr_unavailable">Δεν είναι διαθέσιμο σε αυτή τη συνεδρία.</string>
|
||||
<string name="cardboard_steamvr_failed">Το SteamVR δεν αποκρίθηκε· ελέγξτε το αρχείο καταγραφής της συνεδρίας.</string>
|
||||
<string name="drawer_component_downloads">Λήψη στοιχείων</string>
|
||||
@@ -2242,4 +2340,195 @@
|
||||
<string name="stores_hltb_completionist">Ολοκλήρωση</string>
|
||||
<string name="stores_hltb_hours">%1$s ώ.</string>
|
||||
<string name="stores_hltb_minutes">%1$d λεπ.</string>
|
||||
<string name="display_picture_position">Θέση εικόνας</string>
|
||||
<string name="display_position_top">Πάνω</string>
|
||||
<string name="display_position_bottom">Κάτω</string>
|
||||
<string name="display_position_center">Κέντρο (προεπιλογή)</string>
|
||||
<string name="setup_tab_storage">Αποθήκευση</string>
|
||||
<string name="storage_measuring">Μέτρηση αποθήκευσης…</string>
|
||||
<string name="storage_measuring_hint">Την πρώτη φορά παίρνει λίγα δευτερόλεπτα.</string>
|
||||
<string name="storage_internal">Εσωτερική αποθήκευση</string>
|
||||
<string name="storage_used_of">χρησιμοποιούνται %1$s από %2$s</string>
|
||||
<string name="storage_free">%1$s ελεύθερα</string>
|
||||
<string name="storage_droiddeck_uses">Το DroidDeck χρησιμοποιεί %1$s. Μετρήθηκε στις %2$s.</string>
|
||||
<string name="storage_rescanning">νέα μέτρηση…</string>
|
||||
<string name="storage_other_apps">Άλλες εφαρμογές Android</string>
|
||||
<string name="storage_safe_now">Μπορούν να ελευθερωθούν τώρα</string>
|
||||
<string name="storage_safe_hint">Αχρησιμοποίητα περιβάλλοντα, υπολείμματα, παλιά αρχεία καταγραφής</string>
|
||||
<string name="storage_clean_up">Καθαρισμός…</string>
|
||||
<string name="storage_session_running">Σταμάτησε τη συνεδρία για να καθαρίσεις ή να διαγράψεις κάτι. Τα μεγέθη παραμένουν ορατά.</string>
|
||||
<string name="storage_where">Πού πηγαίνει</string>
|
||||
<string name="storage_games">Παιχνίδια Steam</string>
|
||||
<string name="storage_game_data">Δεδομένα παιχνιδιών</string>
|
||||
<string name="storage_game_data_hint">Prefix %1$s · workshop %2$s · shaders %3$s</string>
|
||||
<string name="storage_tools">Proton και περιβάλλοντα</string>
|
||||
<string name="storage_tools_unused">%1$s αχρησιμοποίητα</string>
|
||||
<string name="storage_stores">GOG, Epic και άλλα καταστήματα</string>
|
||||
<string name="storage_stores_hint">Αφαίρεσέ τα στα Καταστήματα</string>
|
||||
<string name="storage_desktop">Εφαρμογές επιφάνειας εργασίας</string>
|
||||
<string name="storage_desktop_hint">Εφαρμογές Flatpak και περιβάλλοντα</string>
|
||||
<string name="storage_system">Σύστημα Linux και πελάτης Steam</string>
|
||||
<string name="storage_system_hint">Απαραίτητα για να τρέξει οτιδήποτε</string>
|
||||
<string name="storage_app_data">Δεδομένα DroidDeck</string>
|
||||
<string name="storage_app_data_hint">Αρχεία καταγραφής παλιών συνεδριών %1$s · προσωρινή μνήμη · προγράμματα οδήγησης</string>
|
||||
<string name="storage_legend_games">Παιχνίδια</string>
|
||||
<string name="storage_legend_data">Δεδομένα παιχνιδιών</string>
|
||||
<string name="storage_legend_tools">Proton και περιβάλλοντα</string>
|
||||
<string name="storage_legend_stores">Άλλα καταστήματα</string>
|
||||
<string name="storage_legend_desktop">Εφαρμογές επιφάνειας εργασίας</string>
|
||||
<string name="storage_legend_system">Σύστημα Linux</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">Καθαρισμός</string>
|
||||
<string name="storage_clean_up_lede">Τίποτα εδώ δεν αγγίζει εγκατεστημένα παιχνίδια ή λήψεις σε εξέλιξη.</string>
|
||||
<string name="storage_nothing_to_clean">Δεν υπάρχει κάτι για καθάρισμα τώρα.</string>
|
||||
<string name="storage_selected">Επιλεγμένα: %1$s · %2$s → %3$s θα ελευθερωθούν</string>
|
||||
<string name="storage_cleaning">Καθαρισμός…</string>
|
||||
<string name="storage_free_button">Ελευθέρωση %1$s</string>
|
||||
<string name="storage_freed">Ελευθερώθηκαν %1$s.</string>
|
||||
<string name="storage_item_runtimes">Αχρησιμοποίητα περιβάλλοντα Steam και Proton</string>
|
||||
<string name="storage_item_leftovers">Υπολείμματα διαγραμμένων παιχνιδιών</string>
|
||||
<string name="storage_item_leftovers_unsure">Υπολείμματα που μπορεί να περιέχουν αποθηκεύσεις</string>
|
||||
<string name="storage_item_logs">Αρχεία καταγραφής παλιών συνεδριών</string>
|
||||
<string name="storage_item_logs_detail">Τα αρχεία καταγραφής της τρέχουσας συνεδρίας παραμένουν</string>
|
||||
<string name="storage_item_custom_detail">Κανένα παιχνίδι δεν το χρησιμοποιεί. Εγκατέστησέ το ξανά στα Στοιχεία αν το χρειαστείς.</string>
|
||||
<string name="storage_item_shaders">Προσωρινή μνήμη shaders των εγκατεστημένων παιχνιδιών</string>
|
||||
<string name="storage_item_shaders_detail">Ξαναχτίζεται όσο παίζεις. Τα πρώτα λεπτά κάθε παιχνιδιού θα κολλάνε ξανά.</string>
|
||||
<string name="storage_tag_safe">Ασφαλή</string>
|
||||
<string name="storage_tag_unused">Αχρησιμοποίητα</string>
|
||||
<string name="storage_tag_stutter">Κολλήματα</string>
|
||||
<string name="storage_tag_saves">Έλεγξε τις αποθηκεύσεις</string>
|
||||
<string name="storage_tag_default">Προεπιλογή</string>
|
||||
<string name="storage_tag_required">Απαραίτητα</string>
|
||||
<string name="storage_tag_not_used">Αχρησιμοποίητα</string>
|
||||
<string name="storage_back">Αποθήκευση</string>
|
||||
<string name="storage_games_with_data">%1$s με τα δεδομένα τους</string>
|
||||
<string name="storage_sort_largest">Μεγαλύτερα</string>
|
||||
<string name="storage_sort_least_played">Λιγότερο παιγμένα</string>
|
||||
<string name="storage_sort_sd">Σε κάρτα SD</string>
|
||||
<string name="storage_no_games">Δεν υπάρχουν παιχνίδια εδώ.</string>
|
||||
<string name="storage_played">παίχτηκε %1$s</string>
|
||||
<string name="storage_never_played">Δεν παίχτηκε ποτέ</string>
|
||||
<string name="storage_game_parts">Παιχνίδι %1$s · δεδομένα %2$s</string>
|
||||
<string name="storage_uninstall">Κατάργηση εγκατάστασης</string>
|
||||
<string name="storage_games_note">Η κατάργηση εγκατάστασης ανοίγει το παράθυρο του Steam για να το καταγράψει. Για να μετακινήσεις παιχνίδι στην κάρτα SD, χρησιμοποίησε Steam › Ρυθμίσεις › Αποθήκευση.</string>
|
||||
<string name="storage_uninstall_title">Κατάργηση εγκατάστασης του %1$s;</string>
|
||||
<string name="storage_uninstall_body">Το Steam ανοίγει και ζητά επιβεβαίωση. Έτσι ελευθερώνονται τα %1$s του παιχνιδιού. Το prefix μένει μέχρι να καθαρίσεις τα υπολείμματα, και οι αποθηκεύσεις Steam Cloud μένουν στον λογαριασμό σου.</string>
|
||||
<string name="storage_open_steam">Άνοιγμα Steam</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s αχρησιμοποίητα</string>
|
||||
<string name="storage_remove_unused">Αφαίρεση αχρησιμοποίητων · %1$s</string>
|
||||
<string name="storage_in_use">Σε χρήση</string>
|
||||
<string name="storage_not_used">Αχρησιμοποίητο</string>
|
||||
<string name="storage_remove">Αφαίρεση</string>
|
||||
<string name="storage_remove_title">Αφαίρεση του %1$s;</string>
|
||||
<string name="storage_remove_body">Ελευθερώνει %1$s. Το Steam θα το ξανακατεβάσει αν το χρειαστεί κάποιο παιχνίδι.</string>
|
||||
<string name="storage_tool_fex_runtime">Απαραίτητο για παιχνίδια Linux x86 (FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">Απαραίτητο για Proton ARM64</string>
|
||||
<string name="storage_tool_redist">Εγκαταστάσεις VC++ και DirectX που χρειάζονται τα παιχνίδια</string>
|
||||
<string name="storage_tool_fex">Τρέχει παιχνίδια Linux x86</string>
|
||||
<string name="storage_tool_steam">Εργαλείο Steam</string>
|
||||
<string name="storage_tool_old_runtime">Το DroidDeck τρέχει παιχνίδια χωρίς αυτό το περιβάλλον</string>
|
||||
<string name="storage_tool_x86_proton">Proton x86, που το DroidDeck δεν μπορεί να τρέξει</string>
|
||||
<string name="storage_tool_custom_default">Προεπιλογή για όλα τα παιχνίδια</string>
|
||||
<string name="storage_tool_custom_unused">Κανένα παιχνίδι δεν το χρησιμοποιεί</string>
|
||||
<string name="storage_tool_custom_unknown">Οι ρυθμίσεις του Steam δεν διαβάζονται, οπότε διατηρείται</string>
|
||||
<string name="storage_cleanup_busy">Περίμενε να τελειώσει ο καθαρισμός αποθήκευσης</string>
|
||||
<string name="storage_low_title">Μόνο %1$s ελεύθερα</string>
|
||||
<string name="storage_low_body">Το Steam χρειάζεται χώρο για ενημερώσεις και προσωρινή μνήμη shaders. Κάτω από 10 GB οι ενημερώσεις παιχνιδιών μπορεί να αποτύχουν στη μέση.</string>
|
||||
<string name="storage_low_safe">Τα %1$s μπορούν να ελευθερωθούν με ασφάλεια.</string>
|
||||
<string name="storage_free_up">Ελευθέρωσε χώρο</string>
|
||||
<string name="storage_play_anyway">Παίξε παρόλα αυτά</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d εγκατεστημένο</item>
|
||||
<item quantity="other">%d εγκατεστημένα</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d σε κάρτα SD</item>
|
||||
<item quantity="other">%d σε κάρτα SD</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">%d εργαλείο</item>
|
||||
<item quantity="other">%d εργαλεία</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">Κενοί φάκελοι prefix και προσωρινή μνήμη shaders από %d παιχνίδι</item>
|
||||
<item quantity="other">Κενοί φάκελοι prefix και προσωρινή μνήμη shaders από %d παιχνίδια</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">Το prefix του %d παιχνιδιού περιέχει ακόμη αρχεία, μεταξύ τους τοπικές αποθηκεύσεις. Κράτα αντίγραφο πρώτα αν τα χρειάζεσαι.</item>
|
||||
<item quantity="other">Τα prefix των %d παιχνιδιών περιέχουν ακόμη αρχεία, μεταξύ τους τοπικές αποθηκεύσεις. Κράτα αντίγραφο πρώτα αν τα χρειάζεσαι.</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">%d παιχνίδι</item>
|
||||
<item quantity="other">%d παιχνίδια</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">Προεπιλεγμένο Proton του DroidDeck</string>
|
||||
<string name="storage_tool_proton">Επιλογή Proton 11 του DroidDeck</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">Χρησιμοποιείται από %d παιχνίδι</item>
|
||||
<item quantity="other">Χρησιμοποιείται από %d παιχνίδια</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">Στοιχεία</string>
|
||||
<string name="storage_components">Στοιχεία</string>
|
||||
<string name="storage_item_components">Αχρησιμοποίητα στοιχεία</string>
|
||||
<string name="storage_component_original">Αρχικό %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">Ελευθερώνει %1$s. Επαναφέρει μόνο έκδοση Proton που δεν είναι πια εγκατεστημένη.</string>
|
||||
<string name="storage_leftovers_unchecked">Χωρίς έλεγχο: το manifest μιας εφαρμογής Steam δεν διαβάζεται, οπότε τα prefix διαγραμμένων παιχνιδιών μένουν μέχρι να γίνει εφικτό.</string>
|
||||
<string name="storage_failed">Απέτυχε: %1$s</string>
|
||||
<string name="storage_desktop_shared">%1$s κοινά</string>
|
||||
<string name="storage_data_installed">Εγκατεστημένα παιχνίδια</string>
|
||||
<string name="storage_data_parts">Prefix %1$s · shaders %2$s · workshop %3$s</string>
|
||||
<string name="storage_clear_shaders">Εκκαθάριση shaders</string>
|
||||
<string name="storage_data_leftovers">Διαγραμμένα παιχνίδια</string>
|
||||
<string name="storage_steam_app">Εφαρμογή Steam %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">Το prefix περιέχει ακόμη αρχεία, ίσως τοπικές αποθηκεύσεις</string>
|
||||
<string name="storage_leftover_confirm">Καθαρίζει το prefix Wine και την προσωρινή μνήμη shaders (%1$s). Κράτα πρώτα αντίγραφο των αποθηκεύσεων που χρειάζεσαι· οι αποθηκεύσεις Steam Cloud μένουν στον λογαριασμό σου.</string>
|
||||
<string name="storage_data_other">Διατηρούνται</string>
|
||||
<string name="storage_data_other_title">Παιχνίδια εκτός Steam και κοινό prefix Steam</string>
|
||||
<string name="storage_data_other_detail">Prefix παιχνιδιών που προστέθηκαν από καταστήματα ή χειροκίνητα</string>
|
||||
<string name="storage_components_packages">Ληφθέντα πακέτα</string>
|
||||
<string name="storage_components_originals">Διατηρημένα αρχικά</string>
|
||||
<string name="storage_components_none">Δεν έχουν ληφθεί πακέτα στοιχείων.</string>
|
||||
<string name="storage_component_in_use">Επιλεγμένο για Proton ή παιχνίδι</string>
|
||||
<string name="storage_component_unused">Δεν έχει επιλεγεί πουθενά</string>
|
||||
<string name="storage_original_kept">Επαναφέρει το εγκατεστημένο Proton</string>
|
||||
<string name="storage_original_old">Έκδοση Proton που δεν είναι πια εγκατεστημένη</string>
|
||||
<string name="storage_tag_in_use">Σε χρήση</string>
|
||||
<string name="storage_components_note">Τα στοιχεία σε χρήση μένουν κλειδωμένα. Διάλεξέ τα στα Στοιχεία· ό,τι αφαιρέσεις εδώ μπορείς να το ξανακατεβάσεις εκεί.</string>
|
||||
<string name="storage_component_confirm">Ελευθερώνει %1$s. Πακέτα που εισήγαγες μόνος σου δεν ξανακατεβαίνουν.</string>
|
||||
<string name="storage_desktop_detail">%1$s για εφαρμογές Flatpak και ό,τι μοιράζονται</string>
|
||||
<string name="storage_desktop_apps_title">Εφαρμογές</string>
|
||||
<string name="storage_desktop_none">Δεν υπάρχουν εγκατεστημένες εφαρμογές Flatpak.</string>
|
||||
<string name="storage_desktop_shared_title">Κοινά από τις εφαρμογές</string>
|
||||
<string name="storage_desktop_runtimes">Περιβάλλοντα</string>
|
||||
<string name="storage_desktop_runtimes_detail">Οι βιβλιοθήκες στις οποίες τρέχουν οι εφαρμογές· τα αχρησιμοποίητα φεύγουν με Αφαίρεση αχρησιμοποίητων</string>
|
||||
<string name="storage_desktop_cache">Αποθήκευση λήψεων</string>
|
||||
<string name="storage_desktop_cache_detail">Χώρος αντικειμένων και ληφθείσα προσωρινή μνήμη του Flatpak</string>
|
||||
<string name="storage_desktop_prune">Αφαίρεση αχρησιμοποίητων</string>
|
||||
<string name="storage_desktop_prune_body">Αφαιρεί περιβάλλοντα που δεν χρειάζεται καμία εγκατεστημένη εφαρμογή και καθαρίζει τον χώρο αντικειμένων του Flatpak. Οι εγκατεστημένες εφαρμογές συνεχίζουν να δουλεύουν.</string>
|
||||
<string name="storage_flatpak_uninstall_body">Αφαιρεί την εφαρμογή (%1$s) και τα περιβάλλοντα που δεν χρησιμοποιεί κανείς άλλος. Τα δεδομένα στον προσωπικό σου φάκελο μένουν.</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">%d πακέτο</item>
|
||||
<item quantity="other">%d πακέτα</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">%d εφαρμογή</item>
|
||||
<item quantity="other">%d εφαρμογές</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">Βίντεο εκκίνησης</string>
|
||||
<string name="boot_movie_video">Βίντεο</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d fps</string>
|
||||
<string name="boot_movie_note">Τα 60 fps φαίνονται πιο ομαλά, αλλά επιβαρύνουν περισσότερο τον επεξεργαστή όσο ξεκινά το Steam. Ένα βίντεο εκκίνησης που επιλέχθηκε στις ρυθμίσεις του Steam έχει προτεραιότητα.</string>
|
||||
<string name="boot_movie_sound">Ήχος</string>
|
||||
<string name="boot_movie_custom">Δικό σας βίντεο</string>
|
||||
<string name="boot_movie_custom_hint">MP4, MOV, MKV ή WebM. Άλλες μορφές και αρχεία πάνω από %1$d MB μετατρέπονται στη συσκευή σε WebM 720p, έως %2$d δευτερόλεπτα.</string>
|
||||
<string name="boot_movie_import">Εισαγωγή</string>
|
||||
<string name="boot_movie_remove">Αφαίρεση</string>
|
||||
<string name="boot_movie_pick">Επιλέξτε βίντεο εκκίνησης</string>
|
||||
<string name="boot_movie_imported">Το %1$s είναι πλέον το βίντεο εκκίνησης</string>
|
||||
<string name="boot_movie_imported_silent">Το %1$s είναι πλέον το βίντεο εκκίνησης, χωρίς ήχο</string>
|
||||
<string name="boot_movie_too_long">Τα βίντεο προς μετατροπή πρέπει να διαρκούν έως %1$d δευτερόλεπτα</string>
|
||||
<string name="boot_movie_converting">Μετατροπή %1$d%%</string>
|
||||
<string name="boot_movie_converting_started">Μετατροπή του %1$s για το Steam. Αφήστε το DroidDeck ανοιχτό.</string>
|
||||
<string name="boot_movie_unsupported">Η συσκευή δεν μπορεί να αποκωδικοποιήσει αυτό το βίντεο.</string>
|
||||
<string name="boot_movie_import_failed">Δεν ήταν δυνατή η εισαγωγή του βίντεο</string>
|
||||
</resources>
|
||||
@@ -58,7 +58,7 @@
|
||||
<string name="game_env_save">Guardar</string>
|
||||
<string name="game_env_remove">Quitar</string>
|
||||
<string name="game_env_restore">Heredar de nuevo</string>
|
||||
<string name="game_env_reset">Restablecer este perfil</string>
|
||||
<string name="game_env_reset">Borrar los ajustes propios del entorno</string>
|
||||
<string name="game_env_unset">Quitar valor en este perfil</string>
|
||||
<string name="game_env_empty">Sin variables definidas.</string>
|
||||
<string name="game_env_inherited">Heredada</string>
|
||||
@@ -266,7 +266,7 @@
|
||||
<string name="mode_controller_ds4">DualShock 4</string>
|
||||
<string name="mode_controller_dualsense">DualSense</string>
|
||||
<string name="mode_controller_x360">Mando Xbox 360</string>
|
||||
<string name="mode_controller_note">Deck: botón de Quick Access y giroscopio. Xbox 360: abre Quick Access con Guide+A.</string>
|
||||
<string name="mode_controller_note">Deck: botón de Quick Access y giroscopio. Steam Controller: lo mismo, con detección de agarre en los botones traseros. DualShock 4 y DualSense: giroscopio, y los trackpads como su panel táctil. Xbox 360: abre Quick Access con Guide+A.</string>
|
||||
<string name="mode_back">Atrás</string>
|
||||
<string name="back_menu_then_qam">Menú → QAM</string>
|
||||
<string name="back_qam_then_menu">QAM → Menú</string>
|
||||
@@ -471,7 +471,7 @@
|
||||
<string name="comp_applies_next_game">Se aplica la próxima vez que se inicie un juego</string>
|
||||
<string name="comp_import_zip">Importar .zip</string>
|
||||
<string name="comp_reading">Leyendo Protons…</string>
|
||||
<string name="comp_no_proton">Aún no hay ningún Proton instalado. Inicia Steam una vez para descargar su Proton ARM64, o añade GE-Proton o CachyOS desde Configuración.</string>
|
||||
<string name="comp_no_proton">Aún no hay ningún Proton instalado. Inicia Steam una vez para descargar su Proton ARM64, o añade GE-Proton, CachyOS o DroidDeck-Proton desde Configuración.</string>
|
||||
<string name="comp_original">Original · %1$s</string>
|
||||
<string name="comp_this_build">%1$s · esta versión</string>
|
||||
<string name="comp_earlier_build">%1$s · versión anterior</string>
|
||||
@@ -577,6 +577,10 @@
|
||||
<string name="drawer_cas">Nitidez (CAS)</string>
|
||||
<string name="drawer_cas_level">Nivel de nitidez</string>
|
||||
<string name="drawer_fake_hdr">HDR simulado</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">Entrar en SteamVR</string>
|
||||
<string name="steamvr_checking">Comprobando SteamVR…</string>
|
||||
<string name="drawer_deband">Reducción de bandas</string>
|
||||
<string name="drawer_deband_strength">Intensidad de reducción de bandas</string>
|
||||
<string name="drawer_brightness">Brillo</string>
|
||||
@@ -594,6 +598,11 @@
|
||||
<string name="drawer_texture_sharpness_note">Un sesgo de mip negativo: texturas más nítidas, algo de parpadeo. Auto sigue cuánto se amplía la sesión a la pantalla.</string>
|
||||
<string name="drawer_touch_auto">Auto (%1$s)</string>
|
||||
<string name="drawer_keyboard">Teclado</string>
|
||||
<string name="drawer_latency">Latencia</string>
|
||||
<string name="drawer_low_latency_controller">Mando de baja latencia</string>
|
||||
<string name="drawer_low_latency_controller_hint">El mando informa a 1000 Hz en lugar de 250 Hz. Usa algo más de CPU.</string>
|
||||
<string name="drawer_low_latency_touch">Táctil de baja latencia</string>
|
||||
<string name="drawer_low_latency_touch_hint">Los toques omiten la agrupación de fotogramas; los controles táctiles de Steam van a 1000 Hz. Usa algo más de CPU.</string>
|
||||
<string name="drawer_pc_keyboard">Teclado de PC</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">Segunda pantalla</string>
|
||||
@@ -857,7 +866,7 @@
|
||||
<string name="gate_eyebrow">Steam</string>
|
||||
<string name="gate_lede">Android detiene las apps que inician muchos procesos en segundo plano, y Steam inicia decenas. Hasta que esto cambie, los juegos se cierran sin mostrar ningún error.</string>
|
||||
<string name="gate_fix_for_me">Solucionarlo por mí</string>
|
||||
<string name="gate_toggle_steps">Activa “Desactivar restricciones de procesos secundarios” y vuelve. Deja activadas las Opciones de desarrollador o el interruptor se desactivará de nuevo.</string>
|
||||
<string name="gate_toggle_steps">Activa “Inhabilitar restricciones de procesos secundarios” y vuelve. Deja activadas las Opciones de desarrollador o el interruptor se desactivará de nuevo.</string>
|
||||
<string name="gate_open_dev">Abrir Opciones de desarrollador</string>
|
||||
<string name="gate_no_toggle">Esta versión de Android no tiene un interruptor para ello, así que DroidDeck lo cambia mediante la Depuración inalámbrica. Tarda un minuto y no necesitas ordenador.</string>
|
||||
<string name="gate_needs_wifi">La Depuración inalámbrica necesita Wi-Fi. Sirve cualquier red; no necesita internet.</string>
|
||||
@@ -1023,7 +1032,6 @@
|
||||
<string name="gpu_set_with_runtime">Se define junto con el controlador de runtime: %1$s incluye ambos.</string>
|
||||
<string name="gpu_runtime_hint">Lo usan los juegos y el escritorio.</string>
|
||||
<string name="gpu_runtime_default">Predeterminado del runtime</string>
|
||||
<string name="gpu_display_hint">Dibuja los fotogramas en pantalla. Se aplica al reiniciar DroidDeck.</string>
|
||||
<string name="gpu_auto_by_gpu">Auto - elegido según la GPU</string>
|
||||
<string name="gpu_manage">Descargar, importar o eliminar…</string>
|
||||
<string name="gpu_in_use_caps">EN USO</string>
|
||||
@@ -1257,10 +1265,16 @@
|
||||
<string name="drivers_downloading_pair">Descargando %1$s %2$s…</string>
|
||||
<string name="drivers_updated_to">Actualizado a %1$s %2$s.</string>
|
||||
<string name="drivers_updated_to_restart">Actualizado a %1$s %2$s. El controlador de pantalla se aplica al reiniciar DroidDeck.</string>
|
||||
<string name="drivers_auto_waits_for_session">Las actualizaciones de controladores esperan a que termine la sesión.</string>
|
||||
<string name="drivers_auto_waits_for_network">%1$s %2$s se descargará cuando desactives los datos móviles y el ahorro de batería.</string>
|
||||
<string name="drivers_skipping_bad">%1$s %2$s no inició una sesión aquí, así que Auto se queda con los controladores que sí funcionaron.</string>
|
||||
<string name="drivers_rolled_back">%1$s no inició una sesión dos veces, así que Auto volvió a los controladores que funcionaban.</string>
|
||||
<string name="drivers_switched_to">Cambiado a %1$s %2$s.</string>
|
||||
<string name="drivers_switched_to_restart">Cambiado a %1$s %2$s. El controlador de pantalla se aplica al reiniciar DroidDeck.</string>
|
||||
<string name="drivers_using">Usando %1$s %2$s.</string>
|
||||
<string name="drivers_using_restart">Usando %1$s %2$s. El controlador de pantalla se aplica al reiniciar DroidDeck.</string>
|
||||
<string name="drivers_using_quest">Usando el controlador de pantalla Quest incluido y el valor predeterminado del runtime.</string>
|
||||
<string name="drivers_using_quest_restart">Usando el controlador de pantalla Quest incluido y el valor predeterminado del runtime. El controlador de pantalla se aplica tras reiniciar DroidDeck.</string>
|
||||
<string name="drivers_not_a_driver">No es un zip de controlador: no es un controlador de AdrenoTools, ni un Turnip -Linux, ni un paquete Android + Linux</string>
|
||||
<string name="drivers_import_failed">La importación falló: %1$s</string>
|
||||
<string name="drivers_imported_unnamed">Controlador importado</string>
|
||||
@@ -1693,7 +1707,7 @@
|
||||
<string name="adbpair_done_text">Toca para volver a DroidDeck. Ya puedes desactivar la Depuración inalámbrica.</string>
|
||||
<string name="adbpair_title_failed">Se detuvo la configuración de la Depuración inalámbrica</string>
|
||||
|
||||
<string name="selfinst_http">La descarga respondió HTTP %1$d</string>
|
||||
<string name="selfinst_download_failed">Error en la descarga</string>
|
||||
<string name="selfinst_size_mismatch">El tamaño de la descarga no coincidía con el APK publicado y se descartó - inténtalo de nuevo</string>
|
||||
<string name="selfinst_checksum_mismatch">La descarga no coincidía con su suma de verificación y se descartó - inténtalo de nuevo</string>
|
||||
<string name="selfinst_wrong_package">La actualización es para el paquete de Android %1$s, no %2$s</string>
|
||||
@@ -1791,7 +1805,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">Se alcanzó el límite de solicitudes de GitHub - inténtalo más tarde</string>
|
||||
<string name="turnip_github_http">GitHub respondió HTTP %1$d</string>
|
||||
<string name="turnip_download_http">la descarga respondió HTTP %1$d</string>
|
||||
<string name="turnip_download_failed">Error en la descarga</string>
|
||||
<string name="turnip_list_predates_checksums">Esta lista de controladores es anterior a las sumas de verificación - actualízala e inténtalo de nuevo</string>
|
||||
<string name="turnip_checksum_mismatch">La suma de verificación no coincide - se descartó la descarga</string>
|
||||
<string name="turnip_not_adrenotools">No es un controlador de AdrenoTools: %1$s</string>
|
||||
@@ -1828,7 +1842,7 @@
|
||||
<string name="phantom_instructions_fix">%1$s Si no es posible, conecta este dispositivo a un ordenador con ADB y ejecuta el comando de abajo. Después vuelve aquí y comprueba de nuevo.</string>
|
||||
<string name="phantom_instructions_off">Ya se pueden iniciar sesiones de Steam.</string>
|
||||
<string name="phantom_instructions_not_applicable">Esta versión de Android no usa este límite.</string>
|
||||
<string name="phantom_fix_toggle">Activa “Desactivar restricciones de procesos secundarios” en Opciones de desarrollador y deja activadas las Opciones de desarrollador después.</string>
|
||||
<string name="phantom_fix_toggle">Activa “Inhabilitar restricciones de procesos secundarios” en Opciones de desarrollador y deja activadas las Opciones de desarrollador después.</string>
|
||||
<string name="phantom_fix_no_toggle">Esta versión de Android no tiene un interruptor para ello. Usa la Depuración inalámbrica para cambiarlo desde este dispositivo.</string>
|
||||
<string name="upd_block_no_apk">Esta versión no tiene descarga para esta copia de DroidDeck.</string>
|
||||
<string name="upd_block_version_code">Android no la instalará sobre esta versión porque su versionCode %1$d es inferior al %2$d instalado.</string>
|
||||
@@ -2095,6 +2109,21 @@
|
||||
<string name="edit_other_exe">Otro exe…</string>
|
||||
<string name="edit_remove">Quitar</string>
|
||||
<string name="art_cover">Portada</string>
|
||||
<string name="art_wide_cover">Portada ancha</string>
|
||||
<string name="art_adjust_logo">Ajustar logotipo</string>
|
||||
<string name="logo_pos_default">Predeterminada</string>
|
||||
<string name="logo_pos_upper_left">Arriba a la izquierda</string>
|
||||
<string name="logo_pos_upper_center">Arriba al centro</string>
|
||||
<string name="logo_pos_center">Centro</string>
|
||||
<string name="logo_pos_bottom_left">Abajo a la izquierda</string>
|
||||
<string name="logo_pos_bottom_center">Abajo al centro</string>
|
||||
<string name="logo_pos_position">Posición</string>
|
||||
<string name="logo_pos_size">Tamaño</string>
|
||||
<string name="logo_pos_save">Guardar</string>
|
||||
<string name="logo_pos_reset">Volver al valor predeterminado</string>
|
||||
<string name="logo_pos_hint">Se dibuja sobre el fondo del juego, igual que en Steam. La posición exacta puede variar unos píxeles.</string>
|
||||
<string name="logo_pos_no_logo">Este juego aún no tiene logotipo. Elige uno en Logotipo para verlo aquí.</string>
|
||||
<string name="logo_pos_failed">No se pudo guardar la posición del logotipo. Inténtalo de nuevo.</string>
|
||||
<string name="art_background">Fondo</string>
|
||||
<string name="art_logo">Logotipo</string>
|
||||
<string name="art_icon">Icono</string>
|
||||
@@ -2107,6 +2136,17 @@
|
||||
<string name="art_pick_image">Elegir imagen…</string>
|
||||
<string name="art_pick_title">Elige una imagen: %1$s</string>
|
||||
<string name="art_reset">Restablecer a automático</string>
|
||||
<string name="art_search_sgdb">Buscar en SteamGridDB…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB: %1$s</string>
|
||||
<string name="art_sgdb_search_label">Buscar un juego</string>
|
||||
<string name="art_sgdb_other_games">Otras coincidencias</string>
|
||||
<string name="art_sgdb_none">No se encontró nada.</string>
|
||||
<string name="art_sgdb_no_key">SteamGridDB necesita una clave de API. Añade la tuya en Configuración › Launcher.</string>
|
||||
<string name="art_sgdb_bad_key">SteamGridDB no aceptó la clave de API. Compruébala en Configuración › Launcher.</string>
|
||||
<string name="art_sgdb_limit">SteamGridDB está limitando las solicitudes ahora mismo. Inténtalo de nuevo en un minuto.</string>
|
||||
<string name="art_sgdb_offline">No se pudo conectar con SteamGridDB. Comprueba la conexión.</string>
|
||||
<string name="art_sgdb_server">SteamGridDB tuvo un problema. Inténtalo más tarde.</string>
|
||||
<string name="art_sgdb_no_game">SteamGridDB no tiene ninguna coincidencia para este juego. Prueba a buscarlo.</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="one">%1$d juego añadido</item>
|
||||
<item quantity="many">%1$d juegos añadidos</item>
|
||||
@@ -2136,6 +2176,45 @@
|
||||
<string name="gpu_bar_auto">Auto · %1$s</string>
|
||||
<string name="gpu_separately">Controladores de runtime y de pantalla por separado</string>
|
||||
<string name="gpu_use_title">¿Usar %1$s?</string>
|
||||
<string name="gpu_mode_title">Cómo se eligen los controladores</string>
|
||||
<string name="gpu_mode_auto_detail">La mejor pareja para esta GPU, siempre al día</string>
|
||||
<string name="gpu_mode_pair">Pareja combinada</string>
|
||||
<string name="gpu_mode_pair_detail">Elige una pareja; define ambos controladores</string>
|
||||
<string name="gpu_mode_mix">Combinar libremente</string>
|
||||
<string name="gpu_mode_mix_detail">Elige pantalla y runtime por separado</string>
|
||||
<string name="gpu_mode_pair_short">Pareja</string>
|
||||
<string name="gpu_mode_mix_short">Mixto</string>
|
||||
<string name="gpu_src_auto">AUTO</string>
|
||||
<string name="gpu_src_pair">PAREJA</string>
|
||||
<string name="gpu_src_bundle">PAQUETE</string>
|
||||
<string name="gpu_src_chosen">ELEGIDO</string>
|
||||
<string name="gpu_src_by_gpu">POR GPU</string>
|
||||
<string name="gpu_src_default">PREDETERMINADO</string>
|
||||
<string name="gpu_display_live">Dibujando ahora</string>
|
||||
<string name="gpu_display_pending">Reinicio pendiente · %1$s dibuja hasta entonces</string>
|
||||
<string name="gpu_runtime_live">En ejecución</string>
|
||||
<string name="gpu_runtime_pending">Próxima sesión · esta sesión mantiene %1$s</string>
|
||||
<string name="gpu_display_next">Dibuja la pantalla. Se aplica desde la próxima sesión.</string>
|
||||
<string name="gpu_previous_driver">el controlador anterior</string>
|
||||
<string name="gpu_restart_banner">Reinicia para cambiar el controlador de pantalla.</string>
|
||||
<string name="gpu_restart_blocked">Detén la sesión en curso y luego reinicia DroidDeck para cambiar el controlador de pantalla.</string>
|
||||
<string name="gpu_restart_now">Reiniciar ahora</string>
|
||||
<string name="gpu_restart_later">Más tarde</string>
|
||||
<string name="gpu_auto_title">Auto eligió esta pareja para tu %1$s</string>
|
||||
<string name="gpu_auto_builtin_title">Auto usa los controladores integrados para tu %1$s</string>
|
||||
<string name="gpu_auto_reason">Elegida para la familia %1$s. Las versiones nuevas se instalan en segundo plano.</string>
|
||||
<string name="gpu_pair_title">Elige una pareja</string>
|
||||
<string name="gpu_pair_hint">Una pareja define juntos el controlador de pantalla y el de runtime.</string>
|
||||
<string name="gpu_mix_title">Elige cada controlador</string>
|
||||
<string name="gpu_mix_hint">Un paquete rellena ambos huecos. Los cambios de pantalla se aplican tras reiniciar; los de runtime, en la próxima sesión.</string>
|
||||
<string name="gpu_change_display">Cambiar controlador de pantalla</string>
|
||||
<string name="gpu_change_runtime">Cambiar controlador de runtime</string>
|
||||
<string name="gpu_autos_pick">ELECCIÓN DE AUTO</string>
|
||||
<string name="gpu_installed">Instalado</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · reinicia para aplicar</string>
|
||||
<string name="gpu_chip_restart">%1$s · reinicio pendiente</string>
|
||||
<string name="comp_this_device">Este dispositivo</string>
|
||||
<string name="proton_remove_title">¿Quitar %1$s?</string>
|
||||
<string name="setup_tab_diagnostics">Diagnóstico</string>
|
||||
<string name="setup_ready">Listo para jugar</string>
|
||||
@@ -2147,6 +2226,14 @@
|
||||
<string name="setup_group_look">Aspecto</string>
|
||||
<string name="setup_group_device">Este dispositivo</string>
|
||||
<string name="setup_group_art">Ilustraciones de la biblioteca</string>
|
||||
<string name="setup_group_download">Descargas</string>
|
||||
<string name="setup_download_source">Origen</string>
|
||||
<string name="setup_download_source_auto">Automático</string>
|
||||
<string name="setup_download_source_direct">Directo</string>
|
||||
<string name="setup_download_source_mirror">Espejo</string>
|
||||
<string name="setup_download_source_custom">Prefijo propio</string>
|
||||
<string name="setup_download_mirror">Sitio</string>
|
||||
<string name="setup_download_prefix">Prefijo</string>
|
||||
<string name="flathub_eyebrow">APPS DE LINUX</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">Apps para el escritorio de Linux, que se abren desde Escritorio.</string>
|
||||
@@ -2264,12 +2351,9 @@
|
||||
<string name="cardboard_recenter_button">Recentrar</string>
|
||||
<string name="cardboard_steamvr">SteamVR (experimental)</string>
|
||||
<string name="cardboard_steamvr_install">Instalar</string>
|
||||
<string name="cardboard_steamvr_install_hint">Descarga SteamVR para ARM64 de Valve (571 MB).</string>
|
||||
<string name="cardboard_steamvr_installing">Descargando y descomprimiendo SteamVR…</string>
|
||||
<string name="cardboard_steamvr_start">Iniciar</string>
|
||||
<string name="cardboard_steamvr_ready_hint">Ejecuta SteamVR usando tu cabeza como visor; la imagen cambia a 3D lado a lado.</string>
|
||||
<string name="cardboard_steamvr_stop">Detener</string>
|
||||
<string name="cardboard_steamvr_running_hint">SteamVR está en ejecución.</string>
|
||||
<string name="cardboard_steamvr_unavailable">No disponible en esta sesión.</string>
|
||||
<string name="cardboard_steamvr_failed">SteamVR no respondió; revisa el registro de la sesión.</string>
|
||||
<string name="drawer_component_downloads">Descargar componentes</string>
|
||||
@@ -2281,4 +2365,195 @@
|
||||
<string name="stores_storage_insufficient">No hay suficiente espacio libre: se necesitan %1$s y solo hay %2$s libres</string>
|
||||
<string name="stores_storage_low">Espacio insuficiente (%1$s libres, se necesitan %2$s)</string>
|
||||
<string name="stores_target_free_unknown">Espacio libre desconocido</string>
|
||||
<string name="display_picture_position">Posición de la imagen</string>
|
||||
<string name="display_position_top">Arriba</string>
|
||||
<string name="display_position_bottom">Abajo</string>
|
||||
<string name="display_position_center">Centro (predeterminado)</string>
|
||||
<string name="setup_tab_storage">Almacenamiento</string>
|
||||
<string name="storage_measuring">Midiendo el almacenamiento…</string>
|
||||
<string name="storage_measuring_hint">La primera vez tarda unos segundos.</string>
|
||||
<string name="storage_internal">Almacenamiento interno</string>
|
||||
<string name="storage_used_of">%1$s de %2$s usados</string>
|
||||
<string name="storage_free">%1$s libres</string>
|
||||
<string name="storage_droiddeck_uses">DroidDeck usa %1$s. Medido el %2$s.</string>
|
||||
<string name="storage_rescanning">midiendo de nuevo…</string>
|
||||
<string name="storage_other_apps">Otras apps de Android</string>
|
||||
<string name="storage_safe_now">Se puede liberar ahora</string>
|
||||
<string name="storage_safe_hint">Runtimes sin usar, restos, registros antiguos</string>
|
||||
<string name="storage_clean_up">Limpiar…</string>
|
||||
<string name="storage_session_running">Detén la sesión para limpiar o eliminar algo. Los tamaños se siguen mostrando.</string>
|
||||
<string name="storage_where">Dónde va</string>
|
||||
<string name="storage_games">Juegos de Steam</string>
|
||||
<string name="storage_game_data">Datos de juegos</string>
|
||||
<string name="storage_game_data_hint">Prefijos %1$s · taller %2$s · shaders %3$s</string>
|
||||
<string name="storage_tools">Proton y runtimes</string>
|
||||
<string name="storage_tools_unused">%1$s sin usar</string>
|
||||
<string name="storage_stores">GOG, Epic y otras tiendas</string>
|
||||
<string name="storage_stores_hint">Desinstálalos desde Tiendas</string>
|
||||
<string name="storage_desktop">Apps de escritorio</string>
|
||||
<string name="storage_desktop_hint">Apps y runtimes de Flatpak</string>
|
||||
<string name="storage_system">Sistema Linux y cliente de Steam</string>
|
||||
<string name="storage_system_hint">Necesario para ejecutar cualquier cosa</string>
|
||||
<string name="storage_app_data">Datos de DroidDeck</string>
|
||||
<string name="storage_app_data_hint">Registros de sesiones antiguas %1$s · cachés · controladores</string>
|
||||
<string name="storage_legend_games">Juegos</string>
|
||||
<string name="storage_legend_data">Datos de juegos</string>
|
||||
<string name="storage_legend_tools">Proton y runtimes</string>
|
||||
<string name="storage_legend_stores">Otras tiendas</string>
|
||||
<string name="storage_legend_desktop">Apps de escritorio</string>
|
||||
<string name="storage_legend_system">Sistema Linux</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">Limpiar</string>
|
||||
<string name="storage_clean_up_lede">Nada de aquí afecta a tus juegos instalados ni a las descargas en curso.</string>
|
||||
<string name="storage_nothing_to_clean">Ahora mismo no hay nada que limpiar.</string>
|
||||
<string name="storage_selected">Seleccionado: %1$s · %2$s → %3$s libres</string>
|
||||
<string name="storage_cleaning">Limpiando…</string>
|
||||
<string name="storage_free_button">Liberar %1$s</string>
|
||||
<string name="storage_freed">Se liberaron %1$s.</string>
|
||||
<string name="storage_item_runtimes">Runtimes y Proton de Steam sin usar</string>
|
||||
<string name="storage_item_leftovers">Restos de juegos desinstalados</string>
|
||||
<string name="storage_item_leftovers_unsure">Restos que pueden contener partidas guardadas</string>
|
||||
<string name="storage_item_logs">Registros de sesiones antiguas</string>
|
||||
<string name="storage_item_logs_detail">Conserva los registros de la sesión actual</string>
|
||||
<string name="storage_item_custom_detail">Ningún juego lo usa. Reinstálalo desde Componentes si lo necesitas.</string>
|
||||
<string name="storage_item_shaders">Cachés de shaders de juegos instalados</string>
|
||||
<string name="storage_item_shaders_detail">Se reconstruyen al jugar. Los primeros minutos de cada juego vuelven a dar tirones.</string>
|
||||
<string name="storage_tag_safe">Seguro</string>
|
||||
<string name="storage_tag_unused">Sin usar</string>
|
||||
<string name="storage_tag_stutter">Tirones</string>
|
||||
<string name="storage_tag_saves">Revisar partidas</string>
|
||||
<string name="storage_tag_default">Predeterminado</string>
|
||||
<string name="storage_tag_required">Necesario</string>
|
||||
<string name="storage_tag_not_used">Sin usar</string>
|
||||
<string name="storage_back">Almacenamiento</string>
|
||||
<string name="storage_games_with_data">%1$s con sus datos</string>
|
||||
<string name="storage_sort_largest">Más grandes</string>
|
||||
<string name="storage_sort_least_played">Menos jugados</string>
|
||||
<string name="storage_sort_sd">En la tarjeta SD</string>
|
||||
<string name="storage_no_games">Aquí no hay juegos.</string>
|
||||
<string name="storage_played">Jugado %1$s</string>
|
||||
<string name="storage_never_played">Nunca jugado</string>
|
||||
<string name="storage_game_parts">Juego %1$s · datos %2$s</string>
|
||||
<string name="storage_uninstall">Desinstalar</string>
|
||||
<string name="storage_games_note">Desinstalar abre el diálogo de Steam para que Steam lo registre. Para mover un juego a la tarjeta SD, usa Steam › Ajustes › Almacenamiento.</string>
|
||||
<string name="storage_uninstall_title">¿Desinstalar %1$s?</string>
|
||||
<string name="storage_uninstall_body">Steam se abre y te pide confirmación. Esto libera los %1$s del juego. Su prefijo se queda hasta que limpies los restos, y las partidas guardadas en Steam Cloud siguen en tu cuenta.</string>
|
||||
<string name="storage_open_steam">Abrir Steam</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s sin usar</string>
|
||||
<string name="storage_remove_unused">Quitar lo que no se usa · %1$s</string>
|
||||
<string name="storage_in_use">En uso</string>
|
||||
<string name="storage_not_used">Sin usar</string>
|
||||
<string name="storage_remove">Quitar</string>
|
||||
<string name="storage_remove_title">¿Quitar %1$s?</string>
|
||||
<string name="storage_remove_body">Libera %1$s. Steam lo volverá a descargar si algún juego lo pide.</string>
|
||||
<string name="storage_tool_fex_runtime">Necesario para juegos Linux x86 (FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">Lo necesitan los Proton ARM64</string>
|
||||
<string name="storage_tool_redist">Instaladores de VC++ y DirectX que piden los juegos</string>
|
||||
<string name="storage_tool_fex">Ejecuta juegos Linux x86</string>
|
||||
<string name="storage_tool_steam">Herramienta de Steam</string>
|
||||
<string name="storage_tool_old_runtime">DroidDeck ejecuta juegos sin este runtime</string>
|
||||
<string name="storage_tool_x86_proton">Proton x86, que DroidDeck no puede ejecutar</string>
|
||||
<string name="storage_tool_custom_default">Predeterminado para todos los juegos</string>
|
||||
<string name="storage_tool_custom_unused">Ningún juego lo usa</string>
|
||||
<string name="storage_tool_custom_unknown">No se pudieron leer los ajustes de Steam, así que se conserva</string>
|
||||
<string name="storage_cleanup_busy">Espera a que termine la limpieza del almacenamiento</string>
|
||||
<string name="storage_low_title">Solo quedan %1$s libres</string>
|
||||
<string name="storage_low_body">Steam necesita espacio para actualizaciones y cachés de shaders. Por debajo de 10 GB, las actualizaciones de juegos pueden fallar a medias.</string>
|
||||
<string name="storage_low_safe">Se pueden liberar %1$s sin riesgo.</string>
|
||||
<string name="storage_free_up">Liberar espacio</string>
|
||||
<string name="storage_play_anyway">Jugar igualmente</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d instalado</item>
|
||||
<item quantity="other">%d instalados</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d en la tarjeta SD</item>
|
||||
<item quantity="other">%d en la tarjeta SD</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">%d herramienta</item>
|
||||
<item quantity="other">%d herramientas</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">Carpetas de prefijo vacías y caché de shaders de %d juego</item>
|
||||
<item quantity="other">Carpetas de prefijo vacías y cachés de shaders de %d juegos</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">El prefijo de %d juego aún contiene archivos, que pueden incluir partidas guardadas locales. Haz una copia antes si las quieres.</item>
|
||||
<item quantity="other">Los prefijos de %d juegos aún contienen archivos, que pueden incluir partidas guardadas locales. Haz una copia antes si las quieres.</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">%d juego</item>
|
||||
<item quantity="other">%d juegos</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">Proton predeterminado de DroidDeck</string>
|
||||
<string name="storage_tool_proton">Opción Proton 11 de DroidDeck</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">Usado por %d juego</item>
|
||||
<item quantity="other">Usado por %d juegos</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">Componentes</string>
|
||||
<string name="storage_components">Componentes</string>
|
||||
<string name="storage_item_components">Componentes sin usar</string>
|
||||
<string name="storage_component_original">Original %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">Libera %1$s. Solo restaura una versión de Proton que ya no está instalada.</string>
|
||||
<string name="storage_leftovers_unchecked">Sin comprobar: no se pudo leer un manifiesto de app de Steam, así que los prefijos de juegos desinstalados se conservan hasta que se pueda.</string>
|
||||
<string name="storage_failed">No se pudo terminar: %1$s</string>
|
||||
<string name="storage_desktop_shared">compartidos %1$s</string>
|
||||
<string name="storage_data_installed">Juegos instalados</string>
|
||||
<string name="storage_data_parts">Prefijo %1$s · shaders %2$s · taller %3$s</string>
|
||||
<string name="storage_clear_shaders">Borrar shaders</string>
|
||||
<string name="storage_data_leftovers">Juegos desinstalados</string>
|
||||
<string name="storage_steam_app">App de Steam %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">Su prefijo aún contiene archivos, que pueden incluir partidas guardadas locales</string>
|
||||
<string name="storage_leftover_confirm">Elimina su prefijo de Wine y su caché de shaders (%1$s). Haz copia de las partidas que quieras antes; las de Steam Cloud siguen en tu cuenta.</string>
|
||||
<string name="storage_data_other">Conservados</string>
|
||||
<string name="storage_data_other_title">Juegos que no son de Steam y prefijo compartido de Steam</string>
|
||||
<string name="storage_data_other_detail">Prefijos de juegos añadidos desde tiendas o a mano</string>
|
||||
<string name="storage_components_packages">Paquetes descargados</string>
|
||||
<string name="storage_components_originals">Originales guardados</string>
|
||||
<string name="storage_components_none">No hay paquetes de componentes descargados.</string>
|
||||
<string name="storage_component_in_use">Elegido para un Proton o un juego</string>
|
||||
<string name="storage_component_unused">No elegido en ningún sitio</string>
|
||||
<string name="storage_original_kept">Restaura el Proton instalado</string>
|
||||
<string name="storage_original_old">Versión de Proton que ya no está instalada</string>
|
||||
<string name="storage_tag_in_use">En uso</string>
|
||||
<string name="storage_components_note">Los componentes en uso permanecen bloqueados. Elígelos en Componentes; lo que quites aquí se puede volver a descargar allí.</string>
|
||||
<string name="storage_component_confirm">Libera %1$s. Los paquetes que importaste tú no se pueden volver a descargar.</string>
|
||||
<string name="storage_desktop_detail">%1$s para apps de Flatpak y lo que comparten</string>
|
||||
<string name="storage_desktop_apps_title">Apps</string>
|
||||
<string name="storage_desktop_none">No hay apps de Flatpak instaladas.</string>
|
||||
<string name="storage_desktop_shared_title">Compartido por las apps</string>
|
||||
<string name="storage_desktop_runtimes">Runtimes</string>
|
||||
<string name="storage_desktop_runtimes_detail">Bibliotecas sobre las que funcionan las apps; las que no se usan se van con Quitar lo que no se usa</string>
|
||||
<string name="storage_desktop_cache">Almacén de descargas</string>
|
||||
<string name="storage_desktop_cache_detail">Almacén de objetos y descargas en caché de Flatpak</string>
|
||||
<string name="storage_desktop_prune">Quitar lo que no se usa</string>
|
||||
<string name="storage_desktop_prune_body">Quita los runtimes que ninguna app instalada necesita y limpia el almacén de objetos de Flatpak. Las apps instaladas siguen funcionando.</string>
|
||||
<string name="storage_flatpak_uninstall_body">Quita la app (%1$s) y cualquier runtime que no use nada más. Sus datos en tu carpeta personal se quedan.</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">%d paquete</item>
|
||||
<item quantity="other">%d paquetes</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">%d app</item>
|
||||
<item quantity="other">%d apps</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">Vídeo de arranque</string>
|
||||
<string name="boot_movie_video">Vídeo</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d fps</string>
|
||||
<string name="boot_movie_note">60 fps se ve más fluido pero usa más CPU mientras Steam se inicia. Un vídeo de inicio elegido en los ajustes de Steam tiene prioridad.</string>
|
||||
<string name="boot_movie_sound">Sonido</string>
|
||||
<string name="boot_movie_custom">Tu propio vídeo</string>
|
||||
<string name="boot_movie_custom_hint">MP4, MOV, MKV o WebM. Otros formatos y archivos de más de %1$d MB se convierten a WebM 720p en el dispositivo, hasta %2$d segundos.</string>
|
||||
<string name="boot_movie_import">Importar</string>
|
||||
<string name="boot_movie_remove">Quitar</string>
|
||||
<string name="boot_movie_pick">Elige un vídeo de arranque</string>
|
||||
<string name="boot_movie_imported">%1$s es ahora el vídeo de arranque</string>
|
||||
<string name="boot_movie_imported_silent">%1$s es ahora el vídeo de arranque, sin sonido</string>
|
||||
<string name="boot_movie_too_long">Los vídeos que hay que convertir deben durar %1$d segundos o menos</string>
|
||||
<string name="boot_movie_converting">Convirtiendo %1$d %%</string>
|
||||
<string name="boot_movie_converting_started">Convirtiendo %1$s para Steam. Mantén DroidDeck abierto.</string>
|
||||
<string name="boot_movie_unsupported">Este dispositivo no puede decodificar ese vídeo.</string>
|
||||
<string name="boot_movie_import_failed">No se pudo importar ese vídeo</string>
|
||||
</resources>
|
||||
@@ -278,6 +278,9 @@
|
||||
<string name="mode_suspend_steam_note">Automaattinen keskeyttää ja jatkaa, kun sovellus tulee näkyviin. Natiivi valmistelee Steamin lepotilaan ensin. Manuaalinen odottaa Jatka-painiketta. Ei koskaan pitää sovelluksen aina käynnissä.</string>
|
||||
<string name="mode_background_downloads">Pidä lataukset käynnissä</string>
|
||||
<string name="mode_background_downloads_hint">Viivästytä taustakeskeytystä, kun Steam lataa aktiivisesti.</string>
|
||||
<string name="mode_zram">zRAM-pakkaus</string>
|
||||
<string name="mode_zram_hint">Pelin pyöriessä Steam-asiakkaan muisti pakataan laitteen zramiin, jolloin pelille jää enemmän RAM-muistia. Astuu voimaan, kun Steam käynnistetään seuraavan kerran.</string>
|
||||
<string name="mode_zram_unsupported">Ei käytettävissä: laitteessa ei ole zramia tai swappia, tai ydin on vanhempi kuin 5.4.</string>
|
||||
<string name="mode_suspend_note">Automaattinen keskeyttää taustalla ja jatkaa, kun sovellus tulee näkyviin. Manuaalinen odottaa Jatka-painiketta. Ei koskaan pitää sovelluksen aina käynnissä.</string>
|
||||
<string name="mode_startup">Käynnistys</string>
|
||||
<string name="mode_steam_startup">Käynnistä Steam DroidDeckin kanssa</string>
|
||||
@@ -575,6 +578,10 @@
|
||||
<string name="drawer_cas">Terävöinti (CAS)</string>
|
||||
<string name="drawer_cas_level">Terävöinnin taso</string>
|
||||
<string name="drawer_fake_hdr">Keinotekoinen HDR</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">Käynnistä SteamVR</string>
|
||||
<string name="steamvr_checking">Tarkistetaan SteamVR…</string>
|
||||
<string name="drawer_deband">Raidanpoisto</string>
|
||||
<string name="drawer_deband_strength">Raidanpoiston voimakkuus</string>
|
||||
<string name="drawer_brightness">Kirkkaus</string>
|
||||
@@ -592,6 +599,11 @@
|
||||
<string name="drawer_texture_sharpness_note">Negatiivinen mip-poikkeama: terävämmät tekstuurit, jonkin verran kimmellystä. Automaattinen vastaa sitä, kuinka paljon istunto on suurennettu näytölle.</string>
|
||||
<string name="drawer_touch_auto">Automaattinen (%1$s)</string>
|
||||
<string name="drawer_keyboard">Näppäimistö</string>
|
||||
<string name="drawer_latency">Viive</string>
|
||||
<string name="drawer_low_latency_controller">Matalan viiveen ohjain</string>
|
||||
<string name="drawer_low_latency_controller_hint">Ohjain raportoi 1000 Hz:n taajuudella 250 Hz:n sijaan. Hieman enemmän CPU:ta.</string>
|
||||
<string name="drawer_low_latency_touch">Matalan viiveen kosketus</string>
|
||||
<string name="drawer_low_latency_touch_hint">Kosketukset ohittavat ruutujen niputuksen, joten Steamin kosketusohjaus toimii 1000 Hz:llä. Hieman enemmän CPU:ta.</string>
|
||||
<string name="drawer_pc_keyboard">PC-näppäimistö</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">Toinen näyttö</string>
|
||||
@@ -683,6 +695,14 @@
|
||||
<string name="setup_saved_logs">Tallennetut istunnon lokit</string>
|
||||
<string name="setup_saved_logs_hint">Säilyttää %1$d uusinta</string>
|
||||
<string name="setup_clear_logs">Tyhjennä lokit</string>
|
||||
<string name="setup_reset_settings">Palauta kaikki asetukset</string>
|
||||
<string name="setup_reset_settings_hint">Palauta jokainen asetus oletusarvoon</string>
|
||||
<string name="setup_reset_settings_button">Palauta</string>
|
||||
<string name="setup_reset_settings_title">Palautetaanko kaikki asetukset?</string>
|
||||
<string name="setup_reset_settings_body">Kuvan, suorituskyvyn, ohjaimen, teeman, kielen ja käynnistimen asetukset palaavat oletuksiin. Pelisi, Steam-kirjautuminen, kauppatilit, ladatut ajurit ja tallennuskansiot säilyvät.</string>
|
||||
<string name="setup_reset_settings_profiles">Palauta myös peliprofiilit (Proton, FEX, komponentit ja ajonaikainen ympäristö pelikohtaisesti sekä Oletus-profiilin ympäristö)</string>
|
||||
<string name="setup_reset_settings_session_running">Pysäytä istunto ennen asetusten palauttamista.</string>
|
||||
<string name="setup_reset_settings_partial">Asetukset palautettiin, mutta ei kaikkia. Tiedot ovat lokeissa.</string>
|
||||
<string name="setup_offline">Offline-tila</string>
|
||||
<string name="setup_sign_in_first">Kirjaudu Steamiin ensin</string>
|
||||
<string name="setup_launcher">Käynnistin</string>
|
||||
@@ -843,7 +863,7 @@
|
||||
<string name="gate_eyebrow">Steam</string>
|
||||
<string name="gate_lede">Android pysäyttää sovelluksia, jotka käynnistävät paljon taustaprosesseja, ja Steam käynnistää niitä kymmeniä. Kunnes tämä muuttuu, pelit sulkeutuvat ilman virheilmoitusta.</string>
|
||||
<string name="gate_fix_for_me">Korjaa se puolestani</string>
|
||||
<string name="gate_toggle_steps">Ota käyttöön ”Disable child process restrictions” ja palaa takaisin. Jätä Kehittäjäasetukset päälle, tai kytkin kytkeytyy taas pois päältä.</string>
|
||||
<string name="gate_toggle_steps">Ota käyttöön ”Poista käytöstä alatason prosessirajoitukset” ja palaa takaisin. Jätä Kehittäjäasetukset päälle, tai kytkin kytkeytyy taas pois päältä.</string>
|
||||
<string name="gate_open_dev">Avaa Kehittäjäasetukset</string>
|
||||
<string name="gate_no_toggle">Tässä Android-versiossa ei ole kytkintä tälle, niin DroidDeck muuttaa sen Langattoman virheenkorjauksen kautta. Kestää noin minuutin, ei tarvita tietokonetta.</string>
|
||||
<string name="gate_needs_wifi">Langaton virheenkorjaus vaatii Wi-Fi-yhteyden. Mikä tahansa verkko käy; internet-yhteyttä ei tarvita.</string>
|
||||
@@ -1009,7 +1029,6 @@
|
||||
<string name="gpu_set_with_runtime">Asetetaan yhdessä ajoympäristöajurin kanssa: %1$s kattaa molemmat.</string>
|
||||
<string name="gpu_runtime_hint">Käytetään peleissä ja työpöydällä.</string>
|
||||
<string name="gpu_runtime_default">Ajoympäristön oletus</string>
|
||||
<string name="gpu_display_hint">Piirtää kuvat näytölle. Käytössä DroidDeckin uudelleenkäynnistyksen jälkeen.</string>
|
||||
<string name="gpu_auto_by_gpu">Automaattinen - valittu GPU:n mukaan</string>
|
||||
<string name="gpu_manage">Lataa, tuo tai poista…</string>
|
||||
<string name="gpu_in_use_caps">KÄYTÖSSÄ</string>
|
||||
@@ -1239,10 +1258,16 @@
|
||||
<string name="drivers_downloading_pair">Ladataan %1$s %2$s…</string>
|
||||
<string name="drivers_updated_to">Päivitetty versioon %1$s %2$s.</string>
|
||||
<string name="drivers_updated_to_restart">Päivitetty versioon %1$s %2$s. Näyttöajuri otetaan käyttöön, kun DroidDeck käynnistyy uudelleen.</string>
|
||||
<string name="drivers_auto_waits_for_session">Ajuripäivitykset odottavat, kunnes istunto pysäytetään.</string>
|
||||
<string name="drivers_auto_waits_for_network">%1$s %2$s ladataan, kun poistut mobiilidatasta ja virransäästöstä.</string>
|
||||
<string name="drivers_skipping_bad">%1$s %2$s ei käynnistänyt istuntoa täällä, joten Automaattinen pysyy ajureissa, jotka käynnistivät.</string>
|
||||
<string name="drivers_rolled_back">%1$s ei käynnistänyt istuntoa kahdesti, joten Automaattinen palasi toimineisiin ajureihin.</string>
|
||||
<string name="drivers_switched_to">Vaihdettu versioon %1$s %2$s.</string>
|
||||
<string name="drivers_switched_to_restart">Vaihdettu versioon %1$s %2$s. Näyttöajuri otetaan käyttöön, kun DroidDeck käynnistyy uudelleen.</string>
|
||||
<string name="drivers_using">Käytössä %1$s %2$s.</string>
|
||||
<string name="drivers_using_restart">Käytössä %1$s %2$s. Näyttöajuri otetaan käyttöön, kun DroidDeck käynnistyy uudelleen.</string>
|
||||
<string name="drivers_using_quest">Käytössä ovat Questin mukana tuleva kuva-ajuri ja oletusajonaikaympäristö.</string>
|
||||
<string name="drivers_using_quest_restart">Käytössä ovat Questin mukana tuleva kuva-ajuri ja oletusajonaikaympäristö. Kuva-ajuri astuu voimaan, kun DroidDeck käynnistetään uudelleen.</string>
|
||||
<string name="drivers_not_a_driver">Ei ajuri-zip: ei AdrenoTools-ajuri, -Linux-Turnip eikä Android + Linux -paketti</string>
|
||||
<string name="drivers_import_failed">Tuonti epäonnistui: %1$s</string>
|
||||
<string name="drivers_imported_unnamed">Ajuri tuotu</string>
|
||||
@@ -1664,7 +1689,7 @@
|
||||
<string name="adbpair_done_text">Napauta palataksesi DroidDeckiin. Voit sammuttaa langattoman virheenkorjauksen nyt.</string>
|
||||
<string name="adbpair_title_failed">Langattoman virheenkorjauksen käyttöönotto pysähtyi</string>
|
||||
|
||||
<string name="selfinst_http">Lataus vastasi HTTP %1$d</string>
|
||||
<string name="selfinst_download_failed">Lataus epäonnistui</string>
|
||||
<string name="selfinst_size_mismatch">Latauksen koko ei vastannut julkaistua APK-tiedostoa, ja se hylättiin - yritä uudelleen</string>
|
||||
<string name="selfinst_checksum_mismatch">Lataus ei vastannut tarkistussummaansa, ja se hylättiin - yritä uudelleen</string>
|
||||
<string name="selfinst_wrong_package">Päivitys on Android-paketille %1$s, ei %2$s</string>
|
||||
@@ -1762,7 +1787,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">GitHubin käyttörajoitus tuli vastaan - yritä myöhemmin uudelleen</string>
|
||||
<string name="turnip_github_http">GitHub vastasi HTTP %1$d</string>
|
||||
<string name="turnip_download_http">lataus vastasi HTTP %1$d</string>
|
||||
<string name="turnip_download_failed">Lataus epäonnistui</string>
|
||||
<string name="turnip_list_predates_checksums">Tämä ajuriluettelo on tarkistussummia vanhempi - päivitä se ja yritä uudelleen</string>
|
||||
<string name="turnip_checksum_mismatch">Tarkistussumma ei täsmää - lataus hylättiin</string>
|
||||
<string name="turnip_not_adrenotools">Ei AdrenoTools-ajuri: %1$s</string>
|
||||
@@ -1799,7 +1824,7 @@
|
||||
<string name="phantom_instructions_fix">%1$s Jos se ei ole mahdollista, yhdistä tämä laite tietokoneeseen ADB:llä ja suorita alla oleva komento. Palaa tähän ja tarkista uudelleen.</string>
|
||||
<string name="phantom_instructions_off">Steam-istunnot voivat käynnistyä.</string>
|
||||
<string name="phantom_instructions_not_applicable">Tämä Android-versio ei käytä tätä rajaa.</string>
|
||||
<string name="phantom_fix_toggle">Ota käyttöön ”Poista käytöstä lapsiprosessien rajoitukset” Kehittäjäasetuksissa, ja pidä Kehittäjäasetukset päällä sen jälkeen.</string>
|
||||
<string name="phantom_fix_toggle">Ota käyttöön ”Poista käytöstä alatason prosessirajoitukset” Kehittäjäasetuksissa, ja pidä Kehittäjäasetukset päällä sen jälkeen.</string>
|
||||
<string name="phantom_fix_no_toggle">Tässä Android-versiossa ei ole kytkintä sille. Käytä langatonta virheenkorjausta muuttaaksesi sitä tältä laitteelta.</string>
|
||||
<string name="upd_block_no_apk">Tälle versiolle ei ole latausta tähän DroidDeck-kopioon.</string>
|
||||
<string name="upd_block_version_code">Android ei asenna sitä tämän version päälle, koska sen versionCode %1$d on pienempi kuin asennettu %2$d.</string>
|
||||
@@ -2031,6 +2056,9 @@
|
||||
<string name="stores_install_title">Asenna %1$s:</string>
|
||||
<string name="stores_target_internal">Sisäinen</string>
|
||||
<string name="stores_target_free">%1$s vapaana</string>
|
||||
<string name="stores_target_free_unknown">Vapaa tila tuntematon</string>
|
||||
<string name="stores_storage_insufficient">Liian vähän vapaata tilaa: tarvitaan %1$s, vapaana vain %2$s</string>
|
||||
<string name="stores_storage_low">Liian vähän tilaa (vapaana %1$s, tarvitaan %2$s)</string>
|
||||
<string name="stores_notification_channel">Kauppalataukset</string>
|
||||
<string name="stores_notification_channel_hint">Näyttää pelin latauksen etenemisen ja pitää lataukset käynnissä taustalla</string>
|
||||
<string name="stores_notification_one">Latautuu</string>
|
||||
@@ -2066,6 +2094,21 @@
|
||||
<string name="edit_other_exe">Toinen exe…</string>
|
||||
<string name="edit_remove">Poista</string>
|
||||
<string name="art_cover">Kansikuva</string>
|
||||
<string name="art_wide_cover">Leveä kansikuva</string>
|
||||
<string name="art_adjust_logo">Säädä logoa</string>
|
||||
<string name="logo_pos_default">Oletus</string>
|
||||
<string name="logo_pos_upper_left">Vasen ylä</string>
|
||||
<string name="logo_pos_upper_center">Ylä keskellä</string>
|
||||
<string name="logo_pos_center">Keskellä</string>
|
||||
<string name="logo_pos_bottom_left">Vasen ala</string>
|
||||
<string name="logo_pos_bottom_center">Ala keskellä</string>
|
||||
<string name="logo_pos_position">Sijainti</string>
|
||||
<string name="logo_pos_size">Koko</string>
|
||||
<string name="logo_pos_save">Tallenna</string>
|
||||
<string name="logo_pos_reset">Takaisin oletukseen</string>
|
||||
<string name="logo_pos_hint">Piirretään pelin taustan päälle, kuten Steam tekee. Tarkka sijainti voi poiketa muutaman pikselin.</string>
|
||||
<string name="logo_pos_no_logo">Tällä pelillä ei ole vielä logoa. Valitse sellainen kohdasta Logo, niin näet sen tässä.</string>
|
||||
<string name="logo_pos_failed">Logon sijaintia ei voitu tallentaa. Yritä uudelleen.</string>
|
||||
<string name="art_background">Tausta</string>
|
||||
<string name="art_logo">Logo</string>
|
||||
<string name="art_icon">Kuvake</string>
|
||||
@@ -2078,6 +2121,17 @@
|
||||
<string name="art_pick_image">Valitse kuva…</string>
|
||||
<string name="art_pick_title">Valitse kuva: %1$s</string>
|
||||
<string name="art_reset">Palauta automaattiseksi</string>
|
||||
<string name="art_search_sgdb">Hae SteamGridDB:stä…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB: %1$s</string>
|
||||
<string name="art_sgdb_search_label">Hae peliä</string>
|
||||
<string name="art_sgdb_other_games">Muut tulokset</string>
|
||||
<string name="art_sgdb_none">Ei tuloksia.</string>
|
||||
<string name="art_sgdb_no_key">SteamGridDB vaatii API-avaimen. Lisää omasi kohdassa Asetukset › Käynnistin.</string>
|
||||
<string name="art_sgdb_bad_key">SteamGridDB ei hyväksynyt API-avainta. Tarkista se kohdassa Asetukset › Käynnistin.</string>
|
||||
<string name="art_sgdb_limit">SteamGridDB rajoittaa pyyntöjä juuri nyt. Yritä uudelleen minuutin kuluttua.</string>
|
||||
<string name="art_sgdb_offline">SteamGridDB ei ole tavoitettavissa. Tarkista yhteys.</string>
|
||||
<string name="art_sgdb_server">SteamGridDB:ssä meni jotain pieleen. Yritä myöhemmin uudelleen.</string>
|
||||
<string name="art_sgdb_no_game">SteamGridDB:llä ei ole tuloksia tälle pelille. Kokeile hakea sitä.</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="one">%1$d peli lisätty</item>
|
||||
<item quantity="other">%1$d peliä lisätty</item>
|
||||
@@ -2105,6 +2159,45 @@
|
||||
<string name="gpu_bar_auto">Automaattinen · %1$s</string>
|
||||
<string name="gpu_separately">Ajoympäristön ja näytön ajurit erikseen</string>
|
||||
<string name="gpu_use_title">Käytetäänkö %1$s?</string>
|
||||
<string name="gpu_mode_title">Näin ajurit valitaan</string>
|
||||
<string name="gpu_mode_auto_detail">Paras pari tälle GPU:lle, aina ajan tasalla</string>
|
||||
<string name="gpu_mode_pair">Sovitettu pari</string>
|
||||
<string name="gpu_mode_pair_detail">Valitse pari; asettaa molemmat ajurit</string>
|
||||
<string name="gpu_mode_mix">Vapaa yhdistely</string>
|
||||
<string name="gpu_mode_mix_detail">Valitse kuva ja ajonaikaympäristö erikseen</string>
|
||||
<string name="gpu_mode_pair_short">Pari</string>
|
||||
<string name="gpu_mode_mix_short">Sekoitus</string>
|
||||
<string name="gpu_src_auto">AUTO</string>
|
||||
<string name="gpu_src_pair">PARI</string>
|
||||
<string name="gpu_src_bundle">PAKETTI</string>
|
||||
<string name="gpu_src_chosen">VALITTU</string>
|
||||
<string name="gpu_src_by_gpu">GPU-KOHTAINEN</string>
|
||||
<string name="gpu_src_default">OLETUS</string>
|
||||
<string name="gpu_display_live">Piirtää nyt</string>
|
||||
<string name="gpu_display_pending">Vaatii uudelleenkäynnistyksen · siihen asti piirtää %1$s</string>
|
||||
<string name="gpu_runtime_live">Aktiivinen</string>
|
||||
<string name="gpu_runtime_pending">Seuraava istunto · tämä pitää %1$s</string>
|
||||
<string name="gpu_display_next">Piirtää näytön. Astuu voimaan seuraavasta istunnosta.</string>
|
||||
<string name="gpu_previous_driver">edellinen ajuri</string>
|
||||
<string name="gpu_restart_banner">Käynnistä uudelleen vaihtaaksesi kuva-ajurin.</string>
|
||||
<string name="gpu_restart_blocked">Pysäytä nykyinen istunto ja käynnistä DroidDeck uudelleen vaihtaaksesi kuva-ajurin.</string>
|
||||
<string name="gpu_restart_now">Käynnistä uudelleen</string>
|
||||
<string name="gpu_restart_later">Myöhemmin</string>
|
||||
<string name="gpu_auto_title">Automaattinen valitsi tämän parin GPU:lle %1$s</string>
|
||||
<string name="gpu_auto_builtin_title">Automaattinen käyttää sisäänrakennettuja ajureita GPU:lle %1$s</string>
|
||||
<string name="gpu_auto_reason">Valittu %1$s-perheelle. Uudet versiot asennetaan taustalla.</string>
|
||||
<string name="gpu_pair_title">Valitse pari</string>
|
||||
<string name="gpu_pair_hint">Pari asettaa kuva-ajurin ja ajonaikaisen ajurin yhdessä.</string>
|
||||
<string name="gpu_mix_title">Valitse kukin ajuri</string>
|
||||
<string name="gpu_mix_hint">Paketti täyttää molemmat paikat. Kuvamuutokset astuvat voimaan uudelleenkäynnistyksen jälkeen, ajonaikaiset muutokset seuraavasta istunnosta.</string>
|
||||
<string name="gpu_change_display">Vaihda kuva-ajuri</string>
|
||||
<string name="gpu_change_runtime">Vaihda ajonaikainen ajuri</string>
|
||||
<string name="gpu_autos_pick">AUTOMAATTINEN VALINTA</string>
|
||||
<string name="gpu_installed">Asennettu</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · käynnistä uudelleen ottaaksesi käyttöön</string>
|
||||
<string name="gpu_chip_restart">%1$s · vaatii uudelleenkäynnistyksen</string>
|
||||
<string name="comp_this_device">Tämä laite</string>
|
||||
<string name="proton_remove_title">Poistetaanko %1$s?</string>
|
||||
<string name="setup_tab_diagnostics">Diagnostiikka</string>
|
||||
<string name="setup_ready">Valmis pelattavaksi</string>
|
||||
@@ -2116,6 +2209,14 @@
|
||||
<string name="setup_group_look">Ulkoasu</string>
|
||||
<string name="setup_group_device">Tämä laite</string>
|
||||
<string name="setup_group_art">Kirjaston kuvitus</string>
|
||||
<string name="setup_group_download">Lataus</string>
|
||||
<string name="setup_download_source">Lähde</string>
|
||||
<string name="setup_download_source_auto">Automaattinen</string>
|
||||
<string name="setup_download_source_direct">Suora</string>
|
||||
<string name="setup_download_source_mirror">Peili</string>
|
||||
<string name="setup_download_source_custom">Oma etuliite</string>
|
||||
<string name="setup_download_mirror">Sivusto</string>
|
||||
<string name="setup_download_prefix">Etuliite</string>
|
||||
<string name="flathub_eyebrow">LINUX-SOVELLUKSET</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">Työpöytäsovelluksia Linux-työpöydälle, avataan Työpöydältä.</string>
|
||||
@@ -2223,12 +2324,9 @@
|
||||
<string name="cardboard_recenter_button">Keskitä</string>
|
||||
<string name="cardboard_steamvr">SteamVR (kokeellinen)</string>
|
||||
<string name="cardboard_steamvr_install">Asenna</string>
|
||||
<string name="cardboard_steamvr_install_hint">Lataa Valven ARM64-version SteamVR:stä (571 Mt).</string>
|
||||
<string name="cardboard_steamvr_installing">Ladataan ja puretaan SteamVR:ää…</string>
|
||||
<string name="cardboard_steamvr_start">Käynnistä</string>
|
||||
<string name="cardboard_steamvr_ready_hint">Ajaa SteamVR:ää niin, että pääsi toimii lasina; kuva vaihtuu rinnakkais-3D:ksi.</string>
|
||||
<string name="cardboard_steamvr_stop">Pysäytä</string>
|
||||
<string name="cardboard_steamvr_running_hint">SteamVR on käynnissä.</string>
|
||||
<string name="cardboard_steamvr_unavailable">Ei käytettävissä tässä istunnossa.</string>
|
||||
<string name="cardboard_steamvr_failed">SteamVR ei vastannut; tarkista istunnon loki.</string>
|
||||
<string name="drawer_component_downloads">Lataa komponentteja</string>
|
||||
@@ -2242,4 +2340,195 @@
|
||||
<string name="stores_hltb_completionist">Kaikki</string>
|
||||
<string name="stores_hltb_hours">%1$s t</string>
|
||||
<string name="stores_hltb_minutes">%1$d min</string>
|
||||
<string name="display_picture_position">Kuvan sijainti</string>
|
||||
<string name="display_position_top">Ylhäällä</string>
|
||||
<string name="display_position_bottom">Alhaalla</string>
|
||||
<string name="display_position_center">Keskellä (oletus)</string>
|
||||
<string name="setup_tab_storage">Tallennustila</string>
|
||||
<string name="storage_measuring">Mitataan tallennustilaa…</string>
|
||||
<string name="storage_measuring_hint">Ensimmäisellä kerralla kestää muutaman sekunnin.</string>
|
||||
<string name="storage_internal">Sisäinen tallennustila</string>
|
||||
<string name="storage_used_of">%1$s / %2$s käytössä</string>
|
||||
<string name="storage_free">%1$s vapaana</string>
|
||||
<string name="storage_droiddeck_uses">DroidDeck käyttää %1$s. Mitattu %2$s.</string>
|
||||
<string name="storage_rescanning">mitataan uudelleen…</string>
|
||||
<string name="storage_other_apps">Muut Android-sovellukset</string>
|
||||
<string name="storage_safe_now">Vapautettavissa nyt</string>
|
||||
<string name="storage_safe_hint">Käyttämättömät ajonaikaympäristöt, jäänteet, vanhat lokit</string>
|
||||
<string name="storage_clean_up">Siivotaan…</string>
|
||||
<string name="storage_session_running">Pysäytä istunto, jos haluat siivota tai poistaa jotain. Koot näkyvät silti.</string>
|
||||
<string name="storage_where">Mihin se menee</string>
|
||||
<string name="storage_games">Steam-pelit</string>
|
||||
<string name="storage_game_data">Pelidata</string>
|
||||
<string name="storage_game_data_hint">Etuliitteet %1$s · workshop %2$s · shaderit %3$s</string>
|
||||
<string name="storage_tools">Proton ja ajonaikaympäristöt</string>
|
||||
<string name="storage_tools_unused">%1$s käyttämätön</string>
|
||||
<string name="storage_stores">GOG, Epic ja muut kaupat</string>
|
||||
<string name="storage_stores_hint">Poista ne kohdassa Kaupat</string>
|
||||
<string name="storage_desktop">Työpöytäsovellukset</string>
|
||||
<string name="storage_desktop_hint">Flatpak-sovellukset ja ajonaikaympäristöt</string>
|
||||
<string name="storage_system">Linux-järjestelmä ja Steam-asiakas</string>
|
||||
<string name="storage_system_hint">Tarvitaan, jotta mitään voi ajaa</string>
|
||||
<string name="storage_app_data">DroidDeckin tiedot</string>
|
||||
<string name="storage_app_data_hint">Vanhojen istuntojen lokit %1$s · välimuisti · ajurit</string>
|
||||
<string name="storage_legend_games">Pelit</string>
|
||||
<string name="storage_legend_data">Pelidata</string>
|
||||
<string name="storage_legend_tools">Proton ja ajonaikaympäristöt</string>
|
||||
<string name="storage_legend_stores">Muut kaupat</string>
|
||||
<string name="storage_legend_desktop">Työpöytäsovellukset</string>
|
||||
<string name="storage_legend_system">Linux-järjestelmä</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">Siivoa</string>
|
||||
<string name="storage_clean_up_lede">Mikään tässä ei koske asennettuihin peleihin tai käynnissä oleviin latauksiin.</string>
|
||||
<string name="storage_nothing_to_clean">Nyt ei ole siivottavaa.</string>
|
||||
<string name="storage_selected">Valittu: %1$s · %2$s → %3$s vapautuu</string>
|
||||
<string name="storage_cleaning">Siivotaan…</string>
|
||||
<string name="storage_free_button">Vapauta %1$s</string>
|
||||
<string name="storage_freed">Vapautettiin %1$s.</string>
|
||||
<string name="storage_item_runtimes">Käyttämättömät Steam-ajonaikaympäristöt ja Proton</string>
|
||||
<string name="storage_item_leftovers">Poistettujen pelien jäänteet</string>
|
||||
<string name="storage_item_leftovers_unsure">Jäänteet, jotka voivat sisältää tallennuksia</string>
|
||||
<string name="storage_item_logs">Vanhojen istuntojen lokit</string>
|
||||
<string name="storage_item_logs_detail">Nykyisen istunnon lokit säilyvät</string>
|
||||
<string name="storage_item_custom_detail">Mikään peli ei käytä tätä. Asenna se uudelleen kohdassa Komponentit, jos tarvitset sitä.</string>
|
||||
<string name="storage_item_shaders">Asennettujen pelien shadervälimuisti</string>
|
||||
<string name="storage_item_shaders_detail">Rakentuu uudelleen pelaamisen aikana. Jokaisen pelin ensimmäiset minuutit nykivät taas.</string>
|
||||
<string name="storage_tag_safe">Turvallinen</string>
|
||||
<string name="storage_tag_unused">Käyttämätön</string>
|
||||
<string name="storage_tag_stutter">Nykiminen</string>
|
||||
<string name="storage_tag_saves">Tarkista tallennukset</string>
|
||||
<string name="storage_tag_default">Oletus</string>
|
||||
<string name="storage_tag_required">Tarvitaan</string>
|
||||
<string name="storage_tag_not_used">Käyttämätön</string>
|
||||
<string name="storage_back">Tallennustila</string>
|
||||
<string name="storage_games_with_data">%1$s tietoineen</string>
|
||||
<string name="storage_sort_largest">Suurin</string>
|
||||
<string name="storage_sort_least_played">Vähiten pelattu</string>
|
||||
<string name="storage_sort_sd">SD-kortilla</string>
|
||||
<string name="storage_no_games">Täällä ei ole pelejä.</string>
|
||||
<string name="storage_played">pelattu %1$s</string>
|
||||
<string name="storage_never_played">Ei koskaan pelattu</string>
|
||||
<string name="storage_game_parts">Peli %1$s · tiedot %2$s</string>
|
||||
<string name="storage_uninstall">Poista asennus</string>
|
||||
<string name="storage_games_note">Asennuksen poisto avaa Steam-ikkunan, jotta Steam pitää kirjaa. Jos haluat siirtää pelin SD-kortille, käytä kohtaa Steam › Asetukset › Tallennustila.</string>
|
||||
<string name="storage_uninstall_title">Poistetaanko %1$s?</string>
|
||||
<string name="storage_uninstall_body">Steam avautuu ja pyytää vahvistusta. Se vapauttaa pelin %1$s. Etuliite jää, kunnes siivoat jäänteet, ja Steam Cloud -tallennukset pysyvät tililläsi.</string>
|
||||
<string name="storage_open_steam">Avaa Steam</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s käyttämätön</string>
|
||||
<string name="storage_remove_unused">Poista käyttämättömät · %1$s</string>
|
||||
<string name="storage_in_use">Käytössä</string>
|
||||
<string name="storage_not_used">Käyttämätön</string>
|
||||
<string name="storage_remove">Poista</string>
|
||||
<string name="storage_remove_title">Poistetaanko %1$s?</string>
|
||||
<string name="storage_remove_body">Vapauttaa %1$s. Steam lataa sen uudelleen, jos jokin peli tarvitsee sitä.</string>
|
||||
<string name="storage_tool_fex_runtime">Tarvitaan Linux x86 -peleille (FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">Tarvitaan ARM64-Proton-versioille</string>
|
||||
<string name="storage_tool_redist">Pelien tarvitsemat VC++- ja DirectX-asennukset</string>
|
||||
<string name="storage_tool_fex">Ajaa Linux x86 -pelejä</string>
|
||||
<string name="storage_tool_steam">Steam-työkalu</string>
|
||||
<string name="storage_tool_old_runtime">DroidDeck ajaa pelejä ilman tätä ajonaikaympäristöä</string>
|
||||
<string name="storage_tool_x86_proton">Proton x86, jota DroidDeck ei pysty ajamaan</string>
|
||||
<string name="storage_tool_custom_default">Oletus kaikille peleille</string>
|
||||
<string name="storage_tool_custom_unused">Mikään peli ei käytä tätä</string>
|
||||
<string name="storage_tool_custom_unknown">Steam-asetuksia ei voi lukea, joten se säilytetään</string>
|
||||
<string name="storage_cleanup_busy">Odota, kunnes tallennustilan siivous on valmis</string>
|
||||
<string name="storage_low_title">Vain %1$s vapaana</string>
|
||||
<string name="storage_low_body">Steam tarvitsee tilaa päivityksille ja shadervälimuistille. Alle 10 GB:ssä pelipäivitykset voivat keskeytyä puolivälissä.</string>
|
||||
<string name="storage_low_safe">%1$s voidaan vapauttaa turvallisesti.</string>
|
||||
<string name="storage_free_up">Vapauta tilaa</string>
|
||||
<string name="storage_play_anyway">Pelaa silti</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d asennettu</item>
|
||||
<item quantity="other">%d asennettu</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d SD-kortilla</item>
|
||||
<item quantity="other">%d SD-kortilla</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">%d työkalu</item>
|
||||
<item quantity="other">%d työkalua</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">Tyhjät etuliitekansiot ja shadervälimuisti %d pelistä</item>
|
||||
<item quantity="other">Tyhjät etuliitekansiot ja shadervälimuisti %d pelistä</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">%d pelin etuliite sisältää yhä tiedostoja, muun muassa paikallisia tallennuksia. Ota niistä kopio ensin, jos tarvitset niitä.</item>
|
||||
<item quantity="other">%d pelin etuliitteet sisältävät yhä tiedostoja, muun muassa paikallisia tallennuksia. Ota niistä kopio ensin, jos tarvitset niitä.</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">%d peli</item>
|
||||
<item quantity="other">%d peliä</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">DroidDeckin oletus-Proton</string>
|
||||
<string name="storage_tool_proton">DroidDeckin Proton 11 -vaihtoehto</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">%d peli käyttää</item>
|
||||
<item quantity="other">%d peliä käyttää</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">Komponentit</string>
|
||||
<string name="storage_components">Komponentit</string>
|
||||
<string name="storage_item_components">Käyttämättömät komponentit</string>
|
||||
<string name="storage_component_original">Alkuperäinen %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">Vapauttaa %1$s. Palauttaa vain Proton-version, jota ei enää ole asennettu.</string>
|
||||
<string name="storage_leftovers_unchecked">Ei tarkistettu: Steam-sovelluksen manifestia ei voi lukea, joten poistettujen pelien etuliitteet jäävät, kunnes se onnistuu.</string>
|
||||
<string name="storage_failed">Epäonnistui: %1$s</string>
|
||||
<string name="storage_desktop_shared">%1$s jaettua</string>
|
||||
<string name="storage_data_installed">Asennetut pelit</string>
|
||||
<string name="storage_data_parts">Etuliite %1$s · shaderit %2$s · workshop %3$s</string>
|
||||
<string name="storage_clear_shaders">Tyhjennä shaderit</string>
|
||||
<string name="storage_data_leftovers">Poistetut pelit</string>
|
||||
<string name="storage_steam_app">Steam-sovellus %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">Etuliite sisältää yhä tiedostoja, mahdollisesti paikallisia tallennuksia</string>
|
||||
<string name="storage_leftover_confirm">Tyhjentää Wine-etuliitteen ja shadervälimuistin (%1$s). Ota ensin kopio tarvitsemistasi tallennuksista; Steam Cloud -tallennukset pysyvät tililläsi.</string>
|
||||
<string name="storage_data_other">Säilytetään</string>
|
||||
<string name="storage_data_other_title">Muut kuin Steam-pelit ja jaettu Steam-etuliite</string>
|
||||
<string name="storage_data_other_detail">Kauppojen kautta tai käsin lisättyjen pelien etuliitteet</string>
|
||||
<string name="storage_components_packages">Ladatut paketit</string>
|
||||
<string name="storage_components_originals">Säilytetyt alkuperäiset</string>
|
||||
<string name="storage_components_none">Komponenttipaketteja ei ole ladattu.</string>
|
||||
<string name="storage_component_in_use">Valittu Protonille tai pelille</string>
|
||||
<string name="storage_component_unused">Ei valittu missään</string>
|
||||
<string name="storage_original_kept">Palauttaa asennetun Protonin</string>
|
||||
<string name="storage_original_old">Proton-versio, jota ei enää ole asennettu</string>
|
||||
<string name="storage_tag_in_use">Käytössä</string>
|
||||
<string name="storage_components_note">Käytössä olevat komponentit pysyvät lukittuina. Valitse ne kohdassa Komponentit; täältä poistetut voi ladata siellä uudelleen.</string>
|
||||
<string name="storage_component_confirm">Vapauttaa %1$s. Itse tuotuja paketteja ei voi ladata uudelleen.</string>
|
||||
<string name="storage_desktop_detail">%1$s Flatpak-sovelluksille ja niiden jakamalle</string>
|
||||
<string name="storage_desktop_apps_title">Sovellukset</string>
|
||||
<string name="storage_desktop_none">Flatpak-sovelluksia ei ole asennettu.</string>
|
||||
<string name="storage_desktop_shared_title">Sovellusten jakama</string>
|
||||
<string name="storage_desktop_runtimes">Ajonaikaympäristöt</string>
|
||||
<string name="storage_desktop_runtimes_detail">Kirjastot, joilla sovellukset pyörivät; käyttämättömät lähtevät kohdasta Poista käyttämättömät</string>
|
||||
<string name="storage_desktop_cache">Latausvarasto</string>
|
||||
<string name="storage_desktop_cache_detail">Flatpakin objektivarasto ja ladattu välimuisti</string>
|
||||
<string name="storage_desktop_prune">Poista käyttämättömät</string>
|
||||
<string name="storage_desktop_prune_body">Poistaa ajonaikaympäristöt, joita mikään asennettu sovellus ei tarvitse, ja siivoaa Flatpakin objektivaraston. Asennetut sovellukset toimivat edelleen.</string>
|
||||
<string name="storage_flatpak_uninstall_body">Poistaa sovelluksen (%1$s) ja ajonaikaympäristöt, joita mikään muu ei käytä. Kotikansiosi tiedot säilyvät.</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">%d paketti</item>
|
||||
<item quantity="other">%d pakettia</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">%d sovellus</item>
|
||||
<item quantity="other">%d sovellusta</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">Käynnistysvideo</string>
|
||||
<string name="boot_movie_video">Video</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d fps</string>
|
||||
<string name="boot_movie_note">60 fps näyttää sulavammalta, mutta kuormittaa suoritinta enemmän Steamin käynnistyessä. Steamin omista asetuksista valittu käynnistysvideo on etusijalla.</string>
|
||||
<string name="boot_movie_sound">Ääni</string>
|
||||
<string name="boot_movie_custom">Oma video</string>
|
||||
<string name="boot_movie_custom_hint">MP4, MOV, MKV tai WebM. Muut muodot ja yli %1$d Mt:n tiedostot muunnetaan laitteella 720p-WebM-muotoon, enintään %2$d sekuntia.</string>
|
||||
<string name="boot_movie_import">Tuo</string>
|
||||
<string name="boot_movie_remove">Poista</string>
|
||||
<string name="boot_movie_pick">Valitse käynnistysvideo</string>
|
||||
<string name="boot_movie_imported">%1$s on nyt käynnistysvideo</string>
|
||||
<string name="boot_movie_imported_silent">%1$s on nyt käynnistysvideo, ilman ääntä</string>
|
||||
<string name="boot_movie_too_long">Muunnettavan videon pituus saa olla enintään %1$d sekuntia</string>
|
||||
<string name="boot_movie_converting">Muunnetaan %1$d %%</string>
|
||||
<string name="boot_movie_converting_started">Muunnetaan %1$s Steamille. Pidä DroidDeck auki.</string>
|
||||
<string name="boot_movie_unsupported">Laite ei pysty purkamaan videota.</string>
|
||||
<string name="boot_movie_import_failed">Videon tuonti epäonnistui</string>
|
||||
</resources>
|
||||
@@ -58,7 +58,7 @@
|
||||
<string name="game_env_save">Enregistrer</string>
|
||||
<string name="game_env_remove">Supprimer</string>
|
||||
<string name="game_env_restore">Rétablir l\'héritage</string>
|
||||
<string name="game_env_reset">Réinitialiser ce profil</string>
|
||||
<string name="game_env_reset">Effacer les remplacements d\'environnement</string>
|
||||
<string name="game_env_unset">Annuler la définition pour ce profil</string>
|
||||
<string name="game_env_empty">Aucune variable définie.</string>
|
||||
<string name="game_env_inherited">Hérité</string>
|
||||
@@ -140,7 +140,7 @@
|
||||
<string name="user_apps_failed">Échec de l\'ajout de l\'application</string>
|
||||
<string name="user_apps_github_checking">Recherche des versions de %1$s</string>
|
||||
<string name="user_apps_github_unreachable">Impossible de lire les versions de %1$s</string>
|
||||
<string name="user_apps_github_no_appimage">%1$s ne fournit pas de version ARM64 AppImage</string>
|
||||
<string name="user_apps_github_no_appimage">Aucune version de %1$s ne contient d\'AppImage ARM64 ou x86_64</string>
|
||||
<string name="user_apps_downloading">Téléchargement de %1$s en cours</string>
|
||||
<string name="user_apps_download_failed">Échec du téléchargement</string>
|
||||
<string name="user_apps_update_failed">Échec de la mise à jour de l\'application</string>
|
||||
@@ -159,8 +159,8 @@
|
||||
<string name="add_app_tab_github">GitHub</string>
|
||||
<string name="add_app_tab_flatpak">Flatpak</string>
|
||||
<string name="add_app_script_hint">Un fichier .sh. Il s\'exécutera depuis son propre dossier et s\'affichera en plein écran via gamescope.</string>
|
||||
<string name="add_app_appimage_hint">Un AppImage ARM64 (aarch64). Il sera décompressé dans l\'environnement d\'exécution Linux lors de la première utilisation.</string>
|
||||
<string name="add_app_github_hint">Installer l\'AppImage ARM64 depuis la dernière version du dépôt.</string>
|
||||
<string name="add_app_appimage_hint">Un AppImage ARM64 (aarch64), ou un AppImage x86_64 exécuté via FEX. Il sera décompressé dans l\'environnement d\'exécution Linux lors de la première utilisation.</string>
|
||||
<string name="add_app_github_hint">Installer l\'AppImage ARM64 de la dernière version du dépôt, ou son AppImage x86_64 via FEX.</string>
|
||||
<string name="app_fex_title">Utiliser FEX</string>
|
||||
<string name="app_fex_auto">Auto</string>
|
||||
<string name="app_fex_on">Activé</string>
|
||||
@@ -266,7 +266,7 @@
|
||||
<string name="mode_controller_ds4">DualShock 4</string>
|
||||
<string name="mode_controller_dualsense">DualSense</string>
|
||||
<string name="mode_controller_x360">Manette Xbox 360</string>
|
||||
<string name="mode_controller_note">Deck : fournit un bouton d\'accès rapide et un gyroscope. Xbox 360 : utilisez Guide+A pour ouvrir l\'accès rapide.</string>
|
||||
<string name="mode_controller_note">Deck : bouton d\'accès rapide et gyroscope. Steam Controller : idem, avec détection de la prise en main sur les boutons arrière. DualShock 4 et DualSense : gyroscope, et les trackpads servent de pavé tactile. Xbox 360 : utilisez Guide+A pour ouvrir l\'accès rapide.</string>
|
||||
<string name="mode_back">Retour</string>
|
||||
<string name="back_menu_then_qam">Menu → QAM</string>
|
||||
<string name="back_qam_then_menu">QAM → Menu</string>
|
||||
@@ -471,7 +471,7 @@
|
||||
<string name="comp_applies_next_game">Prendra effet au prochain lancement du jeu</string>
|
||||
<string name="comp_import_zip">Importer .zip</string>
|
||||
<string name="comp_reading">Lecture de Proton…</string>
|
||||
<string name="comp_no_proton">Proton n\'est pas encore installé. Lancez Steam une fois pour télécharger son Proton ARM64, ou ajoutez GE-Proton ou CachyOS depuis les « Paramètres ».</string>
|
||||
<string name="comp_no_proton">Proton n\'est pas encore installé. Lancez Steam une fois pour télécharger son Proton ARM64, ou ajoutez GE-Proton, CachyOS ou DroidDeck-Proton depuis les « Paramètres ».</string>
|
||||
<string name="comp_original">Original · %1$s</string>
|
||||
<string name="comp_this_build">%1$s · Version actuelle</string>
|
||||
<string name="comp_earlier_build">%1$s · Version antérieure</string>
|
||||
@@ -577,6 +577,10 @@
|
||||
<string name="drawer_cas">Netteté (CAS)</string>
|
||||
<string name="drawer_cas_level">Niveau de netteté</string>
|
||||
<string name="drawer_fake_hdr">Faux HDR</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">Lancer SteamVR</string>
|
||||
<string name="steamvr_checking">Vérification de SteamVR…</string>
|
||||
<string name="drawer_deband">Anti-banding</string>
|
||||
<string name="drawer_deband_strength">Intensité de l\'anti-banding</string>
|
||||
<string name="drawer_brightness">Luminosité</string>
|
||||
@@ -594,6 +598,11 @@
|
||||
<string name="drawer_texture_sharpness_note">Un biais de mip négatif : textures plus nettes, un peu de scintillement. Auto s\'adapte au facteur d\'agrandissement de la session à l\'écran.</string>
|
||||
<string name="drawer_touch_auto">Auto (%1$s)</string>
|
||||
<string name="drawer_keyboard">Clavier</string>
|
||||
<string name="drawer_latency">Latence</string>
|
||||
<string name="drawer_low_latency_controller">Manette à faible latence</string>
|
||||
<string name="drawer_low_latency_controller_hint">La manette émet à 1000 Hz au lieu de 250 Hz. Un peu plus de CPU.</string>
|
||||
<string name="drawer_low_latency_touch">Tactile à faible latence</string>
|
||||
<string name="drawer_low_latency_touch_hint">Les touchers ignorent le regroupement d\'images ; les commandes tactiles Steam passent à 1000 Hz. Un peu plus de CPU.</string>
|
||||
<string name="drawer_pc_keyboard">Clavier PC</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">Deuxième écran</string>
|
||||
@@ -857,7 +866,7 @@
|
||||
<string name="gate_eyebrow">Steam</string>
|
||||
<string name="gate_lede">Android arrête les applications qui lancent de nombreux processus en arrière-plan, et Steam en lance des dizaines. Tant que ce paramètre n\'est pas modifié, les jeux peuvent se fermer sans message d\'erreur.</string>
|
||||
<string name="gate_fix_for_me">Corriger pour moi</string>
|
||||
<string name="gate_toggle_steps">Activez « Désactiver la limite de processus enfants », puis revenez. Gardez les options pour les développeurs activées, sinon l\'interrupteur se désactivera à nouveau.</string>
|
||||
<string name="gate_toggle_steps">Activez « Désactiver les limites pour les processus enfant », puis revenez. Gardez les options pour les développeurs activées, sinon l\'interrupteur se désactivera à nouveau.</string>
|
||||
<string name="gate_open_dev">Ouvrir les options pour les développeurs</string>
|
||||
<string name="gate_no_toggle">Cette version d\'Android ne dispose pas de cet interrupteur, DroidDeck effectuera donc la modification via le débogage sans fil. Cela prend environ une minute, sans ordinateur.</string>
|
||||
<string name="gate_needs_wifi">Le débogage sans fil nécessite un Wi-Fi. N\'importe quel réseau convient, Internet n\'est pas nécessaire.</string>
|
||||
@@ -907,8 +916,8 @@
|
||||
<string name="wadb_turn_off">Désactiver la limite</string>
|
||||
<string name="wadb_other_ways">Autres méthodes de modification</string>
|
||||
<string name="wadb_dev_try_first">1 · Options pour les développeurs (à essayer en premier)</string>
|
||||
<string name="wadb_restrict_on">Désactivez «Désactiver les limites pour les processus enfant».</string>
|
||||
<string name="wadb_restrict_off">Activez «Désactiver les limites pour les processus enfant».</string>
|
||||
<string name="wadb_restrict_on">Désactivez « Désactiver les limites pour les processus enfant ».</string>
|
||||
<string name="wadb_restrict_off">Activez « Désactiver les limites pour les processus enfant ».</string>
|
||||
<string name="wadb_computer_last">3 · ADB sur PC (en dernier recours)</string>
|
||||
<string name="wadb_only_if">À utiliser uniquement si le débogage sans fil n\'est pas disponible.</string>
|
||||
<string name="wadb_computer_group">3 · ADB sur PC (en dernier recours)</string>
|
||||
@@ -1023,7 +1032,6 @@
|
||||
<string name="gpu_set_with_runtime">Configuré avec le pilote d\'exécution : %1$s inclut les deux.</string>
|
||||
<string name="gpu_runtime_hint">Utilisé par les jeux et le bureau.</string>
|
||||
<string name="gpu_runtime_default">Par défaut de l\'environnement d\'exécution</string>
|
||||
<string name="gpu_display_hint">Responsable de la sortie vidéo vers l\'écran. Prendra effet après le redémarrage de DroidDeck.</string>
|
||||
<string name="gpu_auto_by_gpu">Auto - Sélection par GPU</string>
|
||||
<string name="gpu_manage">Télécharger, importer ou supprimer…</string>
|
||||
<string name="gpu_in_use_caps">Utilisé</string>
|
||||
@@ -1257,10 +1265,16 @@
|
||||
<string name="drivers_downloading_pair">Téléchargement de %1$s %2$s…</string>
|
||||
<string name="drivers_updated_to">Mis à jour vers %1$s %2$s.</string>
|
||||
<string name="drivers_updated_to_restart">Mis à jour vers %1$s %2$s. Le pilote d\'affichage prendra effet après le redémarrage de DroidDeck.</string>
|
||||
<string name="drivers_auto_waits_for_session">Les mises à jour de pilotes attendent la fin de la session.</string>
|
||||
<string name="drivers_auto_waits_for_network">%1$s %2$s sera téléchargé dès que vous quitterez les données mobiles et l\'économiseur de batterie.</string>
|
||||
<string name="drivers_skipping_bad">%1$s %2$s n\'a pas démarré de session ici, Auto garde donc les pilotes qui ont fonctionné.</string>
|
||||
<string name="drivers_rolled_back">%1$s n\'a pas démarré de session deux fois, Auto est donc revenu aux pilotes qui fonctionnaient.</string>
|
||||
<string name="drivers_switched_to">Basculé vers %1$s %2$s.</string>
|
||||
<string name="drivers_switched_to_restart">Basculé vers %1$s %2$s. Le pilote d\'affichage prendra effet après le redémarrage de DroidDeck.</string>
|
||||
<string name="drivers_using">Utilise %1$s %2$s.</string>
|
||||
<string name="drivers_using_restart">Utilise %1$s %2$s. Le pilote d\'affichage prendra effet après le redémarrage de DroidDeck.</string>
|
||||
<string name="drivers_using_quest">Utilise le pilote d\'affichage Quest intégré et la valeur par défaut de l\'environnement d\'exécution.</string>
|
||||
<string name="drivers_using_quest_restart">Utilise le pilote d\'affichage Quest intégré et la valeur par défaut de l\'environnement d\'exécution. Le pilote d\'affichage prend effet après un redémarrage de DroidDeck.</string>
|
||||
<string name="drivers_not_a_driver">Ce n\'est pas un zip de pilote : ni un pilote AdrenoTools, ni un Turnip -Linux, ni un bundle Android + Linux</string>
|
||||
<string name="drivers_import_failed">Échec de l\'importation : %1$s</string>
|
||||
<string name="drivers_imported_unnamed">Pilote importé</string>
|
||||
@@ -1693,7 +1707,7 @@
|
||||
<string name="adbpair_done_text">Appuyez pour revenir à DroidDeck. Vous pouvez maintenant désactiver le débogage sans fil.</string>
|
||||
<string name="adbpair_title_failed">Configuration du débogage sans fil interrompue</string>
|
||||
|
||||
<string name="selfinst_http">Le téléchargement a renvoyé HTTP %1$d</string>
|
||||
<string name="selfinst_download_failed">Échec du téléchargement</string>
|
||||
<string name="selfinst_size_mismatch">La taille du téléchargement ne correspond pas à l\'APK publié ; il a été supprimé - réessayez</string>
|
||||
<string name="selfinst_checksum_mismatch">Le téléchargement ne correspond pas à sa somme de contrôle et a été supprimé - réessayez</string>
|
||||
<string name="selfinst_wrong_package">La mise à jour concerne le paquet Android %1$s, et non %2$s</string>
|
||||
@@ -1791,7 +1805,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">Limite de requêtes GitHub atteinte - réessayez plus tard</string>
|
||||
<string name="turnip_github_http">GitHub a répondu HTTP %1$d</string>
|
||||
<string name="turnip_download_http">le téléchargement a répondu HTTP %1$d</string>
|
||||
<string name="turnip_download_failed">Échec du téléchargement</string>
|
||||
<string name="turnip_list_predates_checksums">Cette liste de pilotes est antérieure aux sommes de contrôle - actualisez-la et réessayez</string>
|
||||
<string name="turnip_checksum_mismatch">Somme de contrôle incorrecte - le téléchargement a été supprimé</string>
|
||||
<string name="turnip_not_adrenotools">Ce n\'est pas un pilote AdrenoTools : %1$s</string>
|
||||
@@ -1828,7 +1842,7 @@
|
||||
<string name="phantom_instructions_fix">%1$s Si ce n\'est pas possible, connectez cet appareil à un ordinateur avec ADB et exécutez la commande ci-dessous. Revenez ensuite ici et vérifiez à nouveau.</string>
|
||||
<string name="phantom_instructions_off">Les sessions Steam peuvent démarrer.</string>
|
||||
<string name="phantom_instructions_not_applicable">Cette version d\'Android n\'utilise pas cette limite.</string>
|
||||
<string name="phantom_fix_toggle">Activez « Désactiver les restrictions des processus enfants » dans les options pour les développeurs, puis laissez les options pour les développeurs activées.</string>
|
||||
<string name="phantom_fix_toggle">Activez « Désactiver les limites pour les processus enfant » dans les options pour les développeurs, puis laissez les options pour les développeurs activées.</string>
|
||||
<string name="phantom_fix_no_toggle">Cette version d\'Android ne dispose pas de cet interrupteur. Utilisez le débogage sans fil pour le modifier depuis cet appareil.</string>
|
||||
<string name="upd_block_no_apk">Cette version ne propose aucun téléchargement pour cette copie de DroidDeck.</string>
|
||||
<string name="upd_block_version_code">Android ne l\'installera pas par-dessus cette version, car son versionCode %1$d est inférieur à celui installé (%2$d).</string>
|
||||
@@ -2095,6 +2109,21 @@
|
||||
<string name="edit_other_exe">Autre exe…</string>
|
||||
<string name="edit_remove">Retirer</string>
|
||||
<string name="art_cover">Jaquette</string>
|
||||
<string name="art_wide_cover">Jaquette large</string>
|
||||
<string name="art_adjust_logo">Ajuster le logo</string>
|
||||
<string name="logo_pos_default">Par défaut</string>
|
||||
<string name="logo_pos_upper_left">En haut à gauche</string>
|
||||
<string name="logo_pos_upper_center">En haut au centre</string>
|
||||
<string name="logo_pos_center">Centre</string>
|
||||
<string name="logo_pos_bottom_left">En bas à gauche</string>
|
||||
<string name="logo_pos_bottom_center">En bas au centre</string>
|
||||
<string name="logo_pos_position">Position</string>
|
||||
<string name="logo_pos_size">Taille</string>
|
||||
<string name="logo_pos_save">Enregistrer</string>
|
||||
<string name="logo_pos_reset">Rétablir par défaut</string>
|
||||
<string name="logo_pos_hint">Dessiné sur l\'arrière-plan du jeu, comme le fait Steam. La position exacte peut varier de quelques pixels.</string>
|
||||
<string name="logo_pos_no_logo">Ce jeu n\'a pas encore de logo. Choisissez-en un dans Logo pour le voir ici.</string>
|
||||
<string name="logo_pos_failed">Impossible d\'enregistrer la position du logo. Réessayez.</string>
|
||||
<string name="art_background">Arrière-plan</string>
|
||||
<string name="art_logo">Logo</string>
|
||||
<string name="art_icon">Icône</string>
|
||||
@@ -2107,6 +2136,17 @@
|
||||
<string name="art_pick_image">Choisir une image…</string>
|
||||
<string name="art_pick_title">Choisir une image : %1$s</string>
|
||||
<string name="art_reset">Revenir à automatique</string>
|
||||
<string name="art_search_sgdb">Rechercher sur SteamGridDB…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB : %1$s</string>
|
||||
<string name="art_sgdb_search_label">Rechercher un jeu</string>
|
||||
<string name="art_sgdb_other_games">Autres résultats</string>
|
||||
<string name="art_sgdb_none">Aucun résultat.</string>
|
||||
<string name="art_sgdb_no_key">SteamGridDB nécessite une clé d\'API. Ajoutez la vôtre dans Paramètres › Lanceur.</string>
|
||||
<string name="art_sgdb_bad_key">SteamGridDB a refusé la clé d\'API. Vérifiez-la dans Paramètres › Lanceur.</string>
|
||||
<string name="art_sgdb_limit">SteamGridDB limite les requêtes en ce moment. Réessayez dans une minute.</string>
|
||||
<string name="art_sgdb_offline">Impossible de joindre SteamGridDB. Vérifiez la connexion.</string>
|
||||
<string name="art_sgdb_server">SteamGridDB a rencontré un problème. Réessayez plus tard.</string>
|
||||
<string name="art_sgdb_no_game">SteamGridDB n\'a pas de résultat pour ce jeu. Essayez une recherche.</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="one">%1$d jeu ajouté</item>
|
||||
<item quantity="many">%1$d jeux ajoutés</item>
|
||||
@@ -2136,6 +2176,45 @@
|
||||
<string name="gpu_bar_auto">Auto · %1$s</string>
|
||||
<string name="gpu_separately">Pilotes d\'exécution et d\'affichage séparément</string>
|
||||
<string name="gpu_use_title">Utiliser %1$s ?</string>
|
||||
<string name="gpu_mode_title">Comment les pilotes sont choisis</string>
|
||||
<string name="gpu_mode_auto_detail">La meilleure paire pour ce GPU, toujours à jour</string>
|
||||
<string name="gpu_mode_pair">Paire assortie</string>
|
||||
<string name="gpu_mode_pair_detail">Choisissez une paire ; elle définit les deux pilotes</string>
|
||||
<string name="gpu_mode_mix">Assortir librement</string>
|
||||
<string name="gpu_mode_mix_detail">Choisissez l\'affichage et l\'environnement d\'exécution séparément</string>
|
||||
<string name="gpu_mode_pair_short">Paire</string>
|
||||
<string name="gpu_mode_mix_short">Mixte</string>
|
||||
<string name="gpu_src_auto">AUTO</string>
|
||||
<string name="gpu_src_pair">PAIRE</string>
|
||||
<string name="gpu_src_bundle">LOT</string>
|
||||
<string name="gpu_src_chosen">CHOISI</string>
|
||||
<string name="gpu_src_by_gpu">PAR GPU</string>
|
||||
<string name="gpu_src_default">DÉFAUT</string>
|
||||
<string name="gpu_display_live">Dessine actuellement</string>
|
||||
<string name="gpu_display_pending">Redémarrage en attente · %1$s dessine d\'ici là</string>
|
||||
<string name="gpu_runtime_live">En cours</string>
|
||||
<string name="gpu_runtime_pending">Prochaine session · celle-ci garde %1$s</string>
|
||||
<string name="gpu_display_next">Dessine l\'écran. Prend effet à la prochaine session.</string>
|
||||
<string name="gpu_previous_driver">le pilote précédent</string>
|
||||
<string name="gpu_restart_banner">Redémarrez pour changer le pilote d\'affichage.</string>
|
||||
<string name="gpu_restart_blocked">Arrêtez la session en cours, puis redémarrez DroidDeck pour changer le pilote d\'affichage.</string>
|
||||
<string name="gpu_restart_now">Redémarrer</string>
|
||||
<string name="gpu_restart_later">Plus tard</string>
|
||||
<string name="gpu_auto_title">Auto a choisi cette paire pour votre %1$s</string>
|
||||
<string name="gpu_auto_builtin_title">Auto utilise les pilotes intégrés pour votre %1$s</string>
|
||||
<string name="gpu_auto_reason">Choisie pour la famille %1$s. Les nouvelles versions s\'installent en arrière-plan.</string>
|
||||
<string name="gpu_pair_title">Choisir une paire</string>
|
||||
<string name="gpu_pair_hint">Une paire définit ensemble le pilote d\'affichage et celui de l\'environnement d\'exécution.</string>
|
||||
<string name="gpu_mix_title">Choisir chaque pilote</string>
|
||||
<string name="gpu_mix_hint">Un lot remplit les deux emplacements. Les changements d\'affichage prennent effet après un redémarrage, ceux de l\'environnement d\'exécution à la prochaine session.</string>
|
||||
<string name="gpu_change_display">Changer le pilote d\'affichage</string>
|
||||
<string name="gpu_change_runtime">Changer le pilote d\'exécution</string>
|
||||
<string name="gpu_autos_pick">CHOIX D\'AUTO</string>
|
||||
<string name="gpu_installed">Installé</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · redémarrez pour appliquer</string>
|
||||
<string name="gpu_chip_restart">%1$s · redémarrage en attente</string>
|
||||
<string name="comp_this_device">Cet appareil</string>
|
||||
<string name="proton_remove_title">Supprimer %1$s ?</string>
|
||||
<string name="setup_tab_diagnostics">Diagnostic</string>
|
||||
<string name="setup_ready">Prêt à jouer</string>
|
||||
@@ -2147,6 +2226,14 @@
|
||||
<string name="setup_group_look">Apparence</string>
|
||||
<string name="setup_group_device">Cet appareil</string>
|
||||
<string name="setup_group_art">Illustrations de la bibliothèque</string>
|
||||
<string name="setup_group_download">Téléchargements</string>
|
||||
<string name="setup_download_source">Source</string>
|
||||
<string name="setup_download_source_auto">Automatique</string>
|
||||
<string name="setup_download_source_direct">Directe</string>
|
||||
<string name="setup_download_source_mirror">Miroir</string>
|
||||
<string name="setup_download_source_custom">Préfixe personnalisé</string>
|
||||
<string name="setup_download_mirror">Site</string>
|
||||
<string name="setup_download_prefix">Préfixe</string>
|
||||
<string name="flathub_eyebrow">APPLICATIONS LINUX</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">Applications pour le bureau Linux, à ouvrir depuis l\'onglet Bureau.</string>
|
||||
@@ -2264,12 +2351,9 @@
|
||||
<string name="cardboard_recenter_button">Recentrer</string>
|
||||
<string name="cardboard_steamvr">SteamVR (expérimental)</string>
|
||||
<string name="cardboard_steamvr_install">Installer</string>
|
||||
<string name="cardboard_steamvr_install_hint">Télécharge SteamVR ARM64 de Valve (571 Mo).</string>
|
||||
<string name="cardboard_steamvr_installing">Téléchargement et décompression de SteamVR…</string>
|
||||
<string name="cardboard_steamvr_start">Démarrer</string>
|
||||
<string name="cardboard_steamvr_ready_hint">Lance SteamVR avec votre tête comme casque ; l\'image passe en 3D côte à côte.</string>
|
||||
<string name="cardboard_steamvr_stop">Arrêter</string>
|
||||
<string name="cardboard_steamvr_running_hint">SteamVR est en cours d\'exécution.</string>
|
||||
<string name="cardboard_steamvr_unavailable">Indisponible dans cette session.</string>
|
||||
<string name="cardboard_steamvr_failed">SteamVR ne répond pas ; consultez le journal de session.</string>
|
||||
<string name="drawer_component_downloads">Télécharger des composants</string>
|
||||
@@ -2281,4 +2365,195 @@
|
||||
<string name="stores_storage_insufficient">Espace libre insuffisant : %1$s nécessaires, seulement %2$s libres</string>
|
||||
<string name="stores_storage_low">Espace insuffisant (%1$s libres, %2$s nécessaires)</string>
|
||||
<string name="stores_target_free_unknown">Espace libre inconnu</string>
|
||||
<string name="display_picture_position">Position de l\'image</string>
|
||||
<string name="display_position_top">Haut</string>
|
||||
<string name="display_position_bottom">Bas</string>
|
||||
<string name="display_position_center">Centre (par défaut)</string>
|
||||
<string name="setup_tab_storage">Stockage</string>
|
||||
<string name="storage_measuring">Mesure du stockage…</string>
|
||||
<string name="storage_measuring_hint">Cela prend quelques secondes la première fois.</string>
|
||||
<string name="storage_internal">Stockage interne</string>
|
||||
<string name="storage_used_of">%1$s sur %2$s utilisés</string>
|
||||
<string name="storage_free">%1$s libres</string>
|
||||
<string name="storage_droiddeck_uses">DroidDeck utilise %1$s. Mesuré le %2$s.</string>
|
||||
<string name="storage_rescanning">nouvelle mesure…</string>
|
||||
<string name="storage_other_apps">Autres applications Android</string>
|
||||
<string name="storage_safe_now">Libérable maintenant</string>
|
||||
<string name="storage_safe_hint">Environnements d\'exécution inutilisés, résidus, anciens journaux</string>
|
||||
<string name="storage_clean_up">Nettoyer…</string>
|
||||
<string name="storage_session_running">Arrêtez la session pour nettoyer ou supprimer quoi que ce soit. Les tailles restent affichées.</string>
|
||||
<string name="storage_where">Où ça va</string>
|
||||
<string name="storage_games">Jeux Steam</string>
|
||||
<string name="storage_game_data">Données de jeu</string>
|
||||
<string name="storage_game_data_hint">Préfixes %1$s · atelier %2$s · shaders %3$s</string>
|
||||
<string name="storage_tools">Proton et environnements d\'exécution</string>
|
||||
<string name="storage_tools_unused">%1$s inutilisés</string>
|
||||
<string name="storage_stores">GOG, Epic et autres boutiques</string>
|
||||
<string name="storage_stores_hint">Désinstallez-les depuis Boutiques</string>
|
||||
<string name="storage_desktop">Applications de bureau</string>
|
||||
<string name="storage_desktop_hint">Applications et environnements d\'exécution Flatpak</string>
|
||||
<string name="storage_system">Système Linux et client Steam</string>
|
||||
<string name="storage_system_hint">Nécessaire pour exécuter quoi que ce soit</string>
|
||||
<string name="storage_app_data">Données de DroidDeck</string>
|
||||
<string name="storage_app_data_hint">Journaux des anciennes sessions %1$s · caches · pilotes</string>
|
||||
<string name="storage_legend_games">Jeux</string>
|
||||
<string name="storage_legend_data">Données de jeu</string>
|
||||
<string name="storage_legend_tools">Proton et environnements d\'exécution</string>
|
||||
<string name="storage_legend_stores">Autres boutiques</string>
|
||||
<string name="storage_legend_desktop">Applications de bureau</string>
|
||||
<string name="storage_legend_system">Système Linux</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">Nettoyage</string>
|
||||
<string name="storage_clean_up_lede">Rien ici ne touche à vos jeux installés ni aux téléchargements en cours.</string>
|
||||
<string name="storage_nothing_to_clean">Rien à nettoyer pour le moment.</string>
|
||||
<string name="storage_selected">Sélectionné : %1$s · %2$s → %3$s libres</string>
|
||||
<string name="storage_cleaning">Nettoyage…</string>
|
||||
<string name="storage_free_button">Libérer %1$s</string>
|
||||
<string name="storage_freed">%1$s libérés.</string>
|
||||
<string name="storage_item_runtimes">Environnements d\'exécution et Proton Steam inutilisés</string>
|
||||
<string name="storage_item_leftovers">Résidus de jeux désinstallés</string>
|
||||
<string name="storage_item_leftovers_unsure">Résidus pouvant contenir des sauvegardes</string>
|
||||
<string name="storage_item_logs">Journaux des anciennes sessions</string>
|
||||
<string name="storage_item_logs_detail">Conserve les journaux de la session en cours</string>
|
||||
<string name="storage_item_custom_detail">Aucun jeu ne l\'utilise. Réinstallez-le depuis Composants si besoin.</string>
|
||||
<string name="storage_item_shaders">Caches de shaders des jeux installés</string>
|
||||
<string name="storage_item_shaders_detail">Reconstruits au fil du jeu. Les premières minutes de chaque jeu saccadent à nouveau.</string>
|
||||
<string name="storage_tag_safe">Sûr</string>
|
||||
<string name="storage_tag_unused">Inutilisé</string>
|
||||
<string name="storage_tag_stutter">Saccades</string>
|
||||
<string name="storage_tag_saves">Vérifier les sauvegardes</string>
|
||||
<string name="storage_tag_default">Par défaut</string>
|
||||
<string name="storage_tag_required">Nécessaire</string>
|
||||
<string name="storage_tag_not_used">Inutilisé</string>
|
||||
<string name="storage_back">Stockage</string>
|
||||
<string name="storage_games_with_data">%1$s avec leurs données</string>
|
||||
<string name="storage_sort_largest">Les plus gros</string>
|
||||
<string name="storage_sort_least_played">Les moins joués</string>
|
||||
<string name="storage_sort_sd">Sur la carte SD</string>
|
||||
<string name="storage_no_games">Aucun jeu ici.</string>
|
||||
<string name="storage_played">Joué %1$s</string>
|
||||
<string name="storage_never_played">Jamais joué</string>
|
||||
<string name="storage_game_parts">Jeu %1$s · données %2$s</string>
|
||||
<string name="storage_uninstall">Désinstaller</string>
|
||||
<string name="storage_games_note">Désinstaller ouvre la boîte de dialogue de Steam pour que Steam en tienne compte. Pour déplacer un jeu sur la carte SD, utilisez Steam › Paramètres › Stockage.</string>
|
||||
<string name="storage_uninstall_title">Désinstaller %1$s ?</string>
|
||||
<string name="storage_uninstall_body">Steam s\'ouvre et vous demande de confirmer. Cela libère les %1$s du jeu. Son préfixe reste jusqu\'au nettoyage des résidus, et les sauvegardes Steam Cloud restent dans votre compte.</string>
|
||||
<string name="storage_open_steam">Ouvrir Steam</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s inutilisés</string>
|
||||
<string name="storage_remove_unused">Supprimer l\'inutilisé · %1$s</string>
|
||||
<string name="storage_in_use">Utilisé</string>
|
||||
<string name="storage_not_used">Inutilisé</string>
|
||||
<string name="storage_remove">Supprimer</string>
|
||||
<string name="storage_remove_title">Supprimer %1$s ?</string>
|
||||
<string name="storage_remove_body">Libère %1$s. Steam le retéléchargera si un jeu le demande.</string>
|
||||
<string name="storage_tool_fex_runtime">Nécessaire pour les jeux Linux x86 (FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">Nécessaire aux Proton ARM64</string>
|
||||
<string name="storage_tool_redist">Programmes d\'installation VC++ et DirectX demandés par les jeux</string>
|
||||
<string name="storage_tool_fex">Exécute les jeux Linux x86</string>
|
||||
<string name="storage_tool_steam">Outil Steam</string>
|
||||
<string name="storage_tool_old_runtime">DroidDeck exécute les jeux sans cet environnement d\'exécution</string>
|
||||
<string name="storage_tool_x86_proton">Proton x86, que DroidDeck ne peut pas exécuter</string>
|
||||
<string name="storage_tool_custom_default">Par défaut pour tous les jeux</string>
|
||||
<string name="storage_tool_custom_unused">Aucun jeu ne l\'utilise</string>
|
||||
<string name="storage_tool_custom_unknown">Les paramètres de Steam sont illisibles, il est donc conservé</string>
|
||||
<string name="storage_cleanup_busy">Attendez la fin du nettoyage du stockage</string>
|
||||
<string name="storage_low_title">Seulement %1$s libres</string>
|
||||
<string name="storage_low_body">Steam a besoin de place pour les mises à jour et les caches de shaders. Sous 10 Go, les mises à jour de jeux peuvent échouer en cours de route.</string>
|
||||
<string name="storage_low_safe">%1$s peuvent être libérés sans risque.</string>
|
||||
<string name="storage_free_up">Libérer de l\'espace</string>
|
||||
<string name="storage_play_anyway">Jouer quand même</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d installé</item>
|
||||
<item quantity="other">%d installés</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d sur la carte SD</item>
|
||||
<item quantity="other">%d sur la carte SD</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">%d outil</item>
|
||||
<item quantity="other">%d outils</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">Dossiers de préfixe vides et cache de shaders de %d jeu</item>
|
||||
<item quantity="other">Dossiers de préfixe vides et caches de shaders de %d jeux</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">Le préfixe de %d jeu contient encore des fichiers, dont d\'éventuelles sauvegardes locales. Sauvegardez-les avant si vous y tenez.</item>
|
||||
<item quantity="other">Les préfixes de %d jeux contiennent encore des fichiers, dont d\'éventuelles sauvegardes locales. Sauvegardez-les avant si vous y tenez.</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">%d jeu</item>
|
||||
<item quantity="other">%d jeux</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">Proton par défaut de DroidDeck</string>
|
||||
<string name="storage_tool_proton">Option Proton 11 de DroidDeck</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">Utilisé par %d jeu</item>
|
||||
<item quantity="other">Utilisé par %d jeux</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">Composants</string>
|
||||
<string name="storage_components">Composants</string>
|
||||
<string name="storage_item_components">Composants inutilisés</string>
|
||||
<string name="storage_component_original">Original %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">Libère %1$s. Ne restaure qu\'une version de Proton qui n\'est plus installée.</string>
|
||||
<string name="storage_leftovers_unchecked">Non vérifié : un manifeste d\'application Steam est illisible, les préfixes des jeux désinstallés sont donc conservés tant que ce n\'est pas le cas.</string>
|
||||
<string name="storage_failed">Impossible de terminer : %1$s</string>
|
||||
<string name="storage_desktop_shared">%1$s partagés</string>
|
||||
<string name="storage_data_installed">Jeux installés</string>
|
||||
<string name="storage_data_parts">Préfixe %1$s · shaders %2$s · atelier %3$s</string>
|
||||
<string name="storage_clear_shaders">Effacer les shaders</string>
|
||||
<string name="storage_data_leftovers">Jeux désinstallés</string>
|
||||
<string name="storage_steam_app">Application Steam %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">Son préfixe contient encore des fichiers, dont d\'éventuelles sauvegardes locales</string>
|
||||
<string name="storage_leftover_confirm">Supprime son préfixe Wine et son cache de shaders (%1$s). Sauvegardez d\'abord les sauvegardes que vous voulez ; celles de Steam Cloud restent dans votre compte.</string>
|
||||
<string name="storage_data_other">Conservés</string>
|
||||
<string name="storage_data_other_title">Jeux hors Steam et préfixe partagé de Steam</string>
|
||||
<string name="storage_data_other_detail">Préfixes de jeux ajoutés depuis les boutiques ou à la main</string>
|
||||
<string name="storage_components_packages">Paquets téléchargés</string>
|
||||
<string name="storage_components_originals">Originaux conservés</string>
|
||||
<string name="storage_components_none">Aucun paquet de composant téléchargé.</string>
|
||||
<string name="storage_component_in_use">Choisi pour un Proton ou un jeu</string>
|
||||
<string name="storage_component_unused">Choisi nulle part</string>
|
||||
<string name="storage_original_kept">Restaure le Proton installé</string>
|
||||
<string name="storage_original_old">Version de Proton qui n\'est plus installée</string>
|
||||
<string name="storage_tag_in_use">Utilisé</string>
|
||||
<string name="storage_components_note">Les composants utilisés restent verrouillés. Choisissez-les dans Composants ; ce qui est supprimé ici peut y être retéléchargé.</string>
|
||||
<string name="storage_component_confirm">Libère %1$s. Les paquets que vous avez importés vous-même ne peuvent pas être retéléchargés.</string>
|
||||
<string name="storage_desktop_detail">%1$s pour les applications Flatpak et ce qu\'elles partagent</string>
|
||||
<string name="storage_desktop_apps_title">Applications</string>
|
||||
<string name="storage_desktop_none">Aucune application Flatpak installée.</string>
|
||||
<string name="storage_desktop_shared_title">Partagé par les applications</string>
|
||||
<string name="storage_desktop_runtimes">Environnements d\'exécution</string>
|
||||
<string name="storage_desktop_runtimes_detail">Bibliothèques dont dépendent les applications ; les inutilisées partent avec Supprimer l\'inutilisé</string>
|
||||
<string name="storage_desktop_cache">Dépôt de téléchargement</string>
|
||||
<string name="storage_desktop_cache_detail">Dépôt d\'objets et téléchargements en cache de Flatpak</string>
|
||||
<string name="storage_desktop_prune">Supprimer l\'inutilisé</string>
|
||||
<string name="storage_desktop_prune_body">Supprime les environnements d\'exécution dont aucune application installée n\'a besoin et élague le dépôt d\'objets de Flatpak. Les applications installées continuent de fonctionner.</string>
|
||||
<string name="storage_flatpak_uninstall_body">Supprime l\'application (%1$s) et tout environnement d\'exécution que rien d\'autre n\'utilise. Ses données dans votre dossier personnel restent.</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">%d paquet</item>
|
||||
<item quantity="other">%d paquets</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">%d application</item>
|
||||
<item quantity="other">%d applications</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">Vidéo de démarrage</string>
|
||||
<string name="boot_movie_video">Vidéo</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d i/s</string>
|
||||
<string name="boot_movie_note">60 i/s est plus fluide mais sollicite davantage le processeur pendant le démarrage de Steam. Une vidéo de démarrage choisie dans les paramètres de Steam est prioritaire.</string>
|
||||
<string name="boot_movie_sound">Son</string>
|
||||
<string name="boot_movie_custom">Votre propre vidéo</string>
|
||||
<string name="boot_movie_custom_hint">MP4, MOV, MKV ou WebM. Les autres formats et les fichiers de plus de %1$d Mo sont convertis en WebM 720p sur l\'appareil, jusqu\'à %2$d secondes.</string>
|
||||
<string name="boot_movie_import">Importer</string>
|
||||
<string name="boot_movie_remove">Supprimer</string>
|
||||
<string name="boot_movie_pick">Choisir une vidéo de démarrage</string>
|
||||
<string name="boot_movie_imported">%1$s est maintenant la vidéo de démarrage</string>
|
||||
<string name="boot_movie_imported_silent">%1$s est maintenant la vidéo de démarrage, sans son</string>
|
||||
<string name="boot_movie_too_long">Les vidéos à convertir doivent durer %1$d secondes au maximum</string>
|
||||
<string name="boot_movie_converting">Conversion %1$d %%</string>
|
||||
<string name="boot_movie_converting_started">Conversion de %1$s pour Steam. Gardez DroidDeck ouvert.</string>
|
||||
<string name="boot_movie_unsupported">Cet appareil ne peut pas décoder cette vidéo.</string>
|
||||
<string name="boot_movie_import_failed">Impossible d\'importer cette vidéo</string>
|
||||
</resources>
|
||||
@@ -278,6 +278,9 @@
|
||||
<string name="mode_suspend_steam_note">ऑटो दिखने पर रुकता है और फिर से शुरू होता है। नेटिव पहले Steam को स्लीप के लिए तैयार करता है। मैनुअल फिर से शुरू करें का इंतज़ार करता है। कभी नहीं वाला चलता रहता है।</string>
|
||||
<string name="mode_background_downloads">डाउनलोड चलते रहने दें</string>
|
||||
<string name="mode_background_downloads_hint">Steam के सक्रिय रूप से डाउनलोड करते समय बैकग्राउंड सस्पेंशन को टालें।</string>
|
||||
<string name="mode_zram">zRAM कम्प्रेशन</string>
|
||||
<string name="mode_zram_hint">गेम चलने के दौरान Steam क्लाइंट की मेमोरी डिवाइस के zram में कम्प्रेस होती है, जिससे गेम को ज़्यादा RAM मिलती है। अगली बार Steam शुरू होने पर लागू होगा।</string>
|
||||
<string name="mode_zram_unsupported">उपलब्ध नहीं: इस डिवाइस में zram या swap नहीं है, या कर्नेल 5.4 से पुराना है।</string>
|
||||
<string name="mode_suspend_note">ऑटो बैकग्राउंड में रुकता है और दिखने पर फिर से शुरू होता है। मैनुअल फिर से शुरू करें का इंतज़ार करता है। कभी नहीं वाला चलता रहता है।</string>
|
||||
<string name="mode_startup">स्टार्टअप</string>
|
||||
<string name="mode_steam_startup">DroidDeck के साथ Steam शुरू करें</string>
|
||||
@@ -575,6 +578,10 @@
|
||||
<string name="drawer_cas">शार्पनिंग (CAS)</string>
|
||||
<string name="drawer_cas_level">शार्पनिंग स्तर</string>
|
||||
<string name="drawer_fake_hdr">फ़ेक HDR</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">SteamVR शुरू करें</string>
|
||||
<string name="steamvr_checking">SteamVR जाँचा जा रहा है…</string>
|
||||
<string name="drawer_deband">डीबैंडिंग</string>
|
||||
<string name="drawer_deband_strength">डीबैंडिंग तीव्रता</string>
|
||||
<string name="drawer_brightness">ब्राइटनेस</string>
|
||||
@@ -592,6 +599,11 @@
|
||||
<string name="drawer_texture_sharpness_note">एक नेगेटिव mip बायस: ज़्यादा शार्प टेक्सचर, कुछ शिमर। ऑटो उस हद से मेल खाता है जितना सेशन को स्क्रीन के लिए बड़ा किया गया है।</string>
|
||||
<string name="drawer_touch_auto">ऑटो (%1$s)</string>
|
||||
<string name="drawer_keyboard">कीबोर्ड</string>
|
||||
<string name="drawer_latency">लेटेंसी</string>
|
||||
<string name="drawer_low_latency_controller">कम लेटेंसी कंट्रोलर</string>
|
||||
<string name="drawer_low_latency_controller_hint">कंट्रोलर 250 Hz के बजाय 1000 Hz पर रिपोर्ट करता है। थोड़ा ज़्यादा CPU।</string>
|
||||
<string name="drawer_low_latency_touch">कम लेटेंसी टच</string>
|
||||
<string name="drawer_low_latency_touch_hint">टच फ़्रेम बैचिंग छोड़ देते हैं, इसलिए Steam का टच कंट्रोल 1000 Hz पर चलता है। थोड़ा ज़्यादा CPU।</string>
|
||||
<string name="drawer_pc_keyboard">पीसी कीबोर्ड</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">दूसरी स्क्रीन</string>
|
||||
@@ -683,6 +695,14 @@
|
||||
<string name="setup_saved_logs">सेव किए सेशन लॉग</string>
|
||||
<string name="setup_saved_logs_hint">सबसे नए %1$d रखता है</string>
|
||||
<string name="setup_clear_logs">लॉग हटाएं</string>
|
||||
<string name="setup_reset_settings">सभी सेटिंग रीसेट करें</string>
|
||||
<string name="setup_reset_settings_hint">हर सेटिंग को डिफ़ॉल्ट मान पर लौटाएँ</string>
|
||||
<string name="setup_reset_settings_button">रीसेट करें</string>
|
||||
<string name="setup_reset_settings_title">सभी सेटिंग रीसेट करें?</string>
|
||||
<string name="setup_reset_settings_body">डिस्प्ले, परफ़ॉर्मेंस, कंट्रोलर, थीम, भाषा और लॉन्चर की सेटिंग डिफ़ॉल्ट पर लौट जाएँगी। आपके गेम, Steam लॉगिन, स्टोर खाते, डाउनलोड किए गए ड्राइवर और स्टोरेज फ़ोल्डर बने रहेंगे।</string>
|
||||
<string name="setup_reset_settings_profiles">गेम प्रोफ़ाइल भी रीसेट करें (हर गेम के लिए Proton, FEX, कॉम्पोनेंट और रनटाइम एनवायरनमेंट, और डिफ़ॉल्ट प्रोफ़ाइल का एनवायरनमेंट)</string>
|
||||
<string name="setup_reset_settings_session_running">सेटिंग रीसेट करने से पहले सेशन रोकें।</string>
|
||||
<string name="setup_reset_settings_partial">सेटिंग रीसेट हुईं, पर सभी नहीं। विवरण लॉग में हैं।</string>
|
||||
<string name="setup_offline">ऑफ़लाइन मोड</string>
|
||||
<string name="setup_sign_in_first">पहले Steam में साइन इन करें</string>
|
||||
<string name="setup_launcher">लॉन्चर</string>
|
||||
@@ -843,7 +863,7 @@
|
||||
<string name="gate_eyebrow">Steam</string>
|
||||
<string name="gate_lede">Android उन ऐप को रोकता है जो कई बैकग्राउंड प्रोसेस शुरू करते हैं, और Steam दर्जनों शुरू करता है। जब तक यह नहीं बदलता, गेम बिना किसी एरर के बंद हो जाते हैं।</string>
|
||||
<string name="gate_fix_for_me">मेरे लिए ठीक करें</string>
|
||||
<string name="gate_toggle_steps">“Disable child process restrictions” चालू करें, फिर वापस आएं। डेवलपर विकल्प चालू रहने दें, नहीं तो यह स्विच फिर से बंद हो जाएगा।</string>
|
||||
<string name="gate_toggle_steps">“चाइल्ड प्रोसेस से जुड़ी पाबंदियां हटाएं” चालू करें, फिर वापस आएं। डेवलपर विकल्प चालू रहने दें, नहीं तो यह स्विच फिर से बंद हो जाएगा।</string>
|
||||
<string name="gate_open_dev">डेवलपर विकल्प खोलें</string>
|
||||
<string name="gate_no_toggle">इस Android वर्शन में इसके लिए कोई स्विच नहीं है, इसलिए DroidDeck इसे वायरलेस डीबगिंग के ज़रिए बदलता है। इसमें लगभग एक मिनट लगता है, किसी कंप्यूटर की ज़रूरत नहीं।</string>
|
||||
<string name="gate_needs_wifi">वायरलेस डीबगिंग के लिए Wi-Fi चाहिए। कोई भी नेटवर्क काम करेगा; इंटरनेट की ज़रूरत नहीं है।</string>
|
||||
@@ -1009,7 +1029,6 @@
|
||||
<string name="gpu_set_with_runtime">रनटाइम ड्राइवर के साथ सेट: %1$s दोनों साथ लाता है।</string>
|
||||
<string name="gpu_runtime_hint">गेम और डेस्कटॉप में उपयोग होता है।</string>
|
||||
<string name="gpu_runtime_default">रनटाइम डिफ़ॉल्ट</string>
|
||||
<string name="gpu_display_hint">स्क्रीन पर फ़्रेम बनाता है। DroidDeck रीस्टार्ट होने के बाद लागू होता है।</string>
|
||||
<string name="gpu_auto_by_gpu">ऑटो - GPU के हिसाब से चुना गया</string>
|
||||
<string name="gpu_manage">डाउनलोड, इंपोर्ट या डिलीट करें…</string>
|
||||
<string name="gpu_in_use_caps">उपयोग में</string>
|
||||
@@ -1239,10 +1258,16 @@
|
||||
<string name="drivers_downloading_pair">%1$s %2$s डाउनलोड हो रहा है…</string>
|
||||
<string name="drivers_updated_to">%1$s %2$s में अपडेट हो गया।</string>
|
||||
<string name="drivers_updated_to_restart">%1$s %2$s में अपडेट हो गया। DroidDeck रीस्टार्ट होने के बाद डिस्प्ले ड्राइवर लागू होता है।</string>
|
||||
<string name="drivers_auto_waits_for_session">ड्राइवर अपडेट सेशन रुकने का इंतज़ार करेंगे।</string>
|
||||
<string name="drivers_auto_waits_for_network">मोबाइल डेटा और बैटरी सेवर बंद करते ही %1$s %2$s डाउनलोड होगा।</string>
|
||||
<string name="drivers_skipping_bad">%1$s %2$s ने यहाँ सेशन शुरू नहीं किया, इसलिए ऑटो उन्हीं ड्राइवर पर रहेगा जिन्होंने किया।</string>
|
||||
<string name="drivers_rolled_back">%1$s ने दो बार सेशन शुरू नहीं किया, इसलिए ऑटो उन ड्राइवर पर लौट गया जो काम कर रहे थे।</string>
|
||||
<string name="drivers_switched_to">%1$s %2$s में बदल दिया गया।</string>
|
||||
<string name="drivers_switched_to_restart">%1$s %2$s में बदल दिया गया। DroidDeck रीस्टार्ट होने के बाद डिस्प्ले ड्राइवर लागू होता है।</string>
|
||||
<string name="drivers_using">%1$s %2$s इस्तेमाल हो रहा है।</string>
|
||||
<string name="drivers_using_restart">%1$s %2$s इस्तेमाल हो रहा है। DroidDeck रीस्टार्ट होने के बाद डिस्प्ले ड्राइवर लागू होता है।</string>
|
||||
<string name="drivers_using_quest">Quest के साथ आया डिस्प्ले ड्राइवर और डिफ़ॉल्ट रनटाइम इस्तेमाल हो रहे हैं।</string>
|
||||
<string name="drivers_using_quest_restart">Quest के साथ आया डिस्प्ले ड्राइवर और डिफ़ॉल्ट रनटाइम इस्तेमाल हो रहे हैं। डिस्प्ले ड्राइवर DroidDeck रीस्टार्ट करने के बाद लागू होगा।</string>
|
||||
<string name="drivers_not_a_driver">यह ड्राइवर zip नहीं है: न AdrenoTools ड्राइवर, न -Linux Turnip, न Android + Linux बंडल</string>
|
||||
<string name="drivers_import_failed">इंपोर्ट विफल: %1$s</string>
|
||||
<string name="drivers_imported_unnamed">इंपोर्ट किया गया ड्राइवर</string>
|
||||
@@ -1664,7 +1689,7 @@
|
||||
<string name="adbpair_done_text">DroidDeck पर वापस जाने के लिए टैप करें। अब आप वायरलेस डीबगिंग बंद कर सकते हैं।</string>
|
||||
<string name="adbpair_title_failed">वायरलेस डीबगिंग सेटअप रुक गया</string>
|
||||
|
||||
<string name="selfinst_http">डाउनलोड ने HTTP %1$d जवाब दिया</string>
|
||||
<string name="selfinst_download_failed">डाउनलोड विफल</string>
|
||||
<string name="selfinst_size_mismatch">डाउनलोड का आकार पब्लिश किए APK से मेल नहीं खाया और इसे हटा दिया गया - फिर कोशिश करें</string>
|
||||
<string name="selfinst_checksum_mismatch">डाउनलोड अपने चेकसम से मेल नहीं खाया और इसे हटा दिया गया - फिर कोशिश करें</string>
|
||||
<string name="selfinst_wrong_package">यह अपडेट Android पैकेज %1$s के लिए है, %2$s के लिए नहीं</string>
|
||||
@@ -1762,7 +1787,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">GitHub की रेट लिमिट पार हो गई - बाद में फिर कोशिश करें</string>
|
||||
<string name="turnip_github_http">GitHub ने HTTP %1$d जवाब दिया</string>
|
||||
<string name="turnip_download_http">डाउनलोड ने HTTP %1$d जवाब दिया</string>
|
||||
<string name="turnip_download_failed">डाउनलोड विफल</string>
|
||||
<string name="turnip_list_predates_checksums">यह ड्राइवर लिस्ट चेकसम से पुरानी है - इसे रीफ़्रेश करें और फिर कोशिश करें</string>
|
||||
<string name="turnip_checksum_mismatch">चेकसम मेल नहीं खाया - डाउनलोड हटा दिया गया</string>
|
||||
<string name="turnip_not_adrenotools">यह AdrenoTools ड्राइवर नहीं है: %1$s</string>
|
||||
@@ -1799,7 +1824,7 @@
|
||||
<string name="phantom_instructions_fix">%1$s अगर यह संभव नहीं है, तो इस डिवाइस को ADB वाले कंप्यूटर से कनेक्ट करें और नीचे दिया कमांड चलाएं। यहां वापस आएं और फिर से जांचें।</string>
|
||||
<string name="phantom_instructions_off">Steam सेशन शुरू हो सकते हैं।</string>
|
||||
<string name="phantom_instructions_not_applicable">यह Android वर्शन इस सीमा का उपयोग नहीं करता।</string>
|
||||
<string name="phantom_fix_toggle">डेवलपर विकल्प में “Disable child process restrictions” चालू करें, और बाद में डेवलपर विकल्प चालू रहने दें।</string>
|
||||
<string name="phantom_fix_toggle">डेवलपर विकल्प में “चाइल्ड प्रोसेस से जुड़ी पाबंदियां हटाएं” चालू करें, और बाद में डेवलपर विकल्प चालू रहने दें।</string>
|
||||
<string name="phantom_fix_no_toggle">इस Android वर्शन में इसके लिए कोई स्विच नहीं है। इसे इस डिवाइस से बदलने के लिए वायरलेस डीबगिंग इस्तेमाल करें।</string>
|
||||
<string name="upd_block_no_apk">इस बिल्ड में DroidDeck की इस कॉपी के लिए कोई डाउनलोड नहीं है।</string>
|
||||
<string name="upd_block_version_code">Android इसे इस बिल्ड के ऊपर इंस्टॉल नहीं करेगा क्योंकि इसका versionCode %1$d इंस्टॉल किए %2$d से कम है।</string>
|
||||
@@ -2031,6 +2056,9 @@
|
||||
<string name="stores_install_title">%1$s इंस्टॉल करें:</string>
|
||||
<string name="stores_target_internal">इंटरनल</string>
|
||||
<string name="stores_target_free">%1$s खाली</string>
|
||||
<string name="stores_target_free_unknown">खाली जगह अज्ञात</string>
|
||||
<string name="stores_storage_insufficient">खाली जगह कम है: %1$s चाहिए, सिर्फ़ %2$s खाली है</string>
|
||||
<string name="stores_storage_low">जगह कम है (%1$s खाली, %2$s चाहिए)</string>
|
||||
<string name="stores_notification_channel">स्टोर डाउनलोड</string>
|
||||
<string name="stores_notification_channel_hint">गेम डाउनलोड की प्रोग्रेस दिखाता है और डाउनलोड को बैकग्राउंड में चलते रहने देता है</string>
|
||||
<string name="stores_notification_one">डाउनलोड हो रहा है</string>
|
||||
@@ -2066,6 +2094,21 @@
|
||||
<string name="edit_other_exe">दूसरा exe…</string>
|
||||
<string name="edit_remove">हटाएं</string>
|
||||
<string name="art_cover">कवर</string>
|
||||
<string name="art_wide_cover">चौड़ा कवर</string>
|
||||
<string name="art_adjust_logo">लोगो समायोजित करें</string>
|
||||
<string name="logo_pos_default">डिफ़ॉल्ट</string>
|
||||
<string name="logo_pos_upper_left">ऊपर बाएँ</string>
|
||||
<string name="logo_pos_upper_center">ऊपर बीच में</string>
|
||||
<string name="logo_pos_center">बीच में</string>
|
||||
<string name="logo_pos_bottom_left">नीचे बाएँ</string>
|
||||
<string name="logo_pos_bottom_center">नीचे बीच में</string>
|
||||
<string name="logo_pos_position">स्थिति</string>
|
||||
<string name="logo_pos_size">आकार</string>
|
||||
<string name="logo_pos_save">सहेजें</string>
|
||||
<string name="logo_pos_reset">डिफ़ॉल्ट पर लौटाएँ</string>
|
||||
<string name="logo_pos_hint">गेम की पृष्ठभूमि के ऊपर बनता है, जैसे Steam करता है। सटीक स्थिति कुछ पिक्सेल अलग हो सकती है।</string>
|
||||
<string name="logo_pos_no_logo">इस गेम का लोगो अभी नहीं है। यहाँ देखने के लिए लोगो में से कोई चुनें।</string>
|
||||
<string name="logo_pos_failed">लोगो की स्थिति सहेजी नहीं जा सकी। फिर कोशिश करें।</string>
|
||||
<string name="art_background">बैकग्राउंड</string>
|
||||
<string name="art_logo">लोगो</string>
|
||||
<string name="art_icon">आइकन</string>
|
||||
@@ -2078,6 +2121,17 @@
|
||||
<string name="art_pick_image">इमेज चुनें…</string>
|
||||
<string name="art_pick_title">एक इमेज चुनें: %1$s</string>
|
||||
<string name="art_reset">ऑटोमैटिक पर रीसेट करें</string>
|
||||
<string name="art_search_sgdb">SteamGridDB पर खोजें…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB: %1$s</string>
|
||||
<string name="art_sgdb_search_label">गेम खोजें</string>
|
||||
<string name="art_sgdb_other_games">दूसरे नतीजे</string>
|
||||
<string name="art_sgdb_none">कोई नतीजा नहीं।</string>
|
||||
<string name="art_sgdb_no_key">SteamGridDB के लिए API कुंजी चाहिए। अपनी कुंजी सेटिंग › लॉन्चर में जोड़ें।</string>
|
||||
<string name="art_sgdb_bad_key">SteamGridDB ने API कुंजी नहीं मानी। इसे सेटिंग › लॉन्चर में जाँचें।</string>
|
||||
<string name="art_sgdb_limit">SteamGridDB अभी अनुरोध सीमित कर रहा है। एक मिनट बाद कोशिश करें।</string>
|
||||
<string name="art_sgdb_offline">SteamGridDB से संपर्क नहीं हो रहा। कनेक्शन जाँचें।</string>
|
||||
<string name="art_sgdb_server">SteamGridDB में कुछ गड़बड़ हुई। बाद में कोशिश करें।</string>
|
||||
<string name="art_sgdb_no_game">SteamGridDB पर इस गेम के नतीजे नहीं हैं। इसे खोजकर देखें।</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="one">%1$d गेम जोड़ा गया</item>
|
||||
<item quantity="other">%1$d गेम जोड़े गए</item>
|
||||
@@ -2105,6 +2159,45 @@
|
||||
<string name="gpu_bar_auto">ऑटो · %1$s</string>
|
||||
<string name="gpu_separately">रनटाइम और डिस्प्ले ड्राइवर अलग-अलग</string>
|
||||
<string name="gpu_use_title">%1$s इस्तेमाल करें?</string>
|
||||
<string name="gpu_mode_title">ड्राइवर कैसे चुने जाते हैं</string>
|
||||
<string name="gpu_mode_auto_detail">इस GPU के लिए सबसे अच्छी जोड़ी, हमेशा अपडेट</string>
|
||||
<string name="gpu_mode_pair">मिलती जोड़ी</string>
|
||||
<string name="gpu_mode_pair_detail">जोड़ी चुनें; दोनों ड्राइवर सेट कर देती है</string>
|
||||
<string name="gpu_mode_mix">मुक्त मिश्रण</string>
|
||||
<string name="gpu_mode_mix_detail">डिस्प्ले और रनटाइम अलग-अलग चुनें</string>
|
||||
<string name="gpu_mode_pair_short">जोड़ी</string>
|
||||
<string name="gpu_mode_mix_short">मिश्रित</string>
|
||||
<string name="gpu_src_auto">ऑटो</string>
|
||||
<string name="gpu_src_pair">जोड़ी</string>
|
||||
<string name="gpu_src_bundle">बंडल</string>
|
||||
<string name="gpu_src_chosen">चुना गया</string>
|
||||
<string name="gpu_src_by_gpu">GPU अनुसार</string>
|
||||
<string name="gpu_src_default">डिफ़ॉल्ट</string>
|
||||
<string name="gpu_display_live">अभी रेंडर कर रहा है</string>
|
||||
<string name="gpu_display_pending">रीस्टार्ट बाकी · तब तक %1$s रेंडर करता है</string>
|
||||
<string name="gpu_runtime_live">सक्रिय</string>
|
||||
<string name="gpu_runtime_pending">अगला सेशन · यह सेशन %1$s पर है</string>
|
||||
<string name="gpu_display_next">स्क्रीन रेंडर करता है। अगले सेशन से लागू।</string>
|
||||
<string name="gpu_previous_driver">पिछला ड्राइवर</string>
|
||||
<string name="gpu_restart_banner">डिस्प्ले ड्राइवर बदलने के लिए रीस्टार्ट करें।</string>
|
||||
<string name="gpu_restart_blocked">डिस्प्ले ड्राइवर बदलने के लिए मौजूदा सेशन रोकें और DroidDeck रीस्टार्ट करें।</string>
|
||||
<string name="gpu_restart_now">अभी रीस्टार्ट करें</string>
|
||||
<string name="gpu_restart_later">बाद में</string>
|
||||
<string name="gpu_auto_title">ऑटो ने आपके %1$s के लिए यह जोड़ी चुनी</string>
|
||||
<string name="gpu_auto_builtin_title">ऑटो आपके %1$s के लिए बिल्ट-इन ड्राइवर इस्तेमाल कर रहा है</string>
|
||||
<string name="gpu_auto_reason">%1$s परिवार के लिए चुना गया। नए संस्करण पृष्ठभूमि में इंस्टॉल होते हैं।</string>
|
||||
<string name="gpu_pair_title">जोड़ी चुनें</string>
|
||||
<string name="gpu_pair_hint">जोड़ी डिस्प्ले ड्राइवर और रनटाइम ड्राइवर एक साथ सेट करती है।</string>
|
||||
<string name="gpu_mix_title">हर ड्राइवर चुनें</string>
|
||||
<string name="gpu_mix_hint">बंडल दोनों जगह भर देता है। डिस्प्ले बदलाव रीस्टार्ट के बाद लागू होते हैं, रनटाइम बदलाव अगले सेशन से।</string>
|
||||
<string name="gpu_change_display">डिस्प्ले ड्राइवर बदलें</string>
|
||||
<string name="gpu_change_runtime">रनटाइम ड्राइवर बदलें</string>
|
||||
<string name="gpu_autos_pick">ऑटो की पसंद</string>
|
||||
<string name="gpu_installed">इंस्टॉल है</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · लागू करने के लिए रीस्टार्ट</string>
|
||||
<string name="gpu_chip_restart">%1$s · रीस्टार्ट बाकी</string>
|
||||
<string name="comp_this_device">यह डिवाइस</string>
|
||||
<string name="proton_remove_title">%1$s हटाएं?</string>
|
||||
<string name="setup_tab_diagnostics">डायग्नोस्टिक्स</string>
|
||||
<string name="setup_ready">खेलने के लिए तैयार</string>
|
||||
@@ -2116,6 +2209,14 @@
|
||||
<string name="setup_group_look">लुक</string>
|
||||
<string name="setup_group_device">यह डिवाइस</string>
|
||||
<string name="setup_group_art">लाइब्रेरी आर्ट</string>
|
||||
<string name="setup_group_download">डाउनलोड</string>
|
||||
<string name="setup_download_source">स्रोत</string>
|
||||
<string name="setup_download_source_auto">ऑटो</string>
|
||||
<string name="setup_download_source_direct">सीधा</string>
|
||||
<string name="setup_download_source_mirror">मिरर</string>
|
||||
<string name="setup_download_source_custom">अपना प्रीफ़िक्स</string>
|
||||
<string name="setup_download_mirror">साइट</string>
|
||||
<string name="setup_download_prefix">प्रीफ़िक्स</string>
|
||||
<string name="flathub_eyebrow">LINUX ऐप</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">Linux डेस्कटॉप के लिए डेस्कटॉप ऐप, डेस्कटॉप से खोले जाते हैं।</string>
|
||||
@@ -2223,12 +2324,9 @@
|
||||
<string name="cardboard_recenter_button">बीच में लाएं</string>
|
||||
<string name="cardboard_steamvr">SteamVR (प्रायोगिक)</string>
|
||||
<string name="cardboard_steamvr_install">इंस्टॉल करें</string>
|
||||
<string name="cardboard_steamvr_install_hint">Valve का ARM64 SteamVR डाउनलोड करता है (571 MB)।</string>
|
||||
<string name="cardboard_steamvr_installing">SteamVR डाउनलोड और अनपैक हो रहा है…</string>
|
||||
<string name="cardboard_steamvr_start">शुरू करें</string>
|
||||
<string name="cardboard_steamvr_ready_hint">आपके सिर को हेडसेट मानकर SteamVR चलाता है; तस्वीर साइड-बाय-साइड 3D में बदल जाती है।</string>
|
||||
<string name="cardboard_steamvr_stop">रोकें</string>
|
||||
<string name="cardboard_steamvr_running_hint">SteamVR चल रहा है।</string>
|
||||
<string name="cardboard_steamvr_unavailable">इस सेशन में उपलब्ध नहीं है।</string>
|
||||
<string name="cardboard_steamvr_failed">SteamVR ने जवाब नहीं दिया; सेशन लॉग देखें।</string>
|
||||
<string name="drawer_component_downloads">कंपोनेंट डाउनलोड करें</string>
|
||||
@@ -2242,4 +2340,195 @@
|
||||
<string name="stores_hltb_completionist">पूरा</string>
|
||||
<string name="stores_hltb_hours">%1$s घं</string>
|
||||
<string name="stores_hltb_minutes">%1$d मि</string>
|
||||
<string name="display_picture_position">चित्र की स्थिति</string>
|
||||
<string name="display_position_top">ऊपर</string>
|
||||
<string name="display_position_bottom">नीचे</string>
|
||||
<string name="display_position_center">बीच में (डिफ़ॉल्ट)</string>
|
||||
<string name="setup_tab_storage">स्टोरेज</string>
|
||||
<string name="storage_measuring">स्टोरेज मापा जा रहा है…</string>
|
||||
<string name="storage_measuring_hint">पहली बार कुछ सेकंड लगते हैं।</string>
|
||||
<string name="storage_internal">आंतरिक स्टोरेज</string>
|
||||
<string name="storage_used_of">%2$s में से %1$s इस्तेमाल</string>
|
||||
<string name="storage_free">%1$s खाली</string>
|
||||
<string name="storage_droiddeck_uses">DroidDeck %1$s इस्तेमाल करता है। %2$s पर मापा गया।</string>
|
||||
<string name="storage_rescanning">फिर से माप रहे हैं…</string>
|
||||
<string name="storage_other_apps">दूसरे Android ऐप</string>
|
||||
<string name="storage_safe_now">अभी खाली की जा सकती है</string>
|
||||
<string name="storage_safe_hint">अप्रयुक्त रनटाइम, बचे अवशेष, पुराने लॉग</string>
|
||||
<string name="storage_clean_up">साफ़ कर रहे हैं…</string>
|
||||
<string name="storage_session_running">साफ़ करने या कुछ हटाने के लिए सेशन रोकें। आकार दिखते रहेंगे।</string>
|
||||
<string name="storage_where">कहाँ जाता है</string>
|
||||
<string name="storage_games">Steam गेम</string>
|
||||
<string name="storage_game_data">गेम डेटा</string>
|
||||
<string name="storage_game_data_hint">प्रीफ़िक्स %1$s · वर्कशॉप %2$s · शेडर %3$s</string>
|
||||
<string name="storage_tools">Proton और रनटाइम</string>
|
||||
<string name="storage_tools_unused">%1$s अप्रयुक्त</string>
|
||||
<string name="storage_stores">GOG, Epic और दूसरे स्टोर</string>
|
||||
<string name="storage_stores_hint">इन्हें स्टोर में हटाएँ</string>
|
||||
<string name="storage_desktop">डेस्कटॉप ऐप</string>
|
||||
<string name="storage_desktop_hint">Flatpak ऐप और रनटाइम</string>
|
||||
<string name="storage_system">Linux सिस्टम और Steam क्लाइंट</string>
|
||||
<string name="storage_system_hint">कुछ भी चलाने के लिए ज़रूरी</string>
|
||||
<string name="storage_app_data">DroidDeck डेटा</string>
|
||||
<string name="storage_app_data_hint">पुराने सेशन के लॉग %1$s · कैश · ड्राइवर</string>
|
||||
<string name="storage_legend_games">गेम</string>
|
||||
<string name="storage_legend_data">गेम डेटा</string>
|
||||
<string name="storage_legend_tools">Proton और रनटाइम</string>
|
||||
<string name="storage_legend_stores">दूसरे स्टोर</string>
|
||||
<string name="storage_legend_desktop">डेस्कटॉप ऐप</string>
|
||||
<string name="storage_legend_system">Linux सिस्टम</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">साफ़ करें</string>
|
||||
<string name="storage_clean_up_lede">यहाँ कुछ भी इंस्टॉल किए गेम या चालू डाउनलोड को नहीं छूता।</string>
|
||||
<string name="storage_nothing_to_clean">अभी साफ़ करने के लिए कुछ नहीं है।</string>
|
||||
<string name="storage_selected">चुना गया: %1$s · %2$s → %3$s खाली होगा</string>
|
||||
<string name="storage_cleaning">साफ़ कर रहे हैं…</string>
|
||||
<string name="storage_free_button">%1$s खाली करें</string>
|
||||
<string name="storage_freed">%1$s खाली किया गया।</string>
|
||||
<string name="storage_item_runtimes">अप्रयुक्त Steam रनटाइम और Proton</string>
|
||||
<string name="storage_item_leftovers">हटाए गए गेम के अवशेष</string>
|
||||
<string name="storage_item_leftovers_unsure">अवशेष जिनमें सेव हो सकते हैं</string>
|
||||
<string name="storage_item_logs">पुराने सेशन के लॉग</string>
|
||||
<string name="storage_item_logs_detail">मौजूदा सेशन के लॉग बने रहते हैं</string>
|
||||
<string name="storage_item_custom_detail">कोई गेम इसे इस्तेमाल नहीं करता। ज़रूरत हो तो कॉम्पोनेंट में फिर से इंस्टॉल करें।</string>
|
||||
<string name="storage_item_shaders">इंस्टॉल किए गेम का शेडर कैश</string>
|
||||
<string name="storage_item_shaders_detail">खेलते समय फिर बनता है। हर गेम के पहले कुछ मिनट फिर अटकेंगे।</string>
|
||||
<string name="storage_tag_safe">सुरक्षित</string>
|
||||
<string name="storage_tag_unused">अप्रयुक्त</string>
|
||||
<string name="storage_tag_stutter">अटकना</string>
|
||||
<string name="storage_tag_saves">सेव जाँचें</string>
|
||||
<string name="storage_tag_default">डिफ़ॉल्ट</string>
|
||||
<string name="storage_tag_required">ज़रूरी</string>
|
||||
<string name="storage_tag_not_used">अप्रयुक्त</string>
|
||||
<string name="storage_back">स्टोरेज</string>
|
||||
<string name="storage_games_with_data">%1$s उनके डेटा के साथ</string>
|
||||
<string name="storage_sort_largest">सबसे बड़ा</string>
|
||||
<string name="storage_sort_least_played">सबसे कम खेला</string>
|
||||
<string name="storage_sort_sd">SD कार्ड पर</string>
|
||||
<string name="storage_no_games">यहाँ कोई गेम नहीं है।</string>
|
||||
<string name="storage_played">%1$s खेला</string>
|
||||
<string name="storage_never_played">कभी नहीं खेला</string>
|
||||
<string name="storage_game_parts">गेम %1$s · डेटा %2$s</string>
|
||||
<string name="storage_uninstall">अनइंस्टॉल करें</string>
|
||||
<string name="storage_games_note">अनइंस्टॉल करने पर Steam विंडो खुलती है ताकि Steam इसे दर्ज करे। गेम को SD कार्ड पर ले जाने के लिए Steam › सेटिंग › स्टोरेज इस्तेमाल करें।</string>
|
||||
<string name="storage_uninstall_title">%1$s अनइंस्टॉल करें?</string>
|
||||
<string name="storage_uninstall_body">Steam खुलेगा और पुष्टि माँगेगा। इससे गेम की %1$s खाली होती है। अवशेष साफ़ करने तक प्रीफ़िक्स बना रहता है, और Steam Cloud सेव आपके खाते में रहते हैं।</string>
|
||||
<string name="storage_open_steam">Steam खोलें</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s अप्रयुक्त</string>
|
||||
<string name="storage_remove_unused">अप्रयुक्त हटाएँ · %1$s</string>
|
||||
<string name="storage_in_use">इस्तेमाल में</string>
|
||||
<string name="storage_not_used">अप्रयुक्त</string>
|
||||
<string name="storage_remove">हटाएँ</string>
|
||||
<string name="storage_remove_title">%1$s हटाएँ?</string>
|
||||
<string name="storage_remove_body">%1$s खाली होगा। किसी गेम को ज़रूरत होने पर Steam इसे फिर डाउनलोड करेगा।</string>
|
||||
<string name="storage_tool_fex_runtime">Linux x86 गेम के लिए ज़रूरी (FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">ARM64 Proton के लिए ज़रूरी</string>
|
||||
<string name="storage_tool_redist">गेम को ज़रूरी VC++ और DirectX इंस्टॉल</string>
|
||||
<string name="storage_tool_fex">Linux x86 गेम चलाता है</string>
|
||||
<string name="storage_tool_steam">Steam टूल</string>
|
||||
<string name="storage_tool_old_runtime">DroidDeck इस रनटाइम के बिना गेम चलाता है</string>
|
||||
<string name="storage_tool_x86_proton">Proton x86, जिसे DroidDeck नहीं चला सकता</string>
|
||||
<string name="storage_tool_custom_default">सभी गेम के लिए डिफ़ॉल्ट</string>
|
||||
<string name="storage_tool_custom_unused">कोई गेम इसे इस्तेमाल नहीं करता</string>
|
||||
<string name="storage_tool_custom_unknown">Steam सेटिंग पढ़ी नहीं जा सकतीं, इसलिए यह रखा जाता है</string>
|
||||
<string name="storage_cleanup_busy">स्टोरेज सफ़ाई पूरी होने का इंतज़ार करें</string>
|
||||
<string name="storage_low_title">सिर्फ़ %1$s खाली</string>
|
||||
<string name="storage_low_body">Steam को अपडेट और शेडर कैश के लिए जगह चाहिए। 10 GB से कम पर गेम अपडेट बीच में विफल हो सकते हैं।</string>
|
||||
<string name="storage_low_safe">%1$s सुरक्षित रूप से खाली किया जा सकता है।</string>
|
||||
<string name="storage_free_up">जगह खाली करें</string>
|
||||
<string name="storage_play_anyway">फिर भी खेलें</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d इंस्टॉल</item>
|
||||
<item quantity="other">%d इंस्टॉल</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d SD कार्ड पर</item>
|
||||
<item quantity="other">%d SD कार्ड पर</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">%d टूल</item>
|
||||
<item quantity="other">%d टूल</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">%d गेम के खाली प्रीफ़िक्स फ़ोल्डर और शेडर कैश</item>
|
||||
<item quantity="other">%d गेम के खाली प्रीफ़िक्स फ़ोल्डर और शेडर कैश</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">%d गेम के प्रीफ़िक्स में अब भी फ़ाइलें हैं, जिनमें लोकल सेव भी हैं। ज़रूरत हो तो पहले कॉपी बना लें।</item>
|
||||
<item quantity="other">%d गेम के प्रीफ़िक्स में अब भी फ़ाइलें हैं, जिनमें लोकल सेव भी हैं। ज़रूरत हो तो पहले कॉपी बना लें।</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">%d गेम</item>
|
||||
<item quantity="other">%d गेम</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">DroidDeck का डिफ़ॉल्ट Proton</string>
|
||||
<string name="storage_tool_proton">DroidDeck का Proton 11 विकल्प</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">%d गेम इस्तेमाल करता है</item>
|
||||
<item quantity="other">%d गेम इस्तेमाल करते हैं</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">कॉम्पोनेंट</string>
|
||||
<string name="storage_components">कॉम्पोनेंट</string>
|
||||
<string name="storage_item_components">अप्रयुक्त कॉम्पोनेंट</string>
|
||||
<string name="storage_component_original">मूल %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">%1$s खाली होगा। सिर्फ़ वह Proton संस्करण लौटाता है जो अब इंस्टॉल नहीं है।</string>
|
||||
<string name="storage_leftovers_unchecked">जाँचा नहीं गया: Steam ऐप का मैनिफ़ेस्ट पढ़ा नहीं जा सकता, इसलिए हटाए गेम के प्रीफ़िक्स तब तक रहेंगे जब तक यह संभव न हो।</string>
|
||||
<string name="storage_failed">विफल: %1$s</string>
|
||||
<string name="storage_desktop_shared">%1$s साझा</string>
|
||||
<string name="storage_data_installed">इंस्टॉल किए गेम</string>
|
||||
<string name="storage_data_parts">प्रीफ़िक्स %1$s · शेडर %2$s · वर्कशॉप %3$s</string>
|
||||
<string name="storage_clear_shaders">शेडर साफ़ करें</string>
|
||||
<string name="storage_data_leftovers">हटाए गए गेम</string>
|
||||
<string name="storage_steam_app">Steam ऐप %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">प्रीफ़िक्स में अब भी फ़ाइलें हैं, शायद लोकल सेव</string>
|
||||
<string name="storage_leftover_confirm">Wine प्रीफ़िक्स और शेडर कैश साफ़ करता है (%1$s)। पहले ज़रूरी सेव की कॉपी बना लें; Steam Cloud सेव आपके खाते में रहते हैं।</string>
|
||||
<string name="storage_data_other">रखा गया</string>
|
||||
<string name="storage_data_other_title">गैर-Steam गेम और साझा Steam प्रीफ़िक्स</string>
|
||||
<string name="storage_data_other_detail">स्टोर से या हाथ से जोड़े गेम के प्रीफ़िक्स</string>
|
||||
<string name="storage_components_packages">डाउनलोड किए पैकेज</string>
|
||||
<string name="storage_components_originals">रखे गए मूल</string>
|
||||
<string name="storage_components_none">कोई कॉम्पोनेंट पैकेज डाउनलोड नहीं है।</string>
|
||||
<string name="storage_component_in_use">किसी Proton या गेम के लिए चुना गया</string>
|
||||
<string name="storage_component_unused">कहीं नहीं चुना गया</string>
|
||||
<string name="storage_original_kept">इंस्टॉल किया Proton लौटाता है</string>
|
||||
<string name="storage_original_old">Proton संस्करण जो अब इंस्टॉल नहीं है</string>
|
||||
<string name="storage_tag_in_use">इस्तेमाल में</string>
|
||||
<string name="storage_components_note">इस्तेमाल में कॉम्पोनेंट लॉक रहते हैं। इन्हें कॉम्पोनेंट में चुनें; यहाँ हटाया हुआ वहाँ फिर डाउनलोड किया जा सकता है।</string>
|
||||
<string name="storage_component_confirm">%1$s खाली होगा। खुद आयात किए पैकेज फिर डाउनलोड नहीं हो सकते।</string>
|
||||
<string name="storage_desktop_detail">Flatpak ऐप और उनकी साझा चीज़ों के लिए %1$s</string>
|
||||
<string name="storage_desktop_apps_title">ऐप</string>
|
||||
<string name="storage_desktop_none">कोई Flatpak ऐप इंस्टॉल नहीं है।</string>
|
||||
<string name="storage_desktop_shared_title">ऐप के बीच साझा</string>
|
||||
<string name="storage_desktop_runtimes">रनटाइम</string>
|
||||
<string name="storage_desktop_runtimes_detail">जिन लाइब्रेरी पर ऐप चलते हैं; अप्रयुक्त अप्रयुक्त हटाएँ से चले जाते हैं</string>
|
||||
<string name="storage_desktop_cache">डाउनलोड स्टोरेज</string>
|
||||
<string name="storage_desktop_cache_detail">Flatpak का ऑब्जेक्ट स्टोर और डाउनलोड किया कैश</string>
|
||||
<string name="storage_desktop_prune">अप्रयुक्त हटाएँ</string>
|
||||
<string name="storage_desktop_prune_body">वे रनटाइम हटाता है जिनकी किसी इंस्टॉल किए ऐप को ज़रूरत नहीं, और Flatpak का ऑब्जेक्ट स्टोर साफ़ करता है। इंस्टॉल किए ऐप चलते रहते हैं।</string>
|
||||
<string name="storage_flatpak_uninstall_body">ऐप (%1$s) और वे रनटाइम हटाता है जिन्हें कोई और इस्तेमाल नहीं करता। आपके होम फ़ोल्डर का डेटा बना रहता है।</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">%d पैकेज</item>
|
||||
<item quantity="other">%d पैकेज</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">%d ऐप</item>
|
||||
<item quantity="other">%d ऐप</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">बूट वीडियो</string>
|
||||
<string name="boot_movie_video">वीडियो</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d fps</string>
|
||||
<string name="boot_movie_note">60 fps ज़्यादा स्मूथ दिखता है, लेकिन Steam शुरू होते समय CPU ज़्यादा इस्तेमाल करता है। Steam की अपनी सेटिंग्स में चुना गया स्टार्टअप वीडियो प्राथमिकता लेता है।</string>
|
||||
<string name="boot_movie_sound">आवाज़</string>
|
||||
<string name="boot_movie_custom">अपना वीडियो</string>
|
||||
<string name="boot_movie_custom_hint">MP4, MOV, MKV या WebM. दूसरे फ़ॉर्मैट और %1$d MB से बड़ी फ़ाइलें डिवाइस पर 720p WebM में बदली जाती हैं, %2$d सेकंड तक।</string>
|
||||
<string name="boot_movie_import">इंपोर्ट करें</string>
|
||||
<string name="boot_movie_remove">हटाएं</string>
|
||||
<string name="boot_movie_pick">बूट वीडियो चुनें</string>
|
||||
<string name="boot_movie_imported">%1$s अब बूट वीडियो है</string>
|
||||
<string name="boot_movie_imported_silent">%1$s अब बूट वीडियो है, बिना आवाज़ के</string>
|
||||
<string name="boot_movie_too_long">बदले जाने वाले वीडियो %1$d सेकंड या उससे छोटे होने चाहिए</string>
|
||||
<string name="boot_movie_converting">बदला जा रहा है %1$d%%</string>
|
||||
<string name="boot_movie_converting_started">%1$s को Steam के लिए बदला जा रहा है। DroidDeck खुला रखें।</string>
|
||||
<string name="boot_movie_unsupported">यह डिवाइस उस वीडियो को डिकोड नहीं कर सकता।</string>
|
||||
<string name="boot_movie_import_failed">वीडियो इंपोर्ट नहीं हो सका</string>
|
||||
</resources>
|
||||
@@ -278,6 +278,9 @@
|
||||
<string name="mode_suspend_steam_note">Az Automatikus szüneteltet, és megjelenéskor folytatja. A Natív előbb felkészíti a Steamet az altatásra. A Manuális megvárja a Folytatást. A Soha mindig futva tartja.</string>
|
||||
<string name="mode_background_downloads">Letöltések futtatásának fenntartása</string>
|
||||
<string name="mode_background_downloads_hint">Elhalasztja a háttérbeli felfüggesztést, amíg a Steam aktívan letölt.</string>
|
||||
<string name="mode_zram">zRAM-tömörítés</string>
|
||||
<string name="mode_zram_hint">Játék közben a Steam kliens memóriája a készülék zramjába tömörül, így a játéknak több RAM jut. A Steam következő indításakor lép életbe.</string>
|
||||
<string name="mode_zram_unsupported">Nem elérhető: a készüléken nincs zram vagy swap, vagy a kernel régebbi az 5.4-nél.</string>
|
||||
<string name="mode_suspend_note">Az Automatikus szüneteltet a háttérben, és megjelenéskor folytatja. A Manuális megvárja a Folytatást. A Soha mindig futva tartja.</string>
|
||||
<string name="mode_startup">Indítás</string>
|
||||
<string name="mode_steam_startup">Steam indítása a DroidDeckkel</string>
|
||||
@@ -575,6 +578,10 @@
|
||||
<string name="drawer_cas">Élesítés (CAS)</string>
|
||||
<string name="drawer_cas_level">Élesítés szintje</string>
|
||||
<string name="drawer_fake_hdr">Hamis HDR</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">SteamVR indítása</string>
|
||||
<string name="steamvr_checking">SteamVR ellenőrzése…</string>
|
||||
<string name="drawer_deband">Csíkmentesítés</string>
|
||||
<string name="drawer_deband_strength">Csíkmentesítés erőssége</string>
|
||||
<string name="drawer_brightness">Fényerő</string>
|
||||
@@ -592,6 +599,11 @@
|
||||
<string name="drawer_texture_sharpness_note">Negatív mip-eltolás: élesebb textúrák, némi csillogással. Az Automatikus illeszkedik ahhoz, mennyire van felnagyítva a munkamenet a képernyőhöz.</string>
|
||||
<string name="drawer_touch_auto">Automatikus (%1$s)</string>
|
||||
<string name="drawer_keyboard">Billentyűzet</string>
|
||||
<string name="drawer_latency">Késleltetés</string>
|
||||
<string name="drawer_low_latency_controller">Alacsony késleltetésű kontroller</string>
|
||||
<string name="drawer_low_latency_controller_hint">A kontroller 250 Hz helyett 1000 Hz-en jelent. Kicsit több CPU.</string>
|
||||
<string name="drawer_low_latency_touch">Alacsony késleltetésű érintés</string>
|
||||
<string name="drawer_low_latency_touch_hint">Az érintések kihagyják a képkockák kötegelését, így a Steam érintésvezérlése 1000 Hz-en fut. Kicsit több CPU.</string>
|
||||
<string name="drawer_pc_keyboard">PC-billentyűzet</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">Második képernyő</string>
|
||||
@@ -683,6 +695,14 @@
|
||||
<string name="setup_saved_logs">Mentett munkamenet-naplók</string>
|
||||
<string name="setup_saved_logs_hint">A legújabb %1$d-t tartja meg</string>
|
||||
<string name="setup_clear_logs">Naplók törlése</string>
|
||||
<string name="setup_reset_settings">Minden beállítás visszaállítása</string>
|
||||
<string name="setup_reset_settings_hint">Minden beállítást visszaállít az alapértékre</string>
|
||||
<string name="setup_reset_settings_button">Visszaállítás</string>
|
||||
<string name="setup_reset_settings_title">Visszaállítod az összes beállítást?</string>
|
||||
<string name="setup_reset_settings_body">A kép, a teljesítmény, a kontroller, a téma, a nyelv és az indítóalkalmazás beállításai visszaállnak az alapértékre. A játékaid, a Steam-bejelentkezés, az áruházi fiókok, a letöltött illesztőprogramok és a tárhelymappák megmaradnak.</string>
|
||||
<string name="setup_reset_settings_profiles">A játékprofilok visszaállítása is (Proton, FEX, összetevők és futtatókörnyezet játékonként, valamint az Alapértelmezett profil környezete)</string>
|
||||
<string name="setup_reset_settings_session_running">A beállítások visszaállítása előtt állítsd le a munkamenetet.</string>
|
||||
<string name="setup_reset_settings_partial">A beállítások visszaálltak, de nem mindegyik. A részletek a naplókban vannak.</string>
|
||||
<string name="setup_offline">Offline mód</string>
|
||||
<string name="setup_sign_in_first">Előbb jelentkezzen be a Steambe</string>
|
||||
<string name="setup_launcher">Indítóalkalmazás</string>
|
||||
@@ -843,7 +863,7 @@
|
||||
<string name="gate_eyebrow">Steam</string>
|
||||
<string name="gate_lede">Az Android leállítja azokat az alkalmazásokat, amelyek sok háttérfolyamatot indítanak, a Steam pedig tucatnyit indít. Amíg ez nem változik, a játékok hibaüzenet nélkül bezáródnak.</string>
|
||||
<string name="gate_fix_for_me">Javítsa ezt helyettem</string>
|
||||
<string name="gate_toggle_steps">Kapcsolja be a „Gyermekfolyamat-korlátozások letiltása” kapcsolót, majd térjen vissza. Hagyja bekapcsolva a Fejlesztői beállításokat, különben a kapcsoló visszakapcsol.</string>
|
||||
<string name="gate_toggle_steps">Kapcsolja be az „Alárendelt folyamatok korlátozásának kikapcsolása” kapcsolót, majd térjen vissza. Hagyja bekapcsolva a Fejlesztői beállításokat, különben a kapcsoló visszakapcsol.</string>
|
||||
<string name="gate_open_dev">Fejlesztői beállítások megnyitása</string>
|
||||
<string name="gate_no_toggle">Ehhez az Android-verzióhoz nincs kapcsoló, ezért a DroidDeck a vezeték nélküli hibakeresésen keresztül módosítja. Körülbelül egy percet vesz igénybe, számítógép nélkül.</string>
|
||||
<string name="gate_needs_wifi">A vezeték nélküli hibakereséshez Wi-Fi szükséges. Bármelyik hálózat megfelel; internetkapcsolat nem szükséges.</string>
|
||||
@@ -905,7 +925,7 @@
|
||||
<string name="ctrl_opacity">Átlátszatlanság</string>
|
||||
<string name="ctrl_percent">%1$d%%</string>
|
||||
<string name="ctrl_size">Gombméret</string>
|
||||
<string name="ctrl_size_hint" formatted="false">A 100%% az alapértelmezett méret</string>
|
||||
<string name="ctrl_size_hint" formatted="false">A 100% az alapértelmezett méret</string>
|
||||
<string name="ctrl_stick_click">Kar kattintása</string>
|
||||
<string name="ctrl_stick_click_hint">Koppintson duplán, és tartsa lenyomva a kart az L3 vagy R3 érdekében</string>
|
||||
<string name="ctrl_adaptive">Adaptív karok</string>
|
||||
@@ -1009,7 +1029,6 @@
|
||||
<string name="gpu_set_with_runtime">A futtatókörnyezet-illesztőprogrammal együtt beállítva: a %1$s mindkettőt tartalmazza.</string>
|
||||
<string name="gpu_runtime_hint">A játékok és az asztal használja.</string>
|
||||
<string name="gpu_runtime_default">Futtatókörnyezet alapértelmezése</string>
|
||||
<string name="gpu_display_hint">A képkockákat rajzolja a képernyőre. A DroidDeck újraindítása után lép érvénybe.</string>
|
||||
<string name="gpu_auto_by_gpu">Automatikus – GPU szerint kiválasztva</string>
|
||||
<string name="gpu_manage">Letöltés, importálás vagy törlés…</string>
|
||||
<string name="gpu_in_use_caps">HASZNÁLATBAN</string>
|
||||
@@ -1239,10 +1258,16 @@
|
||||
<string name="drivers_downloading_pair">%1$s %2$s letöltése…</string>
|
||||
<string name="drivers_updated_to">Frissítve erre: %1$s %2$s.</string>
|
||||
<string name="drivers_updated_to_restart">Frissítve erre: %1$s %2$s. A megjelenítő-illesztőprogram a DroidDeck újraindítása után lép érvénybe.</string>
|
||||
<string name="drivers_auto_waits_for_session">Az illesztőprogram-frissítések a munkamenet leállásáig várnak.</string>
|
||||
<string name="drivers_auto_waits_for_network">A(z) %1$s %2$s letöltése akkor indul, amikor kilépsz a mobilnetről és az energiatakarékos módból.</string>
|
||||
<string name="drivers_skipping_bad">A(z) %1$s %2$s nem indított itt munkamenetet, ezért az Automatikus azoknál az illesztőprogramoknál marad, amelyek igen.</string>
|
||||
<string name="drivers_rolled_back">A(z) %1$s kétszer nem indított munkamenetet, ezért az Automatikus visszatért a működő illesztőprogramokhoz.</string>
|
||||
<string name="drivers_switched_to">Átváltva erre: %1$s %2$s.</string>
|
||||
<string name="drivers_switched_to_restart">Átváltva erre: %1$s %2$s. A megjelenítő-illesztőprogram a DroidDeck újraindítása után lép érvénybe.</string>
|
||||
<string name="drivers_using">%1$s %2$s használatban.</string>
|
||||
<string name="drivers_using_restart">%1$s %2$s használatban. A megjelenítő-illesztőprogram a DroidDeck újraindítása után lép érvénybe.</string>
|
||||
<string name="drivers_using_quest">A Questhez adott kép-illesztőprogram és az alapértelmezett futtatókörnyezet van használatban.</string>
|
||||
<string name="drivers_using_quest_restart">A Questhez adott kép-illesztőprogram és az alapértelmezett futtatókörnyezet van használatban. A kép-illesztőprogram a DroidDeck újraindítása után lép életbe.</string>
|
||||
<string name="drivers_not_a_driver">Nem illesztőprogram zip: nem AdrenoTools-illesztőprogram, nem -Linux Turnip és nem Android + Linux csomag</string>
|
||||
<string name="drivers_import_failed">Importálás sikertelen: %1$s</string>
|
||||
<string name="drivers_imported_unnamed">Illesztőprogram importálva</string>
|
||||
@@ -1664,7 +1689,7 @@
|
||||
<string name="adbpair_done_text">Koppintson a DroidDeckhez való visszatéréshez. A vezeték nélküli hibakeresést most kikapcsolhatja.</string>
|
||||
<string name="adbpair_title_failed">A vezeték nélküli hibakeresés beállítása megállt</string>
|
||||
|
||||
<string name="selfinst_http">A letöltés HTTP %1$d választ adott</string>
|
||||
<string name="selfinst_download_failed">A letöltés nem sikerült</string>
|
||||
<string name="selfinst_size_mismatch">A letöltés mérete nem egyezett a közzétett APK-val, és elvetésre került – próbálja újra</string>
|
||||
<string name="selfinst_checksum_mismatch">A letöltés nem egyezett az ellenőrzőösszegével, és elvetésre került – próbálja újra</string>
|
||||
<string name="selfinst_wrong_package">A frissítés a %1$s Android-csomaghoz tartozik, nem a %2$s-hez</string>
|
||||
@@ -1762,7 +1787,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">A GitHub korlátja elérve – próbálja újra később</string>
|
||||
<string name="turnip_github_http">A GitHub HTTP %1$d választ adott</string>
|
||||
<string name="turnip_download_http">a letöltés HTTP %1$d választ adott</string>
|
||||
<string name="turnip_download_failed">A letöltés nem sikerült</string>
|
||||
<string name="turnip_list_predates_checksums">Ez az illesztőprogram-lista régebbi, mint az ellenőrzőösszegek – frissítse, és próbálja újra</string>
|
||||
<string name="turnip_checksum_mismatch">Az ellenőrzőösszeg nem egyezik – a letöltés elvetve</string>
|
||||
<string name="turnip_not_adrenotools">Nem egy AdrenoTools illesztőprogram: %1$s</string>
|
||||
@@ -1799,7 +1824,7 @@
|
||||
<string name="phantom_instructions_fix">%1$s Ha ez nem lehetséges, csatlakoztassa ezt az eszközt egy ADB-vel rendelkező számítógéphez, és futtassa az alábbi parancsot. Térjen vissza ide, és ellenőrizze újra.</string>
|
||||
<string name="phantom_instructions_off">A Steam-munkamenetek elindulhatnak.</string>
|
||||
<string name="phantom_instructions_not_applicable">Ez az Android-verzió nem használja ezt a korlátot.</string>
|
||||
<string name="phantom_fix_toggle">Kapcsolja be a „Gyermekfolyamat-korlátozások letiltása” lehetőséget a Fejlesztői beállításokban, és utána hagyja bekapcsolva a Fejlesztői beállításokat.</string>
|
||||
<string name="phantom_fix_toggle">Kapcsolja be az „Alárendelt folyamatok korlátozásának kikapcsolása” lehetőséget a Fejlesztői beállításokban, és utána hagyja bekapcsolva a Fejlesztői beállításokat.</string>
|
||||
<string name="phantom_fix_no_toggle">Ehhez az Android-verzióhoz nincs kapcsoló. Használja a vezeték nélküli hibakeresést a módosításához erről az eszközről.</string>
|
||||
<string name="upd_block_no_apk">Ehhez a buildhez nincs letöltés a DroidDeck ezen másolatához.</string>
|
||||
<string name="upd_block_version_code">Az Android nem telepíti ezt a buildet fölé, mert a %1$d versionCode-ja a telepített %2$d alatt van.</string>
|
||||
@@ -2031,6 +2056,9 @@
|
||||
<string name="stores_install_title">%1$s telepítése:</string>
|
||||
<string name="stores_target_internal">Belső</string>
|
||||
<string name="stores_target_free">%1$s szabad</string>
|
||||
<string name="stores_target_free_unknown">A szabad hely ismeretlen</string>
|
||||
<string name="stores_storage_insufficient">Kevés a szabad hely: %1$s kell, csak %2$s szabad</string>
|
||||
<string name="stores_storage_low">Kevés a hely (%1$s szabad, %2$s kell)</string>
|
||||
<string name="stores_notification_channel">Áruházletöltések</string>
|
||||
<string name="stores_notification_channel_hint">Megjeleníti a játékletöltés folyamatát, és futva tartja a letöltéseket a háttérben</string>
|
||||
<string name="stores_notification_one">Letöltés</string>
|
||||
@@ -2066,6 +2094,21 @@
|
||||
<string name="edit_other_exe">Másik exe…</string>
|
||||
<string name="edit_remove">Eltávolítás</string>
|
||||
<string name="art_cover">Borító</string>
|
||||
<string name="art_wide_cover">Széles borító</string>
|
||||
<string name="art_adjust_logo">Logó igazítása</string>
|
||||
<string name="logo_pos_default">Alapértelmezett</string>
|
||||
<string name="logo_pos_upper_left">Bal felső</string>
|
||||
<string name="logo_pos_upper_center">Felső közép</string>
|
||||
<string name="logo_pos_center">Közép</string>
|
||||
<string name="logo_pos_bottom_left">Bal alsó</string>
|
||||
<string name="logo_pos_bottom_center">Alsó közép</string>
|
||||
<string name="logo_pos_position">Pozíció</string>
|
||||
<string name="logo_pos_size">Méret</string>
|
||||
<string name="logo_pos_save">Mentés</string>
|
||||
<string name="logo_pos_reset">Vissza az alapértelmezettre</string>
|
||||
<string name="logo_pos_hint">A játék háttere fölött rajzolódik ki, ahogy a Steam is teszi. A pontos pozíció néhány pixellel eltérhet.</string>
|
||||
<string name="logo_pos_no_logo">Ennek a játéknak még nincs logója. Válassz egyet a Logó alatt, hogy itt lásd.</string>
|
||||
<string name="logo_pos_failed">A logó pozícióját nem sikerült menteni. Próbáld újra.</string>
|
||||
<string name="art_background">Háttér</string>
|
||||
<string name="art_logo">Logó</string>
|
||||
<string name="art_icon">Ikon</string>
|
||||
@@ -2078,6 +2121,17 @@
|
||||
<string name="art_pick_image">Kép kiválasztása…</string>
|
||||
<string name="art_pick_title">Kép kiválasztása: %1$s</string>
|
||||
<string name="art_reset">Visszaállítás automatikusra</string>
|
||||
<string name="art_search_sgdb">Keresés a SteamGridDB-n…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB: %1$s</string>
|
||||
<string name="art_sgdb_search_label">Játék keresése</string>
|
||||
<string name="art_sgdb_other_games">További találatok</string>
|
||||
<string name="art_sgdb_none">Nincs találat.</string>
|
||||
<string name="art_sgdb_no_key">A SteamGridDB API-kulcsot igényel. Add hozzá a sajátodat a Beállítások › Indítóalkalmazás alatt.</string>
|
||||
<string name="art_sgdb_bad_key">A SteamGridDB nem fogadta el az API-kulcsot. Ellenőrizd a Beállítások › Indítóalkalmazás alatt.</string>
|
||||
<string name="art_sgdb_limit">A SteamGridDB most korlátozza a kéréseket. Próbáld újra egy perc múlva.</string>
|
||||
<string name="art_sgdb_offline">A SteamGridDB nem érhető el. Ellenőrizd a kapcsolatot.</string>
|
||||
<string name="art_sgdb_server">Valami hiba történt a SteamGridDB-nél. Próbáld később.</string>
|
||||
<string name="art_sgdb_no_game">A SteamGridDB-nek nincs találata erre a játékra. Próbáld rákeresni.</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="one">%1$d játék hozzáadva</item>
|
||||
<item quantity="other">%1$d játék hozzáadva</item>
|
||||
@@ -2105,6 +2159,45 @@
|
||||
<string name="gpu_bar_auto">Automatikus · %1$s</string>
|
||||
<string name="gpu_separately">Futtatókörnyezet- és megjelenítő-illesztőprogramok külön</string>
|
||||
<string name="gpu_use_title">Használja a %1$s-t?</string>
|
||||
<string name="gpu_mode_title">Hogyan választódnak ki az illesztőprogramok</string>
|
||||
<string name="gpu_mode_auto_detail">A legjobb pár ehhez a GPU-hoz, mindig friss</string>
|
||||
<string name="gpu_mode_pair">Illesztett pár</string>
|
||||
<string name="gpu_mode_pair_detail">Válassz párt; mindkét illesztőprogramot beállítja</string>
|
||||
<string name="gpu_mode_mix">Szabad kombinálás</string>
|
||||
<string name="gpu_mode_mix_detail">A képet és a futtatókörnyezetet külön válaszd</string>
|
||||
<string name="gpu_mode_pair_short">Pár</string>
|
||||
<string name="gpu_mode_mix_short">Vegyes</string>
|
||||
<string name="gpu_src_auto">AUTO</string>
|
||||
<string name="gpu_src_pair">PÁR</string>
|
||||
<string name="gpu_src_bundle">CSOMAG</string>
|
||||
<string name="gpu_src_chosen">KIVÁLASZTVA</string>
|
||||
<string name="gpu_src_by_gpu">GPU SZERINT</string>
|
||||
<string name="gpu_src_default">ALAP</string>
|
||||
<string name="gpu_display_live">Most rajzol</string>
|
||||
<string name="gpu_display_pending">Újraindítás kell · addig a(z) %1$s rajzol</string>
|
||||
<string name="gpu_runtime_live">Aktív</string>
|
||||
<string name="gpu_runtime_pending">Következő munkamenet · ez a(z) %1$s-t használja</string>
|
||||
<string name="gpu_display_next">A képernyőt rajzolja. A következő munkamenettől érvényes.</string>
|
||||
<string name="gpu_previous_driver">az előző illesztőprogram</string>
|
||||
<string name="gpu_restart_banner">Indítsd újra a kép-illesztőprogram váltásához.</string>
|
||||
<string name="gpu_restart_blocked">Állítsd le a jelenlegi munkamenetet és indítsd újra a DroidDecket a kép-illesztőprogram váltásához.</string>
|
||||
<string name="gpu_restart_now">Újraindítás most</string>
|
||||
<string name="gpu_restart_later">Később</string>
|
||||
<string name="gpu_auto_title">Az Automatikus ezt a párt választotta ehhez: %1$s</string>
|
||||
<string name="gpu_auto_builtin_title">Az Automatikus a beépített illesztőprogramokat használja ehhez: %1$s</string>
|
||||
<string name="gpu_auto_reason">A(z) %1$s családhoz választva. Az új verziók a háttérben települnek.</string>
|
||||
<string name="gpu_pair_title">Válassz párt</string>
|
||||
<string name="gpu_pair_hint">A pár a kép-illesztőprogramot és a futtatókörnyezet-illesztőprogramot együtt állítja be.</string>
|
||||
<string name="gpu_mix_title">Válaszd ki mindegyik illesztőprogramot</string>
|
||||
<string name="gpu_mix_hint">A csomag mindkét helyet kitölti. A kép módosításai újraindítás után, a futtatókörnyezet módosításai a következő munkamenettől érvényesek.</string>
|
||||
<string name="gpu_change_display">Kép-illesztőprogram váltása</string>
|
||||
<string name="gpu_change_runtime">Futtatókörnyezet-illesztőprogram váltása</string>
|
||||
<string name="gpu_autos_pick">AZ AUTOMATIKUS VÁLASZTÁSA</string>
|
||||
<string name="gpu_installed">Telepítve</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · újraindítás az érvényesítéshez</string>
|
||||
<string name="gpu_chip_restart">%1$s · újraindítás szükséges</string>
|
||||
<string name="comp_this_device">Ez a készülék</string>
|
||||
<string name="proton_remove_title">Eltávolítja a %1$s-t?</string>
|
||||
<string name="setup_tab_diagnostics">Diagnosztika</string>
|
||||
<string name="setup_ready">Készen áll a játékra</string>
|
||||
@@ -2116,6 +2209,14 @@
|
||||
<string name="setup_group_look">Megjelenés</string>
|
||||
<string name="setup_group_device">Ez az eszköz</string>
|
||||
<string name="setup_group_art">Könyvtár illusztrációi</string>
|
||||
<string name="setup_group_download">Letöltés</string>
|
||||
<string name="setup_download_source">Forrás</string>
|
||||
<string name="setup_download_source_auto">Automatikus</string>
|
||||
<string name="setup_download_source_direct">Közvetlen</string>
|
||||
<string name="setup_download_source_mirror">Tükör</string>
|
||||
<string name="setup_download_source_custom">Saját prefix</string>
|
||||
<string name="setup_download_mirror">Webhely</string>
|
||||
<string name="setup_download_prefix">Prefix</string>
|
||||
<string name="flathub_eyebrow">LINUX-ALKALMAZÁSOK</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">Asztali alkalmazások a Linux-asztalhoz, az Asztalról megnyitva.</string>
|
||||
@@ -2223,12 +2324,9 @@
|
||||
<string name="cardboard_recenter_button">Középre</string>
|
||||
<string name="cardboard_steamvr">SteamVR (kísérleti)</string>
|
||||
<string name="cardboard_steamvr_install">Telepítés</string>
|
||||
<string name="cardboard_steamvr_install_hint">Letölti a Valve ARM64-es SteamVR-jét (571 MB).</string>
|
||||
<string name="cardboard_steamvr_installing">SteamVR letöltése és kicsomagolása…</string>
|
||||
<string name="cardboard_steamvr_start">Indítás</string>
|
||||
<string name="cardboard_steamvr_ready_hint">A SteamVR-t a fejeddel mint headsettel futtatja; a kép egymás melletti 3D-re vált.</string>
|
||||
<string name="cardboard_steamvr_stop">Leállítás</string>
|
||||
<string name="cardboard_steamvr_running_hint">A SteamVR fut.</string>
|
||||
<string name="cardboard_steamvr_unavailable">Ebben a munkamenetben nem érhető el.</string>
|
||||
<string name="cardboard_steamvr_failed">A SteamVR nem válaszolt; nézd meg a munkamenet naplóját.</string>
|
||||
<string name="drawer_component_downloads">Összetevők letöltése</string>
|
||||
@@ -2242,4 +2340,195 @@
|
||||
<string name="stores_hltb_completionist">Teljes</string>
|
||||
<string name="stores_hltb_hours">%1$s óra</string>
|
||||
<string name="stores_hltb_minutes">%1$d perc</string>
|
||||
<string name="display_picture_position">A kép pozíciója</string>
|
||||
<string name="display_position_top">Fent</string>
|
||||
<string name="display_position_bottom">Lent</string>
|
||||
<string name="display_position_center">Középen (alapértelmezett)</string>
|
||||
<string name="setup_tab_storage">Tárhely</string>
|
||||
<string name="storage_measuring">Tárhely mérése…</string>
|
||||
<string name="storage_measuring_hint">Első alkalommal pár másodpercig tart.</string>
|
||||
<string name="storage_internal">Belső tárhely</string>
|
||||
<string name="storage_used_of">%1$s / %2$s használt</string>
|
||||
<string name="storage_free">%1$s szabad</string>
|
||||
<string name="storage_droiddeck_uses">A DroidDeck %1$s-t használ. Mérve: %2$s.</string>
|
||||
<string name="storage_rescanning">újramérés…</string>
|
||||
<string name="storage_other_apps">Egyéb Android-alkalmazások</string>
|
||||
<string name="storage_safe_now">Most felszabadítható</string>
|
||||
<string name="storage_safe_hint">Nem használt futtatókörnyezetek, maradványok, régi naplók</string>
|
||||
<string name="storage_clean_up">Takarítás…</string>
|
||||
<string name="storage_session_running">A takarításhoz vagy törléshez állítsd le a munkamenetet. A méretek láthatók maradnak.</string>
|
||||
<string name="storage_where">Hova kerül</string>
|
||||
<string name="storage_games">Steam-játékok</string>
|
||||
<string name="storage_game_data">Játékadatok</string>
|
||||
<string name="storage_game_data_hint">Prefixek %1$s · workshop %2$s · shaderek %3$s</string>
|
||||
<string name="storage_tools">Proton és futtatókörnyezetek</string>
|
||||
<string name="storage_tools_unused">%1$s nem használt</string>
|
||||
<string name="storage_stores">GOG, Epic és más áruházak</string>
|
||||
<string name="storage_stores_hint">Távolítsd el őket az Áruházak alatt</string>
|
||||
<string name="storage_desktop">Asztali alkalmazások</string>
|
||||
<string name="storage_desktop_hint">Flatpak-alkalmazások és futtatókörnyezetek</string>
|
||||
<string name="storage_system">Linux rendszer és Steam kliens</string>
|
||||
<string name="storage_system_hint">Szükséges ahhoz, hogy bármi fusson</string>
|
||||
<string name="storage_app_data">DroidDeck-adatok</string>
|
||||
<string name="storage_app_data_hint">Régi munkamenetek naplói %1$s · gyorsítótár · illesztőprogramok</string>
|
||||
<string name="storage_legend_games">Játékok</string>
|
||||
<string name="storage_legend_data">Játékadatok</string>
|
||||
<string name="storage_legend_tools">Proton és futtatókörnyezetek</string>
|
||||
<string name="storage_legend_stores">Más áruházak</string>
|
||||
<string name="storage_legend_desktop">Asztali alkalmazások</string>
|
||||
<string name="storage_legend_system">Linux rendszer</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">Takarítás</string>
|
||||
<string name="storage_clean_up_lede">Itt semmi sem érinti a telepített játékokat vagy a futó letöltéseket.</string>
|
||||
<string name="storage_nothing_to_clean">Most nincs mit takarítani.</string>
|
||||
<string name="storage_selected">Kijelölve: %1$s · %2$s → %3$s szabadul fel</string>
|
||||
<string name="storage_cleaning">Takarítás…</string>
|
||||
<string name="storage_free_button">%1$s felszabadítása</string>
|
||||
<string name="storage_freed">%1$s felszabadítva.</string>
|
||||
<string name="storage_item_runtimes">Nem használt Steam futtatókörnyezetek és Proton</string>
|
||||
<string name="storage_item_leftovers">Törölt játékok maradványai</string>
|
||||
<string name="storage_item_leftovers_unsure">Maradványok, amelyek mentéseket tartalmazhatnak</string>
|
||||
<string name="storage_item_logs">Régi munkamenetek naplói</string>
|
||||
<string name="storage_item_logs_detail">A jelenlegi munkamenet naplói megmaradnak</string>
|
||||
<string name="storage_item_custom_detail">Egy játék sem használja. Ha kell, telepítsd újra az Összetevők alatt.</string>
|
||||
<string name="storage_item_shaders">Telepített játékok shader-gyorsítótára</string>
|
||||
<string name="storage_item_shaders_detail">Játék közben újraépül. Minden játék első percei újra akadozni fognak.</string>
|
||||
<string name="storage_tag_safe">Biztonságos</string>
|
||||
<string name="storage_tag_unused">Nem használt</string>
|
||||
<string name="storage_tag_stutter">Akadozás</string>
|
||||
<string name="storage_tag_saves">Mentések ellenőrzése</string>
|
||||
<string name="storage_tag_default">Alapértelmezett</string>
|
||||
<string name="storage_tag_required">Szükséges</string>
|
||||
<string name="storage_tag_not_used">Nem használt</string>
|
||||
<string name="storage_back">Tárhely</string>
|
||||
<string name="storage_games_with_data">%1$s az adataikkal</string>
|
||||
<string name="storage_sort_largest">Legnagyobb</string>
|
||||
<string name="storage_sort_least_played">Legkevesebbet játszott</string>
|
||||
<string name="storage_sort_sd">SD-kártyán</string>
|
||||
<string name="storage_no_games">Itt nincsenek játékok.</string>
|
||||
<string name="storage_played">játszva: %1$s</string>
|
||||
<string name="storage_never_played">Sosem játszott</string>
|
||||
<string name="storage_game_parts">Játék %1$s · adatok %2$s</string>
|
||||
<string name="storage_uninstall">Eltávolítás</string>
|
||||
<string name="storage_games_note">Az eltávolítás megnyitja a Steam ablakát, hogy a Steam nyilvántartsa. A játék SD-kártyára mozgatásához használd a Steam › Beállítások › Tárhely menüpontot.</string>
|
||||
<string name="storage_uninstall_title">Eltávolítod ezt: %1$s?</string>
|
||||
<string name="storage_uninstall_body">A Steam megnyílik és megerősítést kér. Ez felszabadítja a játék %1$s-ját. A prefix a maradványok takarításáig megmarad, a Steam Cloud mentések pedig a fiókodban maradnak.</string>
|
||||
<string name="storage_open_steam">Steam megnyitása</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s nem használt</string>
|
||||
<string name="storage_remove_unused">Nem használtak eltávolítása · %1$s</string>
|
||||
<string name="storage_in_use">Használatban</string>
|
||||
<string name="storage_not_used">Nem használt</string>
|
||||
<string name="storage_remove">Eltávolítás</string>
|
||||
<string name="storage_remove_title">Eltávolítod ezt: %1$s?</string>
|
||||
<string name="storage_remove_body">Felszabadít %1$s-t. A Steam újra letölti, ha egy játéknak kell.</string>
|
||||
<string name="storage_tool_fex_runtime">Linux x86-os játékokhoz kell (FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">ARM64-es Protonokhoz kell</string>
|
||||
<string name="storage_tool_redist">A játékokhoz szükséges VC++ és DirectX telepítések</string>
|
||||
<string name="storage_tool_fex">Linux x86-os játékokat futtat</string>
|
||||
<string name="storage_tool_steam">Steam-eszköz</string>
|
||||
<string name="storage_tool_old_runtime">A DroidDeck e futtatókörnyezet nélkül futtat játékokat</string>
|
||||
<string name="storage_tool_x86_proton">Proton x86, amit a DroidDeck nem tud futtatni</string>
|
||||
<string name="storage_tool_custom_default">Alapértelmezett minden játéknak</string>
|
||||
<string name="storage_tool_custom_unused">Egy játék sem használja</string>
|
||||
<string name="storage_tool_custom_unknown">A Steam beállításai nem olvashatók, ezért megmarad</string>
|
||||
<string name="storage_cleanup_busy">Várd meg a tárhelytakarítás végét</string>
|
||||
<string name="storage_low_title">Csak %1$s szabad</string>
|
||||
<string name="storage_low_body">A Steamnek hely kell a frissítésekhez és a shader-gyorsítótárhoz. 10 GB alatt a játékfrissítések félúton meghiúsulhatnak.</string>
|
||||
<string name="storage_low_safe">%1$s biztonságosan felszabadítható.</string>
|
||||
<string name="storage_free_up">Hely felszabadítása</string>
|
||||
<string name="storage_play_anyway">Játék így is</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d telepítve</item>
|
||||
<item quantity="other">%d telepítve</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d SD-kártyán</item>
|
||||
<item quantity="other">%d SD-kártyán</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">%d eszköz</item>
|
||||
<item quantity="other">%d eszköz</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">Üres prefixmappák és shader-gyorsítótár %d játékból</item>
|
||||
<item quantity="other">Üres prefixmappák és shader-gyorsítótár %d játékból</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">%d játék prefixe még tartalmaz fájlokat, köztük helyi mentéseket. Ha kellenek, előbb készíts másolatot.</item>
|
||||
<item quantity="other">%d játék prefixe még tartalmaz fájlokat, köztük helyi mentéseket. Ha kellenek, előbb készíts másolatot.</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">%d játék</item>
|
||||
<item quantity="other">%d játék</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">A DroidDeck alap Protonja</string>
|
||||
<string name="storage_tool_proton">A DroidDeck Proton 11 lehetősége</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">%d játék használja</item>
|
||||
<item quantity="other">%d játék használja</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">Összetevők</string>
|
||||
<string name="storage_components">Összetevők</string>
|
||||
<string name="storage_item_components">Nem használt összetevők</string>
|
||||
<string name="storage_component_original">Eredeti %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">Felszabadít %1$s-t. Csak olyan Proton-verziót tesz vissza, amely már nincs telepítve.</string>
|
||||
<string name="storage_leftovers_unchecked">Nincs ellenőrizve: egy Steam-alkalmazás manifesztje nem olvasható, ezért a törölt játékok prefixei addig megmaradnak.</string>
|
||||
<string name="storage_failed">Sikertelen: %1$s</string>
|
||||
<string name="storage_desktop_shared">%1$s megosztva</string>
|
||||
<string name="storage_data_installed">Telepített játékok</string>
|
||||
<string name="storage_data_parts">Prefix %1$s · shaderek %2$s · workshop %3$s</string>
|
||||
<string name="storage_clear_shaders">Shaderek törlése</string>
|
||||
<string name="storage_data_leftovers">Törölt játékok</string>
|
||||
<string name="storage_steam_app">Steam-alkalmazás %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">A prefix még tartalmaz fájlokat, talán helyi mentéseket</string>
|
||||
<string name="storage_leftover_confirm">Törli a Wine prefixet és a shader-gyorsítótárat (%1$s). Előbb készíts másolatot a szükséges mentésekről; a Steam Cloud mentések a fiókodban maradnak.</string>
|
||||
<string name="storage_data_other">Megtartva</string>
|
||||
<string name="storage_data_other_title">Nem Steam-es játékok és a megosztott Steam prefix</string>
|
||||
<string name="storage_data_other_detail">Áruházból vagy kézzel hozzáadott játékok prefixei</string>
|
||||
<string name="storage_components_packages">Letöltött csomagok</string>
|
||||
<string name="storage_components_originals">Megtartott eredetiek</string>
|
||||
<string name="storage_components_none">Nincs letöltött összetevő-csomag.</string>
|
||||
<string name="storage_component_in_use">Protonhoz vagy játékhoz kiválasztva</string>
|
||||
<string name="storage_component_unused">Sehol nincs kiválasztva</string>
|
||||
<string name="storage_original_kept">Visszateszi a telepített Protont</string>
|
||||
<string name="storage_original_old">Már nem telepített Proton-verzió</string>
|
||||
<string name="storage_tag_in_use">Használatban</string>
|
||||
<string name="storage_components_note">A használatban lévő összetevők zárolva maradnak. Az Összetevők alatt válaszd ki őket; amit itt eltávolítasz, azt ott újra letöltheted.</string>
|
||||
<string name="storage_component_confirm">Felszabadít %1$s-t. A saját magad által importált csomagok nem tölthetők le újra.</string>
|
||||
<string name="storage_desktop_detail">%1$s a Flatpak-alkalmazásokhoz és amit megosztanak</string>
|
||||
<string name="storage_desktop_apps_title">Alkalmazások</string>
|
||||
<string name="storage_desktop_none">Nincs telepített Flatpak-alkalmazás.</string>
|
||||
<string name="storage_desktop_shared_title">Az alkalmazások megosztják</string>
|
||||
<string name="storage_desktop_runtimes">Futtatókörnyezetek</string>
|
||||
<string name="storage_desktop_runtimes_detail">A könyvtárak, amelyeken az alkalmazások futnak; a nem használtak a Nem használtak eltávolítása paranccsal távoznak</string>
|
||||
<string name="storage_desktop_cache">Letöltési tárhely</string>
|
||||
<string name="storage_desktop_cache_detail">A Flatpak objektumtára és letöltött gyorsítótára</string>
|
||||
<string name="storage_desktop_prune">Nem használtak eltávolítása</string>
|
||||
<string name="storage_desktop_prune_body">Eltávolítja azokat a futtatókörnyezeteket, amelyekre egyetlen telepített alkalmazásnak sincs szüksége, és rendezi a Flatpak objektumtárát. A telepített alkalmazások tovább működnek.</string>
|
||||
<string name="storage_flatpak_uninstall_body">Eltávolítja az alkalmazást (%1$s) és azokat a futtatókörnyezeteket, amelyeket más nem használ. A saját mappádban lévő adatok megmaradnak.</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">%d csomag</item>
|
||||
<item quantity="other">%d csomag</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">%d alkalmazás</item>
|
||||
<item quantity="other">%d alkalmazás</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">Indítóvideó</string>
|
||||
<string name="boot_movie_video">Videó</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d fps</string>
|
||||
<string name="boot_movie_note">A 60 fps simább, de jobban terheli a processzort a Steam indulása közben. A Steam saját beállításaiban választott indítóvideó elsőbbséget élvez.</string>
|
||||
<string name="boot_movie_sound">Hang</string>
|
||||
<string name="boot_movie_custom">Saját videó</string>
|
||||
<string name="boot_movie_custom_hint">MP4, MOV, MKV vagy WebM. Más formátumokat és a %1$d MB-nál nagyobb fájlokat az eszköz 720p WebM-re alakítja, legfeljebb %2$d másodpercig.</string>
|
||||
<string name="boot_movie_import">Importálás</string>
|
||||
<string name="boot_movie_remove">Eltávolítás</string>
|
||||
<string name="boot_movie_pick">Válassz indítóvideót</string>
|
||||
<string name="boot_movie_imported">Mostantól %1$s az indítóvideó</string>
|
||||
<string name="boot_movie_imported_silent">Mostantól %1$s az indítóvideó, hang nélkül</string>
|
||||
<string name="boot_movie_too_long">Az átalakítandó videó legfeljebb %1$d másodperces lehet</string>
|
||||
<string name="boot_movie_converting">Átalakítás %1$d%%</string>
|
||||
<string name="boot_movie_converting_started">%1$s átalakítása a Steamhez. Hagyd nyitva a DroidDecket.</string>
|
||||
<string name="boot_movie_unsupported">Ez az eszköz nem tudja dekódolni a videót.</string>
|
||||
<string name="boot_movie_import_failed">Nem sikerült importálni a videót</string>
|
||||
</resources>
|
||||
@@ -278,6 +278,9 @@
|
||||
<string name="mode_suspend_steam_note">Otomatis menjeda dan melanjutkan saat ditampilkan. Native menyiapkan Steam untuk tidur terlebih dahulu. Manual menunggu Lanjutkan. Tidak pernah terus berjalan.</string>
|
||||
<string name="mode_background_downloads">Pertahankan unduhan tetap berjalan</string>
|
||||
<string name="mode_background_downloads_hint">Menunda penjedaan latar belakang saat Steam sedang aktif mengunduh.</string>
|
||||
<string name="mode_zram">Kompresi zRAM</string>
|
||||
<string name="mode_zram_hint">Saat bermain, memori klien Steam dipadatkan ke zram perangkat sehingga gim mendapat lebih banyak RAM. Berlaku saat Steam dijalankan berikutnya.</string>
|
||||
<string name="mode_zram_unsupported">Tidak tersedia: perangkat ini tidak punya zram atau swap, atau kernelnya lebih lama dari 5.4.</string>
|
||||
<string name="mode_suspend_note">Otomatis menjeda di latar belakang dan melanjutkan saat ditampilkan. Manual menunggu Lanjutkan. Tidak pernah terus berjalan.</string>
|
||||
<string name="mode_startup">Mulai</string>
|
||||
<string name="mode_steam_startup">Mulai Steam bersama DroidDeck</string>
|
||||
@@ -573,6 +576,10 @@
|
||||
<string name="drawer_cas">Penajaman (CAS)</string>
|
||||
<string name="drawer_cas_level">Tingkat penajaman</string>
|
||||
<string name="drawer_fake_hdr">HDR palsu</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">Jalankan SteamVR</string>
|
||||
<string name="steamvr_checking">Memeriksa SteamVR…</string>
|
||||
<string name="drawer_deband">Debanding</string>
|
||||
<string name="drawer_deband_strength">Kekuatan debanding</string>
|
||||
<string name="drawer_brightness">Kecerahan</string>
|
||||
@@ -590,6 +597,11 @@
|
||||
<string name="drawer_texture_sharpness_note">Bias mip negatif: tekstur lebih tajam, sedikit kilauan. Otomatis menyesuaikan seberapa besar sesi diperbesar ke layar.</string>
|
||||
<string name="drawer_touch_auto">Otomatis (%1$s)</string>
|
||||
<string name="drawer_keyboard">Keyboard</string>
|
||||
<string name="drawer_latency">Latensi</string>
|
||||
<string name="drawer_low_latency_controller">Pengontrol latensi rendah</string>
|
||||
<string name="drawer_low_latency_controller_hint">Pengontrol melapor pada 1000 Hz, bukan 250 Hz. Sedikit lebih banyak CPU.</string>
|
||||
<string name="drawer_low_latency_touch">Sentuh latensi rendah</string>
|
||||
<string name="drawer_low_latency_touch_hint">Sentuhan melewati pengelompokan frame, jadi kontrol sentuh Steam berjalan pada 1000 Hz. Sedikit lebih banyak CPU.</string>
|
||||
<string name="drawer_pc_keyboard">Keyboard PC</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">Layar kedua</string>
|
||||
@@ -680,6 +692,14 @@
|
||||
<string name="setup_saved_logs">Log sesi tersimpan</string>
|
||||
<string name="setup_saved_logs_hint">Menyimpan %1$d terbaru</string>
|
||||
<string name="setup_clear_logs">Hapus log</string>
|
||||
<string name="setup_reset_settings">Setel ulang semua setelan</string>
|
||||
<string name="setup_reset_settings_hint">Kembalikan setiap setelan ke nilai bawaannya</string>
|
||||
<string name="setup_reset_settings_button">Setel ulang</string>
|
||||
<string name="setup_reset_settings_title">Setel ulang semua setelan?</string>
|
||||
<string name="setup_reset_settings_body">Setelan tampilan, performa, pengontrol, tema, bahasa, dan peluncur kembali ke bawaan. Gim, login Steam, akun toko, driver yang diunduh, dan folder penyimpanan tetap ada.</string>
|
||||
<string name="setup_reset_settings_profiles">Setel ulang profil gim juga (Proton, FEX, komponen, dan lingkungan runtime per gim, serta lingkungan profil Bawaan)</string>
|
||||
<string name="setup_reset_settings_session_running">Hentikan sesi sebelum menyetel ulang setelan.</string>
|
||||
<string name="setup_reset_settings_partial">Setelan disetel ulang, tetapi tidak semuanya. Rinciannya ada di log.</string>
|
||||
<string name="setup_offline">Mode luring</string>
|
||||
<string name="setup_sign_in_first">Masuk ke Steam terlebih dahulu</string>
|
||||
<string name="setup_launcher">Peluncur</string>
|
||||
@@ -837,7 +857,7 @@
|
||||
<string name="gate_eyebrow">Steam</string>
|
||||
<string name="gate_lede">Android menghentikan aplikasi yang memulai banyak proses latar belakang, dan Steam memulai puluhan. Sampai ini berubah, gim akan tertutup tanpa pesan kesalahan.</string>
|
||||
<string name="gate_fix_for_me">Perbaiki untuk saya</string>
|
||||
<string name="gate_toggle_steps">Aktifkan “Disable child process restrictions”, lalu kembali. Biarkan Opsi pengembang tetap aktif, atau sakelarnya akan nonaktif lagi.</string>
|
||||
<string name="gate_toggle_steps">Aktifkan “Nonaktifkan batasan proses turunan”, lalu kembali. Biarkan Opsi pengembang tetap aktif, atau sakelarnya akan nonaktif lagi.</string>
|
||||
<string name="gate_open_dev">Buka Opsi pengembang</string>
|
||||
<string name="gate_no_toggle">Versi Android ini tidak memiliki sakelar untuk itu, sehingga DroidDeck mengubahnya melalui Debugging nirkabel. Membutuhkan waktu sekitar satu menit, tanpa komputer.</string>
|
||||
<string name="gate_needs_wifi">Debugging nirkabel memerlukan Wi-Fi. Jaringan apa pun berfungsi; tidak perlu internet.</string>
|
||||
@@ -1003,7 +1023,6 @@
|
||||
<string name="gpu_set_with_runtime">Diatur bersama driver runtime: %1$s membawa keduanya.</string>
|
||||
<string name="gpu_runtime_hint">Digunakan oleh gim dan desktop.</string>
|
||||
<string name="gpu_runtime_default">Default runtime</string>
|
||||
<string name="gpu_display_hint">Menggambar frame ke layar. Berlaku setelah DroidDeck dimulai ulang.</string>
|
||||
<string name="gpu_auto_by_gpu">Otomatis - dipilih berdasarkan GPU</string>
|
||||
<string name="gpu_manage">Unduh, impor, atau hapus…</string>
|
||||
<string name="gpu_in_use_caps">DIGUNAKAN</string>
|
||||
@@ -1229,10 +1248,16 @@
|
||||
<string name="drivers_downloading_pair">Mengunduh %1$s %2$s…</string>
|
||||
<string name="drivers_updated_to">Diperbarui ke %1$s %2$s.</string>
|
||||
<string name="drivers_updated_to_restart">Diperbarui ke %1$s %2$s. Driver tampilan berlaku setelah DroidDeck dimulai ulang.</string>
|
||||
<string name="drivers_auto_waits_for_session">Pembaruan driver menunggu sesi berhenti.</string>
|
||||
<string name="drivers_auto_waits_for_network">%1$s %2$s akan diunduh setelah keluar dari data seluler dan penghemat baterai.</string>
|
||||
<string name="drivers_skipping_bad">%1$s %2$s tidak memulai sesi di sini, jadi Otomatis tetap memakai driver yang berhasil.</string>
|
||||
<string name="drivers_rolled_back">%1$s gagal memulai sesi dua kali, jadi Otomatis kembali ke driver yang berfungsi.</string>
|
||||
<string name="drivers_switched_to">Beralih ke %1$s %2$s.</string>
|
||||
<string name="drivers_switched_to_restart">Beralih ke %1$s %2$s. Driver tampilan berlaku setelah DroidDeck dimulai ulang.</string>
|
||||
<string name="drivers_using">Menggunakan %1$s %2$s.</string>
|
||||
<string name="drivers_using_restart">Menggunakan %1$s %2$s. Driver tampilan berlaku setelah DroidDeck dimulai ulang.</string>
|
||||
<string name="drivers_using_quest">Driver tampilan bawaan Quest dan runtime bawaan sedang dipakai.</string>
|
||||
<string name="drivers_using_quest_restart">Driver tampilan bawaan Quest dan runtime bawaan sedang dipakai. Driver tampilan berlaku setelah DroidDeck dimulai ulang.</string>
|
||||
<string name="drivers_not_a_driver">Bukan zip driver: bukan driver AdrenoTools, Turnip -Linux, maupun bundle Android + Linux</string>
|
||||
<string name="drivers_import_failed">Impor gagal: %1$s</string>
|
||||
<string name="drivers_imported_unnamed">Driver diimpor</string>
|
||||
@@ -1643,7 +1668,7 @@
|
||||
<string name="adbpair_done_text">Ketuk untuk kembali ke DroidDeck. Anda bisa menonaktifkan Debugging nirkabel sekarang.</string>
|
||||
<string name="adbpair_title_failed">Penyiapan Debugging nirkabel dihentikan</string>
|
||||
|
||||
<string name="selfinst_http">Unduhan merespons HTTP %1$d</string>
|
||||
<string name="selfinst_download_failed">Unduhan gagal</string>
|
||||
<string name="selfinst_size_mismatch">Ukuran unduhan tidak sesuai dengan APK yang dipublikasikan dan dibuang - coba lagi</string>
|
||||
<string name="selfinst_checksum_mismatch">Unduhan tidak sesuai dengan checksum-nya dan dibuang - coba lagi</string>
|
||||
<string name="selfinst_wrong_package">Pembaruan ini untuk paket Android %1$s, bukan %2$s</string>
|
||||
@@ -1741,7 +1766,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">Batas kecepatan GitHub tercapai - coba lagi nanti</string>
|
||||
<string name="turnip_github_http">GitHub merespons HTTP %1$d</string>
|
||||
<string name="turnip_download_http">unduhan merespons HTTP %1$d</string>
|
||||
<string name="turnip_download_failed">Unduhan gagal</string>
|
||||
<string name="turnip_list_predates_checksums">Daftar driver ini dibuat sebelum adanya checksum - segarkan dan coba lagi</string>
|
||||
<string name="turnip_checksum_mismatch">Checksum tidak cocok - unduhan dibuang</string>
|
||||
<string name="turnip_not_adrenotools">Bukan driver AdrenoTools: %1$s</string>
|
||||
@@ -1778,7 +1803,7 @@
|
||||
<string name="phantom_instructions_fix">%1$s Jika itu tidak memungkinkan, sambungkan perangkat ini ke komputer dengan ADB dan jalankan perintah di bawah. Kembali ke sini dan periksa lagi.</string>
|
||||
<string name="phantom_instructions_off">Sesi Steam dapat dimulai.</string>
|
||||
<string name="phantom_instructions_not_applicable">Versi Android ini tidak menggunakan batas ini.</string>
|
||||
<string name="phantom_fix_toggle">Aktifkan “Disable child process restrictions” di Opsi developer, dan biarkan Opsi developer tetap aktif setelahnya.</string>
|
||||
<string name="phantom_fix_toggle">Aktifkan “Nonaktifkan batasan proses turunan” di Opsi developer, dan biarkan Opsi developer tetap aktif setelahnya.</string>
|
||||
<string name="phantom_fix_no_toggle">Versi Android ini tidak memiliki sakelar untuk itu. Gunakan Debugging nirkabel untuk mengubahnya dari perangkat ini.</string>
|
||||
<string name="upd_block_no_apk">Build ini tidak memiliki unduhan untuk salinan DroidDeck ini.</string>
|
||||
<string name="upd_block_version_code">Android tidak akan menginstalnya di atas build ini karena versionCode %1$d lebih rendah dari %2$d yang terinstal.</string>
|
||||
@@ -2010,6 +2035,9 @@
|
||||
<string name="stores_install_title">Instal %1$s:</string>
|
||||
<string name="stores_target_internal">Internal</string>
|
||||
<string name="stores_target_free">%1$s tersedia</string>
|
||||
<string name="stores_target_free_unknown">Ruang kosong tidak diketahui</string>
|
||||
<string name="stores_storage_insufficient">Ruang kosong kurang: butuh %1$s, hanya %2$s tersedia</string>
|
||||
<string name="stores_storage_low">Ruang menipis (%1$s kosong, butuh %2$s)</string>
|
||||
<string name="stores_notification_channel">Unduhan toko</string>
|
||||
<string name="stores_notification_channel_hint">Menampilkan progres unduhan gim dan menjaga unduhan tetap berjalan di latar belakang</string>
|
||||
<string name="stores_notification_one">Mengunduh</string>
|
||||
@@ -2045,6 +2073,21 @@
|
||||
<string name="edit_other_exe">Exe lain…</string>
|
||||
<string name="edit_remove">Hapus</string>
|
||||
<string name="art_cover">Sampul</string>
|
||||
<string name="art_wide_cover">Sampul lebar</string>
|
||||
<string name="art_adjust_logo">Sesuaikan logo</string>
|
||||
<string name="logo_pos_default">Bawaan</string>
|
||||
<string name="logo_pos_upper_left">Kiri atas</string>
|
||||
<string name="logo_pos_upper_center">Tengah atas</string>
|
||||
<string name="logo_pos_center">Tengah</string>
|
||||
<string name="logo_pos_bottom_left">Kiri bawah</string>
|
||||
<string name="logo_pos_bottom_center">Tengah bawah</string>
|
||||
<string name="logo_pos_position">Posisi</string>
|
||||
<string name="logo_pos_size">Ukuran</string>
|
||||
<string name="logo_pos_save">Simpan</string>
|
||||
<string name="logo_pos_reset">Kembalikan ke bawaan</string>
|
||||
<string name="logo_pos_hint">Digambar di atas latar gim, seperti yang dilakukan Steam. Posisi persisnya bisa berbeda beberapa piksel.</string>
|
||||
<string name="logo_pos_no_logo">Gim ini belum punya logo. Pilih salah satu di Logo untuk melihatnya di sini.</string>
|
||||
<string name="logo_pos_failed">Posisi logo tidak tersimpan. Coba lagi.</string>
|
||||
<string name="art_background">Latar belakang</string>
|
||||
<string name="art_logo">Logo</string>
|
||||
<string name="art_icon">Ikon</string>
|
||||
@@ -2057,6 +2100,17 @@
|
||||
<string name="art_pick_image">Pilih gambar…</string>
|
||||
<string name="art_pick_title">Pilih gambar: %1$s</string>
|
||||
<string name="art_reset">Setel ulang ke otomatis</string>
|
||||
<string name="art_search_sgdb">Cari di SteamGridDB…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB: %1$s</string>
|
||||
<string name="art_sgdb_search_label">Cari gim</string>
|
||||
<string name="art_sgdb_other_games">Hasil lain</string>
|
||||
<string name="art_sgdb_none">Tidak ada hasil.</string>
|
||||
<string name="art_sgdb_no_key">SteamGridDB memerlukan kunci API. Tambahkan milik Anda di Setelan › Peluncur.</string>
|
||||
<string name="art_sgdb_bad_key">SteamGridDB menolak kunci API. Periksa di Setelan › Peluncur.</string>
|
||||
<string name="art_sgdb_limit">SteamGridDB sedang membatasi permintaan. Coba lagi sebentar lagi.</string>
|
||||
<string name="art_sgdb_offline">SteamGridDB tidak dapat dihubungi. Periksa koneksi.</string>
|
||||
<string name="art_sgdb_server">Ada masalah di SteamGridDB. Coba lagi nanti.</string>
|
||||
<string name="art_sgdb_no_game">SteamGridDB tidak punya hasil untuk gim ini. Coba cari secara manual.</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="other">%1$d gim ditambahkan</item>
|
||||
</plurals>
|
||||
@@ -2082,6 +2136,45 @@
|
||||
<string name="gpu_bar_auto">Otomatis · %1$s</string>
|
||||
<string name="gpu_separately">Runtime dan driver tampilan secara terpisah</string>
|
||||
<string name="gpu_use_title">Gunakan %1$s?</string>
|
||||
<string name="gpu_mode_title">Cara driver dipilih</string>
|
||||
<string name="gpu_mode_auto_detail">Pasangan terbaik untuk GPU ini, selalu diperbarui</string>
|
||||
<string name="gpu_mode_pair">Pasangan tetap</string>
|
||||
<string name="gpu_mode_pair_detail">Pilih pasangan; menyetel kedua driver sekaligus</string>
|
||||
<string name="gpu_mode_mix">Campur bebas</string>
|
||||
<string name="gpu_mode_mix_detail">Pilih tampilan dan runtime secara terpisah</string>
|
||||
<string name="gpu_mode_pair_short">Pasangan</string>
|
||||
<string name="gpu_mode_mix_short">Campur</string>
|
||||
<string name="gpu_src_auto">OTOMATIS</string>
|
||||
<string name="gpu_src_pair">PASANGAN</string>
|
||||
<string name="gpu_src_bundle">BUNDEL</string>
|
||||
<string name="gpu_src_chosen">DIPILIH</string>
|
||||
<string name="gpu_src_by_gpu">MENURUT GPU</string>
|
||||
<string name="gpu_src_default">BAWAAN</string>
|
||||
<string name="gpu_display_live">Sedang menggambar</string>
|
||||
<string name="gpu_display_pending">Perlu mulai ulang · sampai itu %1$s yang menggambar</string>
|
||||
<string name="gpu_runtime_live">Aktif</string>
|
||||
<string name="gpu_runtime_pending">Sesi berikutnya · sesi ini memakai %1$s</string>
|
||||
<string name="gpu_display_next">Menggambar layar. Berlaku mulai sesi berikutnya.</string>
|
||||
<string name="gpu_previous_driver">driver sebelumnya</string>
|
||||
<string name="gpu_restart_banner">Mulai ulang untuk mengganti driver tampilan.</string>
|
||||
<string name="gpu_restart_blocked">Hentikan sesi saat ini dan mulai ulang DroidDeck untuk mengganti driver tampilan.</string>
|
||||
<string name="gpu_restart_now">Mulai ulang sekarang</string>
|
||||
<string name="gpu_restart_later">Nanti</string>
|
||||
<string name="gpu_auto_title">Otomatis memilih pasangan ini untuk %1$s Anda</string>
|
||||
<string name="gpu_auto_builtin_title">Otomatis memakai driver bawaan untuk %1$s Anda</string>
|
||||
<string name="gpu_auto_reason">Dipilih untuk keluarga %1$s. Versi baru dipasang di latar belakang.</string>
|
||||
<string name="gpu_pair_title">Pilih pasangan</string>
|
||||
<string name="gpu_pair_hint">Pasangan menyetel driver tampilan dan driver runtime bersamaan.</string>
|
||||
<string name="gpu_mix_title">Pilih setiap driver</string>
|
||||
<string name="gpu_mix_hint">Bundel mengisi keduanya. Perubahan tampilan berlaku setelah mulai ulang, perubahan runtime mulai sesi berikutnya.</string>
|
||||
<string name="gpu_change_display">Ganti driver tampilan</string>
|
||||
<string name="gpu_change_runtime">Ganti driver runtime</string>
|
||||
<string name="gpu_autos_pick">PILIHAN OTOMATIS</string>
|
||||
<string name="gpu_installed">Terpasang</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · mulai ulang untuk menerapkan</string>
|
||||
<string name="gpu_chip_restart">%1$s · perlu mulai ulang</string>
|
||||
<string name="comp_this_device">Perangkat ini</string>
|
||||
<string name="proton_remove_title">Hapus %1$s?</string>
|
||||
<string name="setup_tab_diagnostics">Diagnostik</string>
|
||||
<string name="setup_ready">Siap bermain</string>
|
||||
@@ -2093,6 +2186,14 @@
|
||||
<string name="setup_group_look">Tampilan</string>
|
||||
<string name="setup_group_device">Perangkat ini</string>
|
||||
<string name="setup_group_art">Artwork pustaka</string>
|
||||
<string name="setup_group_download">Unduhan</string>
|
||||
<string name="setup_download_source">Sumber</string>
|
||||
<string name="setup_download_source_auto">Otomatis</string>
|
||||
<string name="setup_download_source_direct">Langsung</string>
|
||||
<string name="setup_download_source_mirror">Cermin</string>
|
||||
<string name="setup_download_source_custom">Prefix sendiri</string>
|
||||
<string name="setup_download_mirror">Situs</string>
|
||||
<string name="setup_download_prefix">Prefix</string>
|
||||
<string name="flathub_eyebrow">APLIKASI LINUX</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">Aplikasi desktop untuk desktop Linux, dibuka dari Desktop.</string>
|
||||
@@ -2198,12 +2299,9 @@
|
||||
<string name="cardboard_recenter_button">Posisikan ulang</string>
|
||||
<string name="cardboard_steamvr">SteamVR (eksperimental)</string>
|
||||
<string name="cardboard_steamvr_install">Pasang</string>
|
||||
<string name="cardboard_steamvr_install_hint">Mengunduh SteamVR ARM64 dari Valve (571 MB).</string>
|
||||
<string name="cardboard_steamvr_installing">Mengunduh dan mengekstrak SteamVR…</string>
|
||||
<string name="cardboard_steamvr_start">Mulai</string>
|
||||
<string name="cardboard_steamvr_ready_hint">Menjalankan SteamVR dengan kepala Anda sebagai headset; gambar beralih ke 3D berdampingan.</string>
|
||||
<string name="cardboard_steamvr_stop">Hentikan</string>
|
||||
<string name="cardboard_steamvr_running_hint">SteamVR sedang berjalan.</string>
|
||||
<string name="cardboard_steamvr_unavailable">Tidak tersedia di sesi ini.</string>
|
||||
<string name="cardboard_steamvr_failed">SteamVR tidak merespons; periksa log sesi.</string>
|
||||
<string name="drawer_component_downloads">Unduh komponen</string>
|
||||
@@ -2217,4 +2315,195 @@
|
||||
<string name="stores_hltb_completionist">Tuntas</string>
|
||||
<string name="stores_hltb_hours">%1$s j</string>
|
||||
<string name="stores_hltb_minutes">%1$d mnt</string>
|
||||
<string name="display_picture_position">Posisi gambar</string>
|
||||
<string name="display_position_top">Atas</string>
|
||||
<string name="display_position_bottom">Bawah</string>
|
||||
<string name="display_position_center">Tengah (bawaan)</string>
|
||||
<string name="setup_tab_storage">Penyimpanan</string>
|
||||
<string name="storage_measuring">Mengukur penyimpanan…</string>
|
||||
<string name="storage_measuring_hint">Butuh beberapa detik pada kali pertama.</string>
|
||||
<string name="storage_internal">Penyimpanan internal</string>
|
||||
<string name="storage_used_of">%1$s dari %2$s terpakai</string>
|
||||
<string name="storage_free">%1$s kosong</string>
|
||||
<string name="storage_droiddeck_uses">DroidDeck memakai %1$s. Diukur pada %2$s.</string>
|
||||
<string name="storage_rescanning">memindai ulang…</string>
|
||||
<string name="storage_other_apps">Aplikasi Android lain</string>
|
||||
<string name="storage_safe_now">Bisa dikosongkan sekarang</string>
|
||||
<string name="storage_safe_hint">Runtime yang tidak dipakai, sisa, log lama</string>
|
||||
<string name="storage_clean_up">Membersihkan…</string>
|
||||
<string name="storage_session_running">Hentikan sesi untuk membersihkan atau menghapus apa pun. Ukuran tetap terlihat.</string>
|
||||
<string name="storage_where">Ke mana perginya</string>
|
||||
<string name="storage_games">Gim Steam</string>
|
||||
<string name="storage_game_data">Data gim</string>
|
||||
<string name="storage_game_data_hint">Prefix %1$s · workshop %2$s · shader %3$s</string>
|
||||
<string name="storage_tools">Proton dan runtime</string>
|
||||
<string name="storage_tools_unused">%1$s tidak dipakai</string>
|
||||
<string name="storage_stores">GOG, Epic, dan toko lain</string>
|
||||
<string name="storage_stores_hint">Hapus di Toko</string>
|
||||
<string name="storage_desktop">Aplikasi desktop</string>
|
||||
<string name="storage_desktop_hint">Aplikasi dan runtime Flatpak</string>
|
||||
<string name="storage_system">Sistem Linux dan klien Steam</string>
|
||||
<string name="storage_system_hint">Diperlukan agar apa pun bisa berjalan</string>
|
||||
<string name="storage_app_data">Data DroidDeck</string>
|
||||
<string name="storage_app_data_hint">Log sesi lama %1$s · cache · driver</string>
|
||||
<string name="storage_legend_games">Gim</string>
|
||||
<string name="storage_legend_data">Data gim</string>
|
||||
<string name="storage_legend_tools">Proton dan runtime</string>
|
||||
<string name="storage_legend_stores">Toko lain</string>
|
||||
<string name="storage_legend_desktop">Aplikasi desktop</string>
|
||||
<string name="storage_legend_system">Sistem Linux</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">Bersihkan</string>
|
||||
<string name="storage_clean_up_lede">Tidak ada di sini yang menyentuh gim terpasang atau unduhan yang berjalan.</string>
|
||||
<string name="storage_nothing_to_clean">Tidak ada yang perlu dibersihkan sekarang.</string>
|
||||
<string name="storage_selected">Dipilih: %1$s · %2$s → %3$s akan kosong</string>
|
||||
<string name="storage_cleaning">Membersihkan…</string>
|
||||
<string name="storage_free_button">Kosongkan %1$s</string>
|
||||
<string name="storage_freed">%1$s dikosongkan.</string>
|
||||
<string name="storage_item_runtimes">Runtime Steam dan Proton yang tidak dipakai</string>
|
||||
<string name="storage_item_leftovers">Sisa gim yang dihapus</string>
|
||||
<string name="storage_item_leftovers_unsure">Sisa yang mungkin berisi save</string>
|
||||
<string name="storage_item_logs">Log sesi lama</string>
|
||||
<string name="storage_item_logs_detail">Log sesi saat ini tetap disimpan</string>
|
||||
<string name="storage_item_custom_detail">Tidak ada gim yang memakainya. Pasang ulang di Komponen jika perlu.</string>
|
||||
<string name="storage_item_shaders">Cache shader gim terpasang</string>
|
||||
<string name="storage_item_shaders_detail">Dibangun ulang saat bermain. Beberapa menit pertama tiap gim akan tersendat lagi.</string>
|
||||
<string name="storage_tag_safe">Aman</string>
|
||||
<string name="storage_tag_unused">Tidak dipakai</string>
|
||||
<string name="storage_tag_stutter">Tersendat</string>
|
||||
<string name="storage_tag_saves">Periksa save</string>
|
||||
<string name="storage_tag_default">Bawaan</string>
|
||||
<string name="storage_tag_required">Diperlukan</string>
|
||||
<string name="storage_tag_not_used">Tidak dipakai</string>
|
||||
<string name="storage_back">Penyimpanan</string>
|
||||
<string name="storage_games_with_data">%1$s beserta datanya</string>
|
||||
<string name="storage_sort_largest">Terbesar</string>
|
||||
<string name="storage_sort_least_played">Paling jarang dimainkan</string>
|
||||
<string name="storage_sort_sd">Di kartu SD</string>
|
||||
<string name="storage_no_games">Tidak ada gim di sini.</string>
|
||||
<string name="storage_played">dimainkan %1$s</string>
|
||||
<string name="storage_never_played">Belum pernah dimainkan</string>
|
||||
<string name="storage_game_parts">Gim %1$s · data %2$s</string>
|
||||
<string name="storage_uninstall">Hapus instalasi</string>
|
||||
<string name="storage_games_note">Menghapus instalasi membuka jendela Steam agar Steam mencatatnya. Untuk memindahkan gim ke kartu SD, pakai Steam › Setelan › Penyimpanan.</string>
|
||||
<string name="storage_uninstall_title">Hapus instalasi %1$s?</string>
|
||||
<string name="storage_uninstall_body">Steam akan terbuka dan meminta konfirmasi. Ini mengosongkan %1$s gim. Prefix tetap ada sampai sisa dibersihkan, dan save Steam Cloud tetap di akun Anda.</string>
|
||||
<string name="storage_open_steam">Buka Steam</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s tidak dipakai</string>
|
||||
<string name="storage_remove_unused">Hapus yang tidak dipakai · %1$s</string>
|
||||
<string name="storage_in_use">Sedang dipakai</string>
|
||||
<string name="storage_not_used">Tidak dipakai</string>
|
||||
<string name="storage_remove">Hapus</string>
|
||||
<string name="storage_remove_title">Hapus %1$s?</string>
|
||||
<string name="storage_remove_body">Mengosongkan %1$s. Steam akan mengunduhnya lagi jika ada gim yang membutuhkan.</string>
|
||||
<string name="storage_tool_fex_runtime">Diperlukan untuk gim Linux x86 (FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">Diperlukan untuk Proton ARM64</string>
|
||||
<string name="storage_tool_redist">Instalan VC++ dan DirectX yang dibutuhkan gim</string>
|
||||
<string name="storage_tool_fex">Menjalankan gim Linux x86</string>
|
||||
<string name="storage_tool_steam">Alat Steam</string>
|
||||
<string name="storage_tool_old_runtime">DroidDeck menjalankan gim tanpa runtime ini</string>
|
||||
<string name="storage_tool_x86_proton">Proton x86, yang tidak bisa dijalankan DroidDeck</string>
|
||||
<string name="storage_tool_custom_default">Bawaan untuk semua gim</string>
|
||||
<string name="storage_tool_custom_unused">Tidak ada gim yang memakainya</string>
|
||||
<string name="storage_tool_custom_unknown">Setelan Steam tidak terbaca, jadi ini disimpan</string>
|
||||
<string name="storage_cleanup_busy">Tunggu pembersihan penyimpanan selesai</string>
|
||||
<string name="storage_low_title">Hanya %1$s kosong</string>
|
||||
<string name="storage_low_body">Steam butuh ruang untuk pembaruan dan cache shader. Di bawah 10 GB, pembaruan gim bisa gagal di tengah.</string>
|
||||
<string name="storage_low_safe">%1$s bisa dikosongkan dengan aman.</string>
|
||||
<string name="storage_free_up">Kosongkan ruang</string>
|
||||
<string name="storage_play_anyway">Tetap main</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d terpasang</item>
|
||||
<item quantity="other">%d terpasang</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d di kartu SD</item>
|
||||
<item quantity="other">%d di kartu SD</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">%d alat</item>
|
||||
<item quantity="other">%d alat</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">Folder prefix dan cache shader kosong dari %d gim</item>
|
||||
<item quantity="other">Folder prefix dan cache shader kosong dari %d gim</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">Prefix %d gim masih berisi berkas, termasuk save lokal. Salin dulu jika diperlukan.</item>
|
||||
<item quantity="other">Prefix %d gim masih berisi berkas, termasuk save lokal. Salin dulu jika diperlukan.</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">%d gim</item>
|
||||
<item quantity="other">%d gim</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">Proton bawaan DroidDeck</string>
|
||||
<string name="storage_tool_proton">Opsi Proton 11 DroidDeck</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">dipakai %d gim</item>
|
||||
<item quantity="other">dipakai %d gim</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">Komponen</string>
|
||||
<string name="storage_components">Komponen</string>
|
||||
<string name="storage_item_components">Komponen yang tidak dipakai</string>
|
||||
<string name="storage_component_original">Asli %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">Mengosongkan %1$s. Hanya mengembalikan versi Proton yang tidak terpasang lagi.</string>
|
||||
<string name="storage_leftovers_unchecked">Belum diperiksa: manifes aplikasi Steam tidak terbaca, jadi prefix gim yang dihapus tetap ada sampai itu memungkinkan.</string>
|
||||
<string name="storage_failed">Gagal: %1$s</string>
|
||||
<string name="storage_desktop_shared">%1$s dibagi pakai</string>
|
||||
<string name="storage_data_installed">Gim terpasang</string>
|
||||
<string name="storage_data_parts">Prefix %1$s · shader %2$s · workshop %3$s</string>
|
||||
<string name="storage_clear_shaders">Bersihkan shader</string>
|
||||
<string name="storage_data_leftovers">Gim yang dihapus</string>
|
||||
<string name="storage_steam_app">Aplikasi Steam %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">Prefix masih berisi berkas, mungkin save lokal</string>
|
||||
<string name="storage_leftover_confirm">Menghapus prefix Wine dan cache shader (%1$s). Salin dulu save yang penting; save Steam Cloud tetap di akun Anda.</string>
|
||||
<string name="storage_data_other">Disimpan</string>
|
||||
<string name="storage_data_other_title">Gim non-Steam dan prefix Steam bersama</string>
|
||||
<string name="storage_data_other_detail">Prefix gim dari toko atau yang ditambahkan sendiri</string>
|
||||
<string name="storage_components_packages">Paket yang diunduh</string>
|
||||
<string name="storage_components_originals">Berkas asli yang disimpan</string>
|
||||
<string name="storage_components_none">Tidak ada paket komponen yang diunduh.</string>
|
||||
<string name="storage_component_in_use">Dipilih untuk Proton atau gim</string>
|
||||
<string name="storage_component_unused">Tidak dipilih di mana pun</string>
|
||||
<string name="storage_original_kept">Mengembalikan Proton yang terpasang</string>
|
||||
<string name="storage_original_old">Versi Proton yang tidak terpasang lagi</string>
|
||||
<string name="storage_tag_in_use">Dipakai</string>
|
||||
<string name="storage_components_note">Komponen yang dipakai tetap terkunci. Pilih di Komponen; yang dihapus di sini bisa diunduh lagi di sana.</string>
|
||||
<string name="storage_component_confirm">Mengosongkan %1$s. Paket yang Anda impor sendiri tidak bisa diunduh ulang.</string>
|
||||
<string name="storage_desktop_detail">%1$s untuk aplikasi Flatpak dan yang dibagi pakai</string>
|
||||
<string name="storage_desktop_apps_title">Aplikasi</string>
|
||||
<string name="storage_desktop_none">Tidak ada aplikasi Flatpak terpasang.</string>
|
||||
<string name="storage_desktop_shared_title">Dibagi pakai antar aplikasi</string>
|
||||
<string name="storage_desktop_runtimes">Runtime</string>
|
||||
<string name="storage_desktop_runtimes_detail">Pustaka tempat aplikasi berjalan; yang tidak dipakai hilang lewat Hapus yang tidak dipakai</string>
|
||||
<string name="storage_desktop_cache">Penyimpanan unduhan</string>
|
||||
<string name="storage_desktop_cache_detail">Object store dan cache unduhan Flatpak</string>
|
||||
<string name="storage_desktop_prune">Hapus yang tidak dipakai</string>
|
||||
<string name="storage_desktop_prune_body">Menghapus runtime yang tidak dibutuhkan aplikasi terpasang mana pun dan merapikan object store Flatpak. Aplikasi terpasang tetap berjalan.</string>
|
||||
<string name="storage_flatpak_uninstall_body">Menghapus aplikasi (%1$s) dan runtime yang tidak dipakai yang lain. Data di folder home Anda tetap ada.</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">%d paket</item>
|
||||
<item quantity="other">%d paket</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">%d aplikasi</item>
|
||||
<item quantity="other">%d aplikasi</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">Video boot</string>
|
||||
<string name="boot_movie_video">Video</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d fps</string>
|
||||
<string name="boot_movie_note">60 fps terlihat lebih mulus tetapi memakai lebih banyak CPU saat Steam dimulai. Video startup yang dipilih di pengaturan Steam sendiri lebih diutamakan.</string>
|
||||
<string name="boot_movie_sound">Suara</string>
|
||||
<string name="boot_movie_custom">Video sendiri</string>
|
||||
<string name="boot_movie_custom_hint">MP4, MOV, MKV, atau WebM. Format lain dan file di atas %1$d MB dikonversi ke WebM 720p di perangkat, hingga %2$d detik.</string>
|
||||
<string name="boot_movie_import">Impor</string>
|
||||
<string name="boot_movie_remove">Hapus</string>
|
||||
<string name="boot_movie_pick">Pilih video boot</string>
|
||||
<string name="boot_movie_imported">%1$s sekarang menjadi video boot</string>
|
||||
<string name="boot_movie_imported_silent">%1$s sekarang menjadi video boot, tanpa suara</string>
|
||||
<string name="boot_movie_too_long">Video yang perlu dikonversi maksimal %1$d detik</string>
|
||||
<string name="boot_movie_converting">Mengonversi %1$d%%</string>
|
||||
<string name="boot_movie_converting_started">Mengonversi %1$s untuk Steam. Biarkan DroidDeck tetap terbuka.</string>
|
||||
<string name="boot_movie_unsupported">Perangkat ini tidak dapat mendekode video itu.</string>
|
||||
<string name="boot_movie_import_failed">Tidak dapat mengimpor video itu</string>
|
||||
</resources>
|
||||
@@ -577,6 +577,10 @@
|
||||
<string name="drawer_cas">Nitidezza (CAS)</string>
|
||||
<string name="drawer_cas_level">Livello di nitidezza</string>
|
||||
<string name="drawer_fake_hdr">HDR simulato</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">Avvia SteamVR</string>
|
||||
<string name="steamvr_checking">Controllo di SteamVR…</string>
|
||||
<string name="drawer_deband">Riduzione del banding</string>
|
||||
<string name="drawer_deband_strength">Intensità riduzione banding</string>
|
||||
<string name="drawer_brightness">Luminosità</string>
|
||||
@@ -594,6 +598,11 @@
|
||||
<string name="drawer_texture_sharpness_note">Un mip bias negativo: texture più nitide, un po\' di sfarfallio. Auto si adatta a quanto la sessione viene ingrandita sullo schermo.</string>
|
||||
<string name="drawer_touch_auto">Auto (%1$s)</string>
|
||||
<string name="drawer_keyboard">Tastiera</string>
|
||||
<string name="drawer_latency">Latenza</string>
|
||||
<string name="drawer_low_latency_controller">Controller a bassa latenza</string>
|
||||
<string name="drawer_low_latency_controller_hint">Il controller invia dati a 1000 Hz invece di 250 Hz. Un po\' più di CPU.</string>
|
||||
<string name="drawer_low_latency_touch">Touch a bassa latenza</string>
|
||||
<string name="drawer_low_latency_touch_hint">I tocchi saltano il raggruppamento dei frame; i controlli touch di Steam passano a 1000 Hz. Un po\' più di CPU.</string>
|
||||
<string name="drawer_pc_keyboard">Tastiera PC</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">Secondo schermo</string>
|
||||
@@ -849,7 +858,7 @@
|
||||
<string name="gate_eyebrow">Steam</string>
|
||||
<string name="gate_lede">Android arresta le app che avviano molti processi in background, e Steam ne avvia decine. Finché questo non cambia, i giochi si chiudono senza un errore.</string>
|
||||
<string name="gate_fix_for_me">Risolvilo per me</string>
|
||||
<string name="gate_toggle_steps">Attiva “Disattiva limitazioni dei processi figlio”, poi torna qui. Lascia attive le Opzioni sviluppatore, altrimenti l\'interruttore si disattiva di nuovo.</string>
|
||||
<string name="gate_toggle_steps">Attiva “Disattiva limitazioni per i processi secondari”, poi torna qui. Lascia attive le Opzioni sviluppatore, altrimenti l\'interruttore si disattiva di nuovo.</string>
|
||||
<string name="gate_open_dev">Apri Opzioni sviluppatore</string>
|
||||
<string name="gate_no_toggle">Questa versione di Android non ha un interruttore per questo, quindi DroidDeck lo cambia tramite il Debug wireless. Richiede circa un minuto, senza bisogno di un computer.</string>
|
||||
<string name="gate_needs_wifi">Il Debug wireless richiede il Wi-Fi. Va bene qualsiasi rete; non serve l\'accesso a internet.</string>
|
||||
@@ -1015,7 +1024,6 @@
|
||||
<string name="gpu_set_with_runtime">Impostato insieme al driver del runtime: %1$s include entrambi.</string>
|
||||
<string name="gpu_runtime_hint">Usato dai giochi e dal desktop.</string>
|
||||
<string name="gpu_runtime_default">Runtime predefinito</string>
|
||||
<string name="gpu_display_hint">Disegna i fotogrammi sullo schermo. Si applica dopo il riavvio di DroidDeck.</string>
|
||||
<string name="gpu_auto_by_gpu">Auto - scelto in base alla GPU</string>
|
||||
<string name="gpu_manage">Scarica, importa o elimina…</string>
|
||||
<string name="gpu_in_use_caps">IN USO</string>
|
||||
@@ -1249,10 +1257,16 @@
|
||||
<string name="drivers_downloading_pair">Download di %1$s %2$s…</string>
|
||||
<string name="drivers_updated_to">Aggiornato a %1$s %2$s.</string>
|
||||
<string name="drivers_updated_to_restart">Aggiornato a %1$s %2$s. Il driver video si applica dopo il riavvio di DroidDeck.</string>
|
||||
<string name="drivers_auto_waits_for_session">Gli aggiornamenti dei driver aspettano la fine della sessione.</string>
|
||||
<string name="drivers_auto_waits_for_network">%1$s %2$s verrà scaricato quando disattivi i dati mobili e il risparmio energetico.</string>
|
||||
<string name="drivers_skipping_bad">%1$s %2$s non ha avviato una sessione qui, quindi Auto resta sui driver che hanno funzionato.</string>
|
||||
<string name="drivers_rolled_back">%1$s non ha avviato una sessione per due volte, quindi Auto è tornato ai driver che funzionavano.</string>
|
||||
<string name="drivers_switched_to">Passato a %1$s %2$s.</string>
|
||||
<string name="drivers_switched_to_restart">Passato a %1$s %2$s. Il driver video si applica dopo il riavvio di DroidDeck.</string>
|
||||
<string name="drivers_using">In uso: %1$s %2$s.</string>
|
||||
<string name="drivers_using_restart">In uso: %1$s %2$s. Il driver video si applica dopo il riavvio di DroidDeck.</string>
|
||||
<string name="drivers_using_quest">In uso il driver video Quest incluso e il valore predefinito del runtime.</string>
|
||||
<string name="drivers_using_quest_restart">In uso il driver video Quest incluso e il valore predefinito del runtime. Il driver video si applica dopo il riavvio di DroidDeck.</string>
|
||||
<string name="drivers_not_a_driver">Non è uno zip di driver: non è un driver AdrenoTools, un Turnip -Linux né un pacchetto Android + Linux</string>
|
||||
<string name="drivers_import_failed">Importazione non riuscita: %1$s</string>
|
||||
<string name="drivers_imported_unnamed">Driver importato</string>
|
||||
@@ -1685,7 +1699,7 @@
|
||||
<string name="adbpair_done_text">Tocca per tornare a DroidDeck. Ora puoi disattivare il debug wireless.</string>
|
||||
<string name="adbpair_title_failed">Configurazione del debug wireless interrotta</string>
|
||||
|
||||
<string name="selfinst_http">Il download ha risposto HTTP %1$d</string>
|
||||
<string name="selfinst_download_failed">Download non riuscito</string>
|
||||
<string name="selfinst_size_mismatch">La dimensione del download non corrispondeva all\'APK pubblicato ed è stato scartato - riprova</string>
|
||||
<string name="selfinst_checksum_mismatch">Il download non corrispondeva al suo checksum ed è stato scartato - riprova</string>
|
||||
<string name="selfinst_wrong_package">L\'aggiornamento è per il pacchetto Android %1$s, non %2$s</string>
|
||||
@@ -1783,7 +1797,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">Raggiunto il limite di richieste di GitHub - riprova più tardi</string>
|
||||
<string name="turnip_github_http">GitHub ha risposto HTTP %1$d</string>
|
||||
<string name="turnip_download_http">il download ha risposto HTTP %1$d</string>
|
||||
<string name="turnip_download_failed">Download non riuscito</string>
|
||||
<string name="turnip_list_predates_checksums">Questo elenco di driver è anteriore ai checksum - aggiornalo e riprova</string>
|
||||
<string name="turnip_checksum_mismatch">Checksum non corrispondente - il download è stato scartato</string>
|
||||
<string name="turnip_not_adrenotools">Non è un driver AdrenoTools: %1$s</string>
|
||||
@@ -1820,7 +1834,7 @@
|
||||
<string name="phantom_instructions_fix">%1$s Se non è possibile, collega questo dispositivo a un computer con ADB ed esegui il comando qui sotto. Poi torna qui e ricontrolla.</string>
|
||||
<string name="phantom_instructions_off">Le sessioni Steam possono avviarsi.</string>
|
||||
<string name="phantom_instructions_not_applicable">Questa versione di Android non usa questo limite.</string>
|
||||
<string name="phantom_fix_toggle">Attiva “Disattiva limitazioni processi secondari” in Opzioni sviluppatore e poi lascia attive le Opzioni sviluppatore.</string>
|
||||
<string name="phantom_fix_toggle">Attiva “Disattiva limitazioni per i processi secondari” in Opzioni sviluppatore e poi lascia attive le Opzioni sviluppatore.</string>
|
||||
<string name="phantom_fix_no_toggle">Questa versione di Android non ha un interruttore per farlo. Usa il debug wireless per cambiarlo da questo dispositivo.</string>
|
||||
<string name="upd_block_no_apk">Questa build non ha un download per questa copia di DroidDeck.</string>
|
||||
<string name="upd_block_version_code">Android non la installerà sopra questa build perché il suo versionCode %1$d è inferiore al %2$d installato.</string>
|
||||
@@ -2087,6 +2101,21 @@
|
||||
<string name="edit_other_exe">Altro exe…</string>
|
||||
<string name="edit_remove">Rimuovi</string>
|
||||
<string name="art_cover">Copertina</string>
|
||||
<string name="art_wide_cover">Copertina larga</string>
|
||||
<string name="art_adjust_logo">Regola logo</string>
|
||||
<string name="logo_pos_default">Predefinita</string>
|
||||
<string name="logo_pos_upper_left">In alto a sinistra</string>
|
||||
<string name="logo_pos_upper_center">In alto al centro</string>
|
||||
<string name="logo_pos_center">Centro</string>
|
||||
<string name="logo_pos_bottom_left">In basso a sinistra</string>
|
||||
<string name="logo_pos_bottom_center">In basso al centro</string>
|
||||
<string name="logo_pos_position">Posizione</string>
|
||||
<string name="logo_pos_size">Dimensione</string>
|
||||
<string name="logo_pos_save">Salva</string>
|
||||
<string name="logo_pos_reset">Ripristina predefinita</string>
|
||||
<string name="logo_pos_hint">Viene disegnato sullo sfondo del gioco, come fa Steam. La posizione esatta può variare di qualche pixel.</string>
|
||||
<string name="logo_pos_no_logo">Questo gioco non ha ancora un logo. Scegline uno in Logo per vederlo qui.</string>
|
||||
<string name="logo_pos_failed">Impossibile salvare la posizione del logo. Riprova.</string>
|
||||
<string name="art_background">Sfondo</string>
|
||||
<string name="art_logo">Logo</string>
|
||||
<string name="art_icon">Icona</string>
|
||||
@@ -2099,6 +2128,17 @@
|
||||
<string name="art_pick_image">Scegli immagine…</string>
|
||||
<string name="art_pick_title">Scegli un\'immagine: %1$s</string>
|
||||
<string name="art_reset">Ripristina automatico</string>
|
||||
<string name="art_search_sgdb">Cerca su SteamGridDB…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB: %1$s</string>
|
||||
<string name="art_sgdb_search_label">Cerca un gioco</string>
|
||||
<string name="art_sgdb_other_games">Altri risultati</string>
|
||||
<string name="art_sgdb_none">Nessun risultato.</string>
|
||||
<string name="art_sgdb_no_key">SteamGridDB richiede una chiave API. Aggiungi la tua in Configurazione › Launcher.</string>
|
||||
<string name="art_sgdb_bad_key">SteamGridDB non ha accettato la chiave API. Controllala in Configurazione › Launcher.</string>
|
||||
<string name="art_sgdb_limit">SteamGridDB sta limitando le richieste in questo momento. Riprova tra un minuto.</string>
|
||||
<string name="art_sgdb_offline">Impossibile raggiungere SteamGridDB. Controlla la connessione.</string>
|
||||
<string name="art_sgdb_server">SteamGridDB ha avuto un problema. Riprova più tardi.</string>
|
||||
<string name="art_sgdb_no_game">SteamGridDB non ha risultati per questo gioco. Prova a cercarlo.</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="one">%1$d gioco aggiunto</item>
|
||||
<item quantity="many">%1$d giochi aggiunti</item>
|
||||
@@ -2128,6 +2168,45 @@
|
||||
<string name="gpu_bar_auto">Auto · %1$s</string>
|
||||
<string name="gpu_separately">Driver del runtime e video separati</string>
|
||||
<string name="gpu_use_title">Usare %1$s?</string>
|
||||
<string name="gpu_mode_title">Come vengono scelti i driver</string>
|
||||
<string name="gpu_mode_auto_detail">La coppia migliore per questa GPU, sempre aggiornata</string>
|
||||
<string name="gpu_mode_pair">Coppia abbinata</string>
|
||||
<string name="gpu_mode_pair_detail">Scegli una coppia; imposta entrambi i driver</string>
|
||||
<string name="gpu_mode_mix">Abbina liberamente</string>
|
||||
<string name="gpu_mode_mix_detail">Scegli video e runtime separatamente</string>
|
||||
<string name="gpu_mode_pair_short">Coppia</string>
|
||||
<string name="gpu_mode_mix_short">Misto</string>
|
||||
<string name="gpu_src_auto">AUTO</string>
|
||||
<string name="gpu_src_pair">COPPIA</string>
|
||||
<string name="gpu_src_bundle">PACCHETTO</string>
|
||||
<string name="gpu_src_chosen">SCELTO</string>
|
||||
<string name="gpu_src_by_gpu">PER GPU</string>
|
||||
<string name="gpu_src_default">PREDEFINITO</string>
|
||||
<string name="gpu_display_live">Disegna ora</string>
|
||||
<string name="gpu_display_pending">Riavvio in attesa · fino ad allora disegna %1$s</string>
|
||||
<string name="gpu_runtime_live">In esecuzione</string>
|
||||
<string name="gpu_runtime_pending">Prossima sessione · questa mantiene %1$s</string>
|
||||
<string name="gpu_display_next">Disegna lo schermo. Si applica dalla prossima sessione.</string>
|
||||
<string name="gpu_previous_driver">il driver precedente</string>
|
||||
<string name="gpu_restart_banner">Riavvia per cambiare il driver video.</string>
|
||||
<string name="gpu_restart_blocked">Interrompi la sessione in corso, poi riavvia DroidDeck per cambiare il driver video.</string>
|
||||
<string name="gpu_restart_now">Riavvia ora</string>
|
||||
<string name="gpu_restart_later">Più tardi</string>
|
||||
<string name="gpu_auto_title">Auto ha scelto questa coppia per la tua %1$s</string>
|
||||
<string name="gpu_auto_builtin_title">Auto usa i driver integrati per la tua %1$s</string>
|
||||
<string name="gpu_auto_reason">Scelta per la famiglia %1$s. Le nuove versioni si installano in background.</string>
|
||||
<string name="gpu_pair_title">Scegli una coppia</string>
|
||||
<string name="gpu_pair_hint">Una coppia imposta insieme il driver video e quello del runtime.</string>
|
||||
<string name="gpu_mix_title">Scegli ogni driver</string>
|
||||
<string name="gpu_mix_hint">Un pacchetto riempie entrambi gli slot. Le modifiche video si applicano dopo un riavvio, quelle del runtime dalla prossima sessione.</string>
|
||||
<string name="gpu_change_display">Cambia driver video</string>
|
||||
<string name="gpu_change_runtime">Cambia driver del runtime</string>
|
||||
<string name="gpu_autos_pick">SCELTA DI AUTO</string>
|
||||
<string name="gpu_installed">Installato</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · riavvia per applicare</string>
|
||||
<string name="gpu_chip_restart">%1$s · riavvio in attesa</string>
|
||||
<string name="comp_this_device">Questo dispositivo</string>
|
||||
<string name="proton_remove_title">Rimuovere %1$s?</string>
|
||||
<string name="setup_tab_diagnostics">Diagnostica</string>
|
||||
<string name="setup_ready">Pronto per giocare</string>
|
||||
@@ -2139,6 +2218,14 @@
|
||||
<string name="setup_group_look">Aspetto</string>
|
||||
<string name="setup_group_device">Questo dispositivo</string>
|
||||
<string name="setup_group_art">Illustrazioni della libreria</string>
|
||||
<string name="setup_group_download">Download</string>
|
||||
<string name="setup_download_source">Origine</string>
|
||||
<string name="setup_download_source_auto">Automatica</string>
|
||||
<string name="setup_download_source_direct">Diretta</string>
|
||||
<string name="setup_download_source_mirror">Mirror</string>
|
||||
<string name="setup_download_source_custom">Prefisso personalizzato</string>
|
||||
<string name="setup_download_mirror">Sito</string>
|
||||
<string name="setup_download_prefix">Prefisso</string>
|
||||
<string name="flathub_eyebrow">APP LINUX</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">App per il desktop Linux, da aprire in Desktop.</string>
|
||||
@@ -2248,12 +2335,9 @@
|
||||
<string name="cardboard_recenter_button">Ricentra</string>
|
||||
<string name="cardboard_steamvr">SteamVR (sperimentale)</string>
|
||||
<string name="cardboard_steamvr_install">Installa</string>
|
||||
<string name="cardboard_steamvr_install_hint">Scarica SteamVR ARM64 di Valve (571 MB).</string>
|
||||
<string name="cardboard_steamvr_installing">Download ed estrazione di SteamVR…</string>
|
||||
<string name="cardboard_steamvr_start">Avvia</string>
|
||||
<string name="cardboard_steamvr_ready_hint">Avvia SteamVR usando la tua testa come visore; l\'immagine passa al 3D affiancato.</string>
|
||||
<string name="cardboard_steamvr_stop">Arresta</string>
|
||||
<string name="cardboard_steamvr_running_hint">SteamVR è in esecuzione.</string>
|
||||
<string name="cardboard_steamvr_unavailable">Non disponibile in questa sessione.</string>
|
||||
<string name="cardboard_steamvr_failed">SteamVR non ha risposto; controlla il log della sessione.</string>
|
||||
<string name="drawer_component_downloads">Scarica componenti</string>
|
||||
@@ -2281,4 +2365,195 @@
|
||||
<string name="stores_storage_insufficient">Spazio libero insufficiente: servono %1$s, ne sono liberi solo %2$s</string>
|
||||
<string name="stores_storage_low">Spazio insufficiente (%1$s liberi, ne servono %2$s)</string>
|
||||
<string name="stores_target_free_unknown">Spazio libero sconosciuto</string>
|
||||
<string name="display_picture_position">Posizione immagine</string>
|
||||
<string name="display_position_top">In alto</string>
|
||||
<string name="display_position_bottom">In basso</string>
|
||||
<string name="display_position_center">Centro (predefinito)</string>
|
||||
<string name="setup_tab_storage">Archiviazione</string>
|
||||
<string name="storage_measuring">Misurazione dell\'archiviazione…</string>
|
||||
<string name="storage_measuring_hint">La prima volta richiede qualche secondo.</string>
|
||||
<string name="storage_internal">Archiviazione interna</string>
|
||||
<string name="storage_used_of">%1$s di %2$s usati</string>
|
||||
<string name="storage_free">%1$s liberi</string>
|
||||
<string name="storage_droiddeck_uses">DroidDeck occupa %1$s. Misurato il %2$s.</string>
|
||||
<string name="storage_rescanning">nuova misurazione…</string>
|
||||
<string name="storage_other_apps">Altre app Android</string>
|
||||
<string name="storage_safe_now">Liberabile ora</string>
|
||||
<string name="storage_safe_hint">Runtime inutilizzati, residui, vecchi log</string>
|
||||
<string name="storage_clean_up">Pulisci…</string>
|
||||
<string name="storage_session_running">Interrompi la sessione per pulire o rimuovere qualcosa. Le dimensioni restano visibili.</string>
|
||||
<string name="storage_where">Dove finisce</string>
|
||||
<string name="storage_games">Giochi Steam</string>
|
||||
<string name="storage_game_data">Dati dei giochi</string>
|
||||
<string name="storage_game_data_hint">Prefissi %1$s · workshop %2$s · shader %3$s</string>
|
||||
<string name="storage_tools">Proton e runtime</string>
|
||||
<string name="storage_tools_unused">%1$s inutilizzati</string>
|
||||
<string name="storage_stores">GOG, Epic e altri store</string>
|
||||
<string name="storage_stores_hint">Disinstallali da Store</string>
|
||||
<string name="storage_desktop">App desktop</string>
|
||||
<string name="storage_desktop_hint">App e runtime Flatpak</string>
|
||||
<string name="storage_system">Sistema Linux e client Steam</string>
|
||||
<string name="storage_system_hint">Necessario per eseguire qualsiasi cosa</string>
|
||||
<string name="storage_app_data">Dati di DroidDeck</string>
|
||||
<string name="storage_app_data_hint">Log delle vecchie sessioni %1$s · cache · driver</string>
|
||||
<string name="storage_legend_games">Giochi</string>
|
||||
<string name="storage_legend_data">Dati dei giochi</string>
|
||||
<string name="storage_legend_tools">Proton e runtime</string>
|
||||
<string name="storage_legend_stores">Altri store</string>
|
||||
<string name="storage_legend_desktop">App desktop</string>
|
||||
<string name="storage_legend_system">Sistema Linux</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">Pulizia</string>
|
||||
<string name="storage_clean_up_lede">Qui nulla tocca i giochi installati o i download in corso.</string>
|
||||
<string name="storage_nothing_to_clean">Al momento non c\'è nulla da pulire.</string>
|
||||
<string name="storage_selected">Selezionato: %1$s · %2$s → %3$s liberi</string>
|
||||
<string name="storage_cleaning">Pulizia…</string>
|
||||
<string name="storage_free_button">Libera %1$s</string>
|
||||
<string name="storage_freed">Liberati %1$s.</string>
|
||||
<string name="storage_item_runtimes">Runtime e Proton di Steam inutilizzati</string>
|
||||
<string name="storage_item_leftovers">Residui di giochi disinstallati</string>
|
||||
<string name="storage_item_leftovers_unsure">Residui che possono contenere salvataggi</string>
|
||||
<string name="storage_item_logs">Log delle vecchie sessioni</string>
|
||||
<string name="storage_item_logs_detail">Conserva i log della sessione attuale</string>
|
||||
<string name="storage_item_custom_detail">Nessun gioco lo usa. Reinstallalo da Componenti se ti serve.</string>
|
||||
<string name="storage_item_shaders">Cache shader dei giochi installati</string>
|
||||
<string name="storage_item_shaders_detail">Vengono ricreate giocando. I primi minuti di ogni gioco tornano a scattare.</string>
|
||||
<string name="storage_tag_safe">Sicuro</string>
|
||||
<string name="storage_tag_unused">Inutilizzato</string>
|
||||
<string name="storage_tag_stutter">Scatti</string>
|
||||
<string name="storage_tag_saves">Controlla salvataggi</string>
|
||||
<string name="storage_tag_default">Predefinito</string>
|
||||
<string name="storage_tag_required">Necessario</string>
|
||||
<string name="storage_tag_not_used">Inutilizzato</string>
|
||||
<string name="storage_back">Archiviazione</string>
|
||||
<string name="storage_games_with_data">%1$s con i loro dati</string>
|
||||
<string name="storage_sort_largest">Più grandi</string>
|
||||
<string name="storage_sort_least_played">Meno giocati</string>
|
||||
<string name="storage_sort_sd">Su scheda SD</string>
|
||||
<string name="storage_no_games">Qui non ci sono giochi.</string>
|
||||
<string name="storage_played">Giocato %1$s</string>
|
||||
<string name="storage_never_played">Mai giocato</string>
|
||||
<string name="storage_game_parts">Gioco %1$s · dati %2$s</string>
|
||||
<string name="storage_uninstall">Disinstalla</string>
|
||||
<string name="storage_games_note">La disinstallazione apre la finestra di Steam, così Steam ne tiene traccia. Per spostare un gioco sulla scheda SD, usa Steam › Impostazioni › Archiviazione.</string>
|
||||
<string name="storage_uninstall_title">Disinstallare %1$s?</string>
|
||||
<string name="storage_uninstall_body">Steam si apre e chiede conferma. Questo libera i %1$s del gioco. Il prefisso resta finché non pulisci i residui, e i salvataggi di Steam Cloud restano nel tuo account.</string>
|
||||
<string name="storage_open_steam">Apri Steam</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s inutilizzati</string>
|
||||
<string name="storage_remove_unused">Rimuovi gli inutilizzati · %1$s</string>
|
||||
<string name="storage_in_use">In uso</string>
|
||||
<string name="storage_not_used">Inutilizzato</string>
|
||||
<string name="storage_remove">Rimuovi</string>
|
||||
<string name="storage_remove_title">Rimuovere %1$s?</string>
|
||||
<string name="storage_remove_body">Libera %1$s. Steam lo scaricherà di nuovo se un gioco lo richiede.</string>
|
||||
<string name="storage_tool_fex_runtime">Necessario per i giochi Linux x86 (FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">Necessario ai Proton ARM64</string>
|
||||
<string name="storage_tool_redist">Programmi di installazione VC++ e DirectX richiesti dai giochi</string>
|
||||
<string name="storage_tool_fex">Esegue i giochi Linux x86</string>
|
||||
<string name="storage_tool_steam">Strumento Steam</string>
|
||||
<string name="storage_tool_old_runtime">DroidDeck esegue i giochi senza questo runtime</string>
|
||||
<string name="storage_tool_x86_proton">Proton x86, che DroidDeck non può eseguire</string>
|
||||
<string name="storage_tool_custom_default">Predefinito per tutti i giochi</string>
|
||||
<string name="storage_tool_custom_unused">Nessun gioco lo usa</string>
|
||||
<string name="storage_tool_custom_unknown">Le impostazioni di Steam non sono leggibili, quindi viene conservato</string>
|
||||
<string name="storage_cleanup_busy">Attendi la fine della pulizia dell\'archiviazione</string>
|
||||
<string name="storage_low_title">Solo %1$s liberi</string>
|
||||
<string name="storage_low_body">Steam ha bisogno di spazio per aggiornamenti e cache shader. Sotto i 10 GB gli aggiornamenti dei giochi possono fallire a metà.</string>
|
||||
<string name="storage_low_safe">%1$s si possono liberare senza rischi.</string>
|
||||
<string name="storage_free_up">Libera spazio</string>
|
||||
<string name="storage_play_anyway">Gioca comunque</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d installato</item>
|
||||
<item quantity="other">%d installati</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d su scheda SD</item>
|
||||
<item quantity="other">%d su scheda SD</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">%d strumento</item>
|
||||
<item quantity="other">%d strumenti</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">Cartelle di prefisso vuote e cache shader di %d gioco</item>
|
||||
<item quantity="other">Cartelle di prefisso vuote e cache shader di %d giochi</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">Il prefisso di %d gioco contiene ancora file, che possono includere salvataggi locali. Se ti servono, fanne una copia prima.</item>
|
||||
<item quantity="other">I prefissi di %d giochi contengono ancora file, che possono includere salvataggi locali. Se ti servono, fanne una copia prima.</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">%d gioco</item>
|
||||
<item quantity="other">%d giochi</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">Proton predefinito di DroidDeck</string>
|
||||
<string name="storage_tool_proton">Opzione Proton 11 di DroidDeck</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">Usato da %d gioco</item>
|
||||
<item quantity="other">Usato da %d giochi</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">Componenti</string>
|
||||
<string name="storage_components">Componenti</string>
|
||||
<string name="storage_item_components">Componenti inutilizzati</string>
|
||||
<string name="storage_component_original">Originale %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">Libera %1$s. Ripristina solo una versione di Proton non più installata.</string>
|
||||
<string name="storage_leftovers_unchecked">Non controllato: un manifest di un\'app Steam non è leggibile, quindi i prefissi dei giochi disinstallati restano finché non lo sarà.</string>
|
||||
<string name="storage_failed">Impossibile completare: %1$s</string>
|
||||
<string name="storage_desktop_shared">%1$s condivisi</string>
|
||||
<string name="storage_data_installed">Giochi installati</string>
|
||||
<string name="storage_data_parts">Prefisso %1$s · shader %2$s · workshop %3$s</string>
|
||||
<string name="storage_clear_shaders">Elimina shader</string>
|
||||
<string name="storage_data_leftovers">Giochi disinstallati</string>
|
||||
<string name="storage_steam_app">App Steam %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">Il suo prefisso contiene ancora file, che possono includere salvataggi locali</string>
|
||||
<string name="storage_leftover_confirm">Elimina il suo prefisso Wine e la cache shader (%1$s). Fai prima una copia dei salvataggi che ti servono; quelli di Steam Cloud restano nel tuo account.</string>
|
||||
<string name="storage_data_other">Conservati</string>
|
||||
<string name="storage_data_other_title">Giochi non Steam e prefisso condiviso di Steam</string>
|
||||
<string name="storage_data_other_detail">Prefissi di giochi aggiunti dagli store o a mano</string>
|
||||
<string name="storage_components_packages">Pacchetti scaricati</string>
|
||||
<string name="storage_components_originals">Originali conservati</string>
|
||||
<string name="storage_components_none">Nessun pacchetto di componenti scaricato.</string>
|
||||
<string name="storage_component_in_use">Scelto per un Proton o un gioco</string>
|
||||
<string name="storage_component_unused">Non scelto da nessuna parte</string>
|
||||
<string name="storage_original_kept">Ripristina il Proton installato</string>
|
||||
<string name="storage_original_old">Versione di Proton non più installata</string>
|
||||
<string name="storage_tag_in_use">In uso</string>
|
||||
<string name="storage_components_note">I componenti in uso restano bloccati. Sceglili in Componenti; ciò che rimuovi qui si può scaricare di nuovo lì.</string>
|
||||
<string name="storage_component_confirm">Libera %1$s. I pacchetti importati da te non si possono scaricare di nuovo.</string>
|
||||
<string name="storage_desktop_detail">%1$s per le app Flatpak e ciò che condividono</string>
|
||||
<string name="storage_desktop_apps_title">App</string>
|
||||
<string name="storage_desktop_none">Nessuna app Flatpak installata.</string>
|
||||
<string name="storage_desktop_shared_title">Condiviso dalle app</string>
|
||||
<string name="storage_desktop_runtimes">Runtime</string>
|
||||
<string name="storage_desktop_runtimes_detail">Librerie su cui girano le app; quelle inutilizzate se ne vanno con Rimuovi gli inutilizzati</string>
|
||||
<string name="storage_desktop_cache">Deposito dei download</string>
|
||||
<string name="storage_desktop_cache_detail">Object store e download in cache di Flatpak</string>
|
||||
<string name="storage_desktop_prune">Rimuovi gli inutilizzati</string>
|
||||
<string name="storage_desktop_prune_body">Rimuove i runtime che nessuna app installata richiede e ripulisce l\'object store di Flatpak. Le app installate continuano a funzionare.</string>
|
||||
<string name="storage_flatpak_uninstall_body">Rimuove l\'app (%1$s) e i runtime che nessun altro usa. I suoi dati nella cartella home restano.</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">%d pacchetto</item>
|
||||
<item quantity="other">%d pacchetti</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">%d app</item>
|
||||
<item quantity="other">%d app</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">Video di avvio</string>
|
||||
<string name="boot_movie_video">Video</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d fps</string>
|
||||
<string name="boot_movie_note">60 fps è più fluido ma usa più CPU durante l\'avvio di Steam. Un video di avvio scelto nelle impostazioni di Steam ha la precedenza.</string>
|
||||
<string name="boot_movie_sound">Audio</string>
|
||||
<string name="boot_movie_custom">Il tuo video</string>
|
||||
<string name="boot_movie_custom_hint">MP4, MOV, MKV o WebM. Altri formati e file oltre %1$d MB vengono convertiti in WebM 720p sul dispositivo, fino a %2$d secondi.</string>
|
||||
<string name="boot_movie_import">Importa</string>
|
||||
<string name="boot_movie_remove">Rimuovi</string>
|
||||
<string name="boot_movie_pick">Scegli un video di avvio</string>
|
||||
<string name="boot_movie_imported">%1$s è ora il video di avvio</string>
|
||||
<string name="boot_movie_imported_silent">%1$s è ora il video di avvio, senza audio</string>
|
||||
<string name="boot_movie_too_long">I video da convertire devono durare al massimo %1$d secondi</string>
|
||||
<string name="boot_movie_converting">Conversione %1$d%%</string>
|
||||
<string name="boot_movie_converting_started">Conversione di %1$s per Steam. Tieni aperto DroidDeck.</string>
|
||||
<string name="boot_movie_unsupported">Questo dispositivo non può decodificare il video.</string>
|
||||
<string name="boot_movie_import_failed">Impossibile importare il video</string>
|
||||
</resources>
|
||||
@@ -278,6 +278,9 @@
|
||||
<string name="mode_suspend_steam_note">自動では一時停止し、表示すると再開します。ネイティブでは先にSteamをスリープに備えさせます。手動では「再開」をタップするまで待機します。「しない」では実行し続けます。</string>
|
||||
<string name="mode_background_downloads">ダウンロードを継続</string>
|
||||
<string name="mode_background_downloads_hint">Steamがダウンロード中は、バックグラウンドでの一時停止を遅らせます。</string>
|
||||
<string name="mode_zram">zRAM 圧縮</string>
|
||||
<string name="mode_zram_hint">ゲームの実行中、Steam クライアントのメモリがデバイスの zram に圧縮され、ゲーム用のメモリが増えます。次回 Steam を起動したときに反映されます。</string>
|
||||
<string name="mode_zram_unsupported">使用できません:このデバイスに zram もしくは swap がないか、カーネルが 5.4 未満です。</string>
|
||||
<string name="mode_suspend_note">自動ではバックグラウンドで一時停止し、表示すると再開します。手動では「再開」をタップするまで待機します。「しない」では実行し続けます。</string>
|
||||
<string name="mode_startup">起動</string>
|
||||
<string name="mode_steam_startup">DroidDeck起動時にSteamを起動</string>
|
||||
@@ -573,6 +576,10 @@
|
||||
<string name="drawer_cas">シャープ化(CAS)</string>
|
||||
<string name="drawer_cas_level">シャープ化レベル</string>
|
||||
<string name="drawer_fake_hdr">疑似HDR</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">SteamVR を起動</string>
|
||||
<string name="steamvr_checking">SteamVR を確認しています…</string>
|
||||
<string name="drawer_deband">バンディング除去</string>
|
||||
<string name="drawer_deband_strength">バンディング除去の強度</string>
|
||||
<string name="drawer_brightness">明るさ</string>
|
||||
@@ -590,6 +597,11 @@
|
||||
<string name="drawer_texture_sharpness_note">マイナスのミップマップバイアス:テクスチャがシャープになりますが、ちらつきが出ることがあります。自動では、セッションが画面に合わせて拡大される倍率に合わせます。</string>
|
||||
<string name="drawer_touch_auto">自動(%1$s)</string>
|
||||
<string name="drawer_keyboard">キーボード</string>
|
||||
<string name="drawer_latency">遅延</string>
|
||||
<string name="drawer_low_latency_controller">低遅延コントローラー</string>
|
||||
<string name="drawer_low_latency_controller_hint">コントローラーが 250 Hz ではなく 1000 Hz で報告されます。CPU 使用率がわずかに上がります。</string>
|
||||
<string name="drawer_low_latency_touch">低遅延タッチ</string>
|
||||
<string name="drawer_low_latency_touch_hint">タッチはフレームの結合を省略し、Steam のタッチは 1000 Hz で報告されます。CPU 使用率がわずかに上がります。</string>
|
||||
<string name="drawer_pc_keyboard">PCキーボード</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">セカンドスクリーン</string>
|
||||
@@ -845,7 +857,7 @@
|
||||
<string name="gate_eyebrow">Steam</string>
|
||||
<string name="gate_lede">Androidは多数のバックグラウンドプロセスを起動するアプリを停止しますが、Steamは数十のプロセスを起動します。この設定を変更するまで、ゲームがエラー表示なしで閉じる可能性があります。</string>
|
||||
<string name="gate_fix_for_me">自動で修正</string>
|
||||
<string name="gate_toggle_steps">「子プロセス制限を無効にする」をオンにして戻ってください。開発者向けオプションをオンのままにしないと、スイッチが再びオフになります。</string>
|
||||
<string name="gate_toggle_steps">「子プロセスの制限を無効化」をオンにして戻ってください。開発者向けオプションをオンのままにしないと、スイッチが再びオフになります。</string>
|
||||
<string name="gate_open_dev">開発者向けオプションを開く</string>
|
||||
<string name="gate_no_toggle">このAndroidバージョンにはこのスイッチがないため、DroidDeckはワイヤレスデバッグを介して変更します。約1分かかり、PCは不要です。</string>
|
||||
<string name="gate_needs_wifi">ワイヤレスデバッグにはWi-Fiが必要です。ネットワークは何でもよく、インターネットは不要です。</string>
|
||||
@@ -1011,7 +1023,6 @@
|
||||
<string name="gpu_set_with_runtime">ランタイムドライバーと一緒に設定:%1$s は両方を含みます。</string>
|
||||
<string name="gpu_runtime_hint">ゲームとデスクトップで使用されます。</string>
|
||||
<string name="gpu_runtime_default">ランタイムデフォルト</string>
|
||||
<string name="gpu_display_hint">画面への出力を担当します。DroidDeckの再起動後に有効になります。</string>
|
||||
<string name="gpu_auto_by_gpu">自動 - GPUごとに選択</string>
|
||||
<string name="gpu_manage">ダウンロード、インポート、削除…</string>
|
||||
<string name="gpu_in_use_caps">使用中</string>
|
||||
@@ -1237,10 +1248,16 @@
|
||||
<string name="drivers_downloading_pair">%1$s %2$sをダウンロード中…</string>
|
||||
<string name="drivers_updated_to">%1$s %2$sに更新しました。</string>
|
||||
<string name="drivers_updated_to_restart">%1$s %2$sに更新しました。ディスプレイドライバーはDroidDeckの再起動後に有効になります。</string>
|
||||
<string name="drivers_auto_waits_for_session">ドライバーの更新はセッションの終了後に行われます。</string>
|
||||
<string name="drivers_auto_waits_for_network">モバイルデータと省電力モードを解除すると、%1$s %2$s をダウンロードします。</string>
|
||||
<string name="drivers_skipping_bad">%1$s %2$s はこのデバイスでセッションを開始できなかったため、自動モードは利用できるドライバーを使い続けます。</string>
|
||||
<string name="drivers_rolled_back">%1$s は 2 回セッションを開始できなかったため、自動モードは利用できるドライバーに戻しました。</string>
|
||||
<string name="drivers_switched_to">%1$s %2$sに切り替えました。</string>
|
||||
<string name="drivers_switched_to_restart">%1$s %2$sに切り替えました。ディスプレイドライバーはDroidDeckの再起動後に有効になります。</string>
|
||||
<string name="drivers_using">%1$s %2$sを使用中。</string>
|
||||
<string name="drivers_using_restart">%1$s %2$sを使用中。ディスプレイドライバーはDroidDeckの再起動後に有効になります。</string>
|
||||
<string name="drivers_using_quest">内蔵の Quest ディスプレイドライバーとランタイムの既定値を使用します。</string>
|
||||
<string name="drivers_using_quest_restart">内蔵の Quest ディスプレイドライバーとランタイムの既定値を使用します。ディスプレイドライバーは DroidDeck の再起動後に反映されます。</string>
|
||||
<string name="drivers_not_a_driver">ドライバーzipではありません:AdrenoToolsドライバー、-Linux Turnip、Android + Linuxバンドルのいずれでもありません</string>
|
||||
<string name="drivers_import_failed">インポートに失敗しました:%1$s</string>
|
||||
<string name="drivers_imported_unnamed">インポートしたドライバー</string>
|
||||
@@ -1651,7 +1668,7 @@
|
||||
<string name="adbpair_done_text">タップしてDroidDeckに戻ります。ワイヤレスデバッグはオフにしてかまいません。</string>
|
||||
<string name="adbpair_title_failed">ワイヤレスデバッグのセットアップが中断しました</string>
|
||||
|
||||
<string name="selfinst_http">ダウンロード先がHTTP %1$dを返しました</string>
|
||||
<string name="selfinst_download_failed">ダウンロードに失敗しました</string>
|
||||
<string name="selfinst_size_mismatch">ダウンロードしたファイルのサイズが公開されているAPKと一致しないため、破棄しました。もう一度お試しください</string>
|
||||
<string name="selfinst_checksum_mismatch">ダウンロードしたファイルがチェックサムと一致しないため、破棄しました。もう一度お試しください</string>
|
||||
<string name="selfinst_wrong_package">このアップデートはAndroidパッケージ%1$s用で、%2$s用ではありません</string>
|
||||
@@ -1749,7 +1766,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">GitHubのレート制限に達しました。後でもう一度お試しください</string>
|
||||
<string name="turnip_github_http">GitHubがHTTP %1$dを返しました</string>
|
||||
<string name="turnip_download_http">ダウンロード先がHTTP %1$dを返しました</string>
|
||||
<string name="turnip_download_failed">ダウンロードに失敗しました</string>
|
||||
<string name="turnip_list_predates_checksums">このドライバー一覧はチェックサム導入前のものです。更新してからもう一度お試しください</string>
|
||||
<string name="turnip_checksum_mismatch">チェックサムが一致しないため、ダウンロードを破棄しました</string>
|
||||
<string name="turnip_not_adrenotools">AdrenoToolsドライバーではありません:%1$s</string>
|
||||
@@ -1786,7 +1803,7 @@
|
||||
<string name="phantom_instructions_fix">%1$s それができない場合は、このデバイスをADBが使えるPCに接続し、以下のコマンドを実行してください。その後ここに戻って再確認してください。</string>
|
||||
<string name="phantom_instructions_off">Steamセッションを開始できます。</string>
|
||||
<string name="phantom_instructions_not_applicable">このAndroidバージョンではこの制限は使用されません。</string>
|
||||
<string name="phantom_fix_toggle">開発者向けオプションで「子プロセス制限を無効にする」をオンにし、その後も開発者向けオプションをオンのままにしてください。</string>
|
||||
<string name="phantom_fix_toggle">開発者向けオプションで「子プロセスの制限を無効化」をオンにし、その後も開発者向けオプションをオンのままにしてください。</string>
|
||||
<string name="phantom_fix_no_toggle">このAndroidバージョンにはこのスイッチがありません。ワイヤレスデバッグを使って、このデバイスから変更してください。</string>
|
||||
<string name="upd_block_no_apk">このビルドには、このDroidDeckコピー用のダウンロードがありません。</string>
|
||||
<string name="upd_block_version_code">このビルドのversionCode %1$dはインストール済みの%2$dより小さいため、Androidは上書きインストールしません。</string>
|
||||
@@ -2018,6 +2035,9 @@
|
||||
<string name="stores_install_title">%1$sをインストール:</string>
|
||||
<string name="stores_target_internal">内部ストレージ</string>
|
||||
<string name="stores_target_free">空き %1$s</string>
|
||||
<string name="stores_target_free_unknown">空き容量が不明</string>
|
||||
<string name="stores_storage_insufficient">空き容量が不足しています:%1$s 必要、残り %2$s</string>
|
||||
<string name="stores_storage_low">容量が不足しています(空き %1$s、必要 %2$s)</string>
|
||||
<string name="stores_notification_channel">ストアのダウンロード</string>
|
||||
<string name="stores_notification_channel_hint">ゲームのダウンロードの進行状況を表示し、バックグラウンドでダウンロードを継続します</string>
|
||||
<string name="stores_notification_one">ダウンロード中</string>
|
||||
@@ -2053,6 +2073,21 @@
|
||||
<string name="edit_other_exe">別の exe…</string>
|
||||
<string name="edit_remove">削除</string>
|
||||
<string name="art_cover">カバー</string>
|
||||
<string name="art_wide_cover">ワイドカバー</string>
|
||||
<string name="art_adjust_logo">ロゴを調整</string>
|
||||
<string name="logo_pos_default">既定</string>
|
||||
<string name="logo_pos_upper_left">左上</string>
|
||||
<string name="logo_pos_upper_center">中央上</string>
|
||||
<string name="logo_pos_center">中央</string>
|
||||
<string name="logo_pos_bottom_left">左下</string>
|
||||
<string name="logo_pos_bottom_center">中央下</string>
|
||||
<string name="logo_pos_position">位置</string>
|
||||
<string name="logo_pos_size">サイズ</string>
|
||||
<string name="logo_pos_save">保存</string>
|
||||
<string name="logo_pos_reset">既定に戻す</string>
|
||||
<string name="logo_pos_hint">ゲームの背景に、Steam と同じ方法で描画されます。位置が数ピクセルずれることがあります。</string>
|
||||
<string name="logo_pos_no_logo">このゲームにはまだロゴがありません。ロゴでいずれかを選ぶとここに表示されます。</string>
|
||||
<string name="logo_pos_failed">ロゴの位置を保存できませんでした。もう一度お試しください。</string>
|
||||
<string name="art_background">背景</string>
|
||||
<string name="art_logo">ロゴ</string>
|
||||
<string name="art_icon">アイコン</string>
|
||||
@@ -2065,6 +2100,17 @@
|
||||
<string name="art_pick_image">画像を選択…</string>
|
||||
<string name="art_pick_title">画像を選択: %1$s</string>
|
||||
<string name="art_reset">自動に戻す</string>
|
||||
<string name="art_search_sgdb">SteamGridDB を検索…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB:%1$s</string>
|
||||
<string name="art_sgdb_search_label">ゲームを検索</string>
|
||||
<string name="art_sgdb_other_games">その他の一致</string>
|
||||
<string name="art_sgdb_none">見つかりません。</string>
|
||||
<string name="art_sgdb_no_key">SteamGridDB には API キーが必要です。設定 › ランチャーで追加してください。</string>
|
||||
<string name="art_sgdb_bad_key">SteamGridDB がこの API キーを受け付けませんでした。設定 › ランチャーで確認してください。</string>
|
||||
<string name="art_sgdb_limit">SteamGridDB が現在リクエストを制限しています。しばらくしてからお試しください。</string>
|
||||
<string name="art_sgdb_offline">SteamGridDB に接続できません。ネットワーク接続を確認してください。</string>
|
||||
<string name="art_sgdb_server">SteamGridDB で問題が発生しました。しばらくしてからお試しください。</string>
|
||||
<string name="art_sgdb_no_game">SteamGridDB にこのゲームの一致がありません。検索をお試しください。</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="other">%1$d 本のゲームを追加しました</item>
|
||||
</plurals>
|
||||
@@ -2090,6 +2136,45 @@
|
||||
<string name="gpu_bar_auto">自動 · %1$s</string>
|
||||
<string name="gpu_separately">ランタイムドライバーとディスプレイドライバーを個別に設定</string>
|
||||
<string name="gpu_use_title">%1$sを使用しますか?</string>
|
||||
<string name="gpu_mode_title">ドライバーの選び方</string>
|
||||
<string name="gpu_mode_auto_detail">この GPU に最適な組み合わせを自動で選び、最新に保ちます</string>
|
||||
<string name="gpu_mode_pair">組み合わせ</string>
|
||||
<string name="gpu_mode_pair_detail">1 つの組み合わせを選び、両方のドライバーをまとめて設定します</string>
|
||||
<string name="gpu_mode_mix">個別に選ぶ</string>
|
||||
<string name="gpu_mode_mix_detail">ディスプレイドライバーとランタイムドライバーを別々に選びます</string>
|
||||
<string name="gpu_mode_pair_short">ペア</string>
|
||||
<string name="gpu_mode_mix_short">個別</string>
|
||||
<string name="gpu_src_auto">自動</string>
|
||||
<string name="gpu_src_pair">ペア</string>
|
||||
<string name="gpu_src_bundle">内蔵</string>
|
||||
<string name="gpu_src_chosen">選択済み</string>
|
||||
<string name="gpu_src_by_gpu">GPU 別</string>
|
||||
<string name="gpu_src_default">既定</string>
|
||||
<string name="gpu_display_live">描画中</string>
|
||||
<string name="gpu_display_pending">再起動待ち · 現在は %1$s が描画</string>
|
||||
<string name="gpu_runtime_live">実行中</string>
|
||||
<string name="gpu_runtime_pending">次回セッションで有効 · このセッションは %1$s を使用</string>
|
||||
<string name="gpu_display_next">画面の描画を担当します。次回セッションから有効になります。</string>
|
||||
<string name="gpu_previous_driver">前のドライバー</string>
|
||||
<string name="gpu_restart_banner">再起動してディスプレイドライバーを切り替えます。</string>
|
||||
<string name="gpu_restart_blocked">実行中のセッションを停止してから、ディスプレイドライバーを切り替えるために DroidDeck を再起動してください。</string>
|
||||
<string name="gpu_restart_now">今すぐ再起動</string>
|
||||
<string name="gpu_restart_later">あとで</string>
|
||||
<string name="gpu_auto_title">%1$s に合わせてこの組み合わせを自動で選びました</string>
|
||||
<string name="gpu_auto_builtin_title">自動モードは %1$s に内蔵ドライバーを使用しています</string>
|
||||
<string name="gpu_auto_reason">%1$s シリーズ向けに選択されました。新しいバージョンはバックグラウンドでインストールされます。</string>
|
||||
<string name="gpu_pair_title">組み合わせを選択</string>
|
||||
<string name="gpu_pair_hint">組み合わせはディスプレイドライバーとランタイムドライバーをまとめて設定します。</string>
|
||||
<string name="gpu_mix_title">ドライバーを個別に選択</string>
|
||||
<string name="gpu_mix_hint">内蔵パッケージは両方をまとめて設定します。ディスプレイドライバーの変更は再起動後に、ランタイムドライバーは次回セッションで有効になります。</string>
|
||||
<string name="gpu_change_display">ディスプレイドライバーを変更</string>
|
||||
<string name="gpu_change_runtime">ランタイムドライバーを変更</string>
|
||||
<string name="gpu_autos_pick">自動で選ぶ</string>
|
||||
<string name="gpu_installed">インストール済み</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · 再起動後に有効</string>
|
||||
<string name="gpu_chip_restart">%1$s · 再起動待ち</string>
|
||||
<string name="comp_this_device">このデバイス</string>
|
||||
<string name="proton_remove_title">%1$sを削除しますか?</string>
|
||||
<string name="setup_tab_diagnostics">診断</string>
|
||||
<string name="setup_ready">プレイの準備完了</string>
|
||||
@@ -2101,6 +2186,14 @@
|
||||
<string name="setup_group_look">外観</string>
|
||||
<string name="setup_group_device">このデバイス</string>
|
||||
<string name="setup_group_art">ライブラリのアートワーク</string>
|
||||
<string name="setup_group_download">ダウンロード</string>
|
||||
<string name="setup_download_source">ソース</string>
|
||||
<string name="setup_download_source_auto">自動</string>
|
||||
<string name="setup_download_source_direct">直接</string>
|
||||
<string name="setup_download_source_mirror">ミラー</string>
|
||||
<string name="setup_download_source_custom">カスタムプレフィックス</string>
|
||||
<string name="setup_download_mirror">サイト</string>
|
||||
<string name="setup_download_prefix">プレフィックス</string>
|
||||
<string name="flathub_eyebrow">Linuxアプリ</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">Linuxデスクトップ用のアプリ。「デスクトップ」から開きます。</string>
|
||||
@@ -2214,15 +2307,203 @@
|
||||
<string name="cardboard_recenter_button">リセット</string>
|
||||
<string name="cardboard_steamvr">SteamVR(試験運用)</string>
|
||||
<string name="cardboard_steamvr_install">インストール</string>
|
||||
<string name="cardboard_steamvr_install_hint">ValveのARM64版SteamVRをダウンロードします(571 MB)。</string>
|
||||
<string name="cardboard_steamvr_installing">SteamVRをダウンロードして展開しています…</string>
|
||||
<string name="cardboard_steamvr_start">開始</string>
|
||||
<string name="cardboard_steamvr_ready_hint">頭の動きをヘッドセットとしてSteamVRを実行します。映像はサイドバイサイド3Dに切り替わります。</string>
|
||||
<string name="cardboard_steamvr_stop">停止</string>
|
||||
<string name="cardboard_steamvr_running_hint">SteamVRは実行中です。</string>
|
||||
<string name="cardboard_steamvr_unavailable">このセッションでは利用できません。</string>
|
||||
<string name="cardboard_steamvr_failed">SteamVRが応答しませんでした。セッションログを確認してください。</string>
|
||||
<string name="drawer_component_downloads">コンポーネントをダウンロード</string>
|
||||
<string name="drawer_component_downloads_hint">ダウンロードは「コンポーネント」ページと共有されます。上でダウンロード済みのバージョンを選ぶと、次回のゲーム起動時に使用されます。</string>
|
||||
<string name="drawer_component_choose_download">ダウンロードするバージョンを選択</string>
|
||||
<string name="display_picture_position">画面の位置</string>
|
||||
<string name="display_position_top">上</string>
|
||||
<string name="display_position_bottom">下</string>
|
||||
<string name="display_position_center">中央(既定)</string>
|
||||
<string name="setup_tab_storage">ストレージ</string>
|
||||
<string name="storage_measuring">ストレージを集計しています…</string>
|
||||
<string name="storage_measuring_hint">初回の集計には数秒かかります。</string>
|
||||
<string name="storage_internal">内部ストレージ</string>
|
||||
<string name="storage_used_of">%1$s / %2$s 使用中</string>
|
||||
<string name="storage_free">空き %1$s</string>
|
||||
<string name="storage_droiddeck_uses">DroidDeck が %1$s を使用しています。集計時刻 %2$s。</string>
|
||||
<string name="storage_rescanning">再集計しています…</string>
|
||||
<string name="storage_other_apps">その他の Android アプリ</string>
|
||||
<string name="storage_safe_now">今すぐ安全に解放できます</string>
|
||||
<string name="storage_safe_hint">未使用のランタイム、残留、古いログ</string>
|
||||
<string name="storage_clean_up">クリーンアップ…</string>
|
||||
<string name="storage_session_running">セッションを停止するとクリーンアップや削除ができます。容量は引き続き表示されます。</string>
|
||||
<string name="storage_where">使用内訳</string>
|
||||
<string name="storage_games">Steam ゲーム</string>
|
||||
<string name="storage_game_data">ゲームデータ</string>
|
||||
<string name="storage_game_data_hint">プレフィックス %1$s · ワークショップ %2$s · シェーダー %3$s</string>
|
||||
<string name="storage_tools">Proton とランタイム</string>
|
||||
<string name="storage_tools_unused">%1$s 未使用</string>
|
||||
<string name="storage_stores">GOG、Epic などのストア</string>
|
||||
<string name="storage_stores_hint">ストアでアンインストール</string>
|
||||
<string name="storage_desktop">デスクトップアプリ</string>
|
||||
<string name="storage_desktop_hint">Flatpak アプリとランタイム</string>
|
||||
<string name="storage_system">Linux システムと Steam クライアント</string>
|
||||
<string name="storage_system_hint">何かを動かすのに必要</string>
|
||||
<string name="storage_app_data">DroidDeck のデータ</string>
|
||||
<string name="storage_app_data_hint">古いセッションログ %1$s · キャッシュ · ドライバー</string>
|
||||
<string name="storage_legend_games">ゲーム</string>
|
||||
<string name="storage_legend_data">ゲームデータ</string>
|
||||
<string name="storage_legend_tools">Proton とランタイム</string>
|
||||
<string name="storage_legend_stores">その他のストア</string>
|
||||
<string name="storage_legend_desktop">デスクトップアプリ</string>
|
||||
<string name="storage_legend_system">Linux システム</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">クリーンアップ</string>
|
||||
<string name="storage_clean_up_lede">インストール済みのゲームや進行中のダウンロードには影響しません。</string>
|
||||
<string name="storage_nothing_to_clean">クリーンアップできるものはありません。</string>
|
||||
<string name="storage_selected">選択中:%1$s · %2$s → 空き %3$s</string>
|
||||
<string name="storage_cleaning">クリーンアップしています…</string>
|
||||
<string name="storage_free_button">%1$s を解放</string>
|
||||
<string name="storage_freed">%1$s を解放しました。</string>
|
||||
<string name="storage_item_runtimes">未使用の Steam ランタイムと Proton</string>
|
||||
<string name="storage_item_leftovers">アンインストールしたゲームの残留</string>
|
||||
<string name="storage_item_leftovers_unsure">セーブデータを含む可能性がある残留</string>
|
||||
<string name="storage_item_logs">古いセッションログ</string>
|
||||
<string name="storage_item_logs_detail">現在のセッションのログは保持されます</string>
|
||||
<string name="storage_item_custom_detail">どのゲームも使用していません。使用するにはコンポーネントで再インストールしてください。</string>
|
||||
<string name="storage_item_shaders">インストール済みゲームのシェーダーキャッシュ</string>
|
||||
<string name="storage_item_shaders_detail">プレイすると再生成されます。各ゲームの最初の数分は再びカクつきます。</string>
|
||||
<string name="storage_tag_safe">安全</string>
|
||||
<string name="storage_tag_unused">未使用</string>
|
||||
<string name="storage_tag_stutter">カクつき</string>
|
||||
<string name="storage_tag_saves">セーブデータを確認</string>
|
||||
<string name="storage_tag_default">既定</string>
|
||||
<string name="storage_tag_required">必須</string>
|
||||
<string name="storage_tag_not_used">未使用</string>
|
||||
<string name="storage_back">ストレージ</string>
|
||||
<string name="storage_games_with_data">%1$s(データ込み)</string>
|
||||
<string name="storage_sort_largest">大きい順</string>
|
||||
<string name="storage_sort_least_played">プレイ時間が短い順</string>
|
||||
<string name="storage_sort_sd">SD カード上</string>
|
||||
<string name="storage_no_games">ここにゲームはありません。</string>
|
||||
<string name="storage_played">プレイ時間 %1$s</string>
|
||||
<string name="storage_never_played">未プレイ</string>
|
||||
<string name="storage_game_parts">ゲーム %1$s · データ %2$s</string>
|
||||
<string name="storage_uninstall">アンインストール</string>
|
||||
<string name="storage_games_note">アンインストールすると Steam 標準のダイアログが開き、Steam 側に記録されます。ゲームを SD カードへ移すには Steam › 設定 › ストレージを使用してください。</string>
|
||||
<string name="storage_uninstall_title">%1$s をアンインストールしますか?</string>
|
||||
<string name="storage_uninstall_body">Steam が開いて確認を求めます。このゲームの %1$s が解放されます。プレフィックスは残留のクリーンアップまで残り、Steam クラウドのセーブデータはアカウントに残ります。</string>
|
||||
<string name="storage_open_steam">Steam を開く</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s 未使用</string>
|
||||
<string name="storage_remove_unused">未使用を削除 · %1$s</string>
|
||||
<string name="storage_in_use">使用中</string>
|
||||
<string name="storage_not_used">未使用</string>
|
||||
<string name="storage_remove">削除</string>
|
||||
<string name="storage_remove_title">%1$s を削除しますか?</string>
|
||||
<string name="storage_remove_body">%1$s を解放します。後でゲームが必要とすれば Steam が再ダウンロードします。</string>
|
||||
<string name="storage_tool_fex_runtime">Linux x86 ゲームに必要(FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">ARM64 Proton に必要</string>
|
||||
<string name="storage_tool_redist">ゲームが要求した VC++ と DirectX のインストーラー</string>
|
||||
<string name="storage_tool_fex">Linux x86 ゲームを実行</string>
|
||||
<string name="storage_tool_steam">Steam ツール</string>
|
||||
<string name="storage_tool_old_runtime">DroidDeck はこのランタイムなしでゲームを実行できます</string>
|
||||
<string name="storage_tool_x86_proton">x86 Proton のため DroidDeck では実行できません</string>
|
||||
<string name="storage_tool_custom_default">すべてのゲームの既定</string>
|
||||
<string name="storage_tool_custom_unused">どのゲームも使用していません</string>
|
||||
<string name="storage_tool_custom_unknown">Steam の設定を読み取れないため保持します</string>
|
||||
<string name="storage_cleanup_busy">ストレージのクリーンアップが終わるまでお待ちください</string>
|
||||
<string name="storage_low_title">空きが %1$s のみです</string>
|
||||
<string name="storage_low_body">Steam は更新とシェーダーキャッシュ用の空き容量を必要とします。10 GB を下回ると、ゲームの更新が途中で失敗することがあります。</string>
|
||||
<string name="storage_low_safe">%1$s を安全に解放できます。</string>
|
||||
<string name="storage_free_up">容量を空ける</string>
|
||||
<string name="storage_play_anyway">そのままプレイ</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d 件インストール済み</item>
|
||||
<item quantity="other">%d 件インストール済み</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d 件が SD カード上</item>
|
||||
<item quantity="other">%d 件が SD カード上</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">%d 個のツール</item>
|
||||
<item quantity="other">%d 個のツール</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">%d 件のゲームの空のプレフィックスフォルダーとシェーダーキャッシュ</item>
|
||||
<item quantity="other">%d 件のゲームの空のプレフィックスフォルダーとシェーダーキャッシュ</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">%d 件のゲームのプレフィックスにファイルが残っており、ローカルのセーブデータが含まれる可能性があります。残す場合は先にバックアップしてください。</item>
|
||||
<item quantity="other">%d 件のゲームのプレフィックスにファイルが残っており、ローカルのセーブデータが含まれる可能性があります。残す場合は先にバックアップしてください。</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">%d 件のゲーム</item>
|
||||
<item quantity="other">%d 件のゲーム</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">DroidDeck の既定 Proton</string>
|
||||
<string name="storage_tool_proton">DroidDeck の Proton 11 オプション</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">%d 件のゲームが使用</item>
|
||||
<item quantity="other">%d 件のゲームが使用</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">コンポーネント</string>
|
||||
<string name="storage_components">コンポーネント</string>
|
||||
<string name="storage_item_components">未使用のコンポーネント</string>
|
||||
<string name="storage_component_original">元の %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">%1$s を解放します。すでにインストールされていない Proton バージョンを戻すだけです。</string>
|
||||
<string name="storage_leftovers_unchecked">未確認:Steam のアプリマニフェストを読み取れないため、読み取れるまでアンインストールしたゲームのプレフィックスを保持します。</string>
|
||||
<string name="storage_failed">完了できませんでした:%1$s</string>
|
||||
<string name="storage_desktop_shared">共有 %1$s</string>
|
||||
<string name="storage_data_installed">インストール済みゲーム</string>
|
||||
<string name="storage_data_parts">プレフィックス %1$s · シェーダー %2$s · ワークショップ %3$s</string>
|
||||
<string name="storage_clear_shaders">シェーダーを削除</string>
|
||||
<string name="storage_data_leftovers">アンインストールしたゲーム</string>
|
||||
<string name="storage_steam_app">Steam アプリ %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">プレフィックスにファイルが残っており、ローカルのセーブデータが含まれる可能性があります</string>
|
||||
<string name="storage_leftover_confirm">Wine プレフィックスとシェーダーキャッシュ(%1$s)を削除します。必要なセーブデータは先にバックアップしてください。Steam クラウドのセーブデータはアカウントに残ります。</string>
|
||||
<string name="storage_data_other">保持</string>
|
||||
<string name="storage_data_other_title">Steam 以外のゲームと Steam の共有プレフィックス</string>
|
||||
<string name="storage_data_other_detail">ストアから、または手動で追加したゲームのプレフィックス</string>
|
||||
<string name="storage_components_packages">ダウンロード済みのパッケージ</string>
|
||||
<string name="storage_components_originals">保存済みの元ファイル</string>
|
||||
<string name="storage_components_none">ダウンロード済みのコンポーネントパッケージはありません。</string>
|
||||
<string name="storage_component_in_use">Proton またはゲームで選択済み</string>
|
||||
<string name="storage_component_unused">どこでも選択されていません</string>
|
||||
<string name="storage_original_kept">インストール済みの Proton を戻す</string>
|
||||
<string name="storage_original_old">インストールされていない Proton バージョン</string>
|
||||
<string name="storage_tag_in_use">使用中</string>
|
||||
<string name="storage_components_note">使用中のコンポーネントはロックされます。コンポーネントで選択してください。ここで削除したものはそこで再ダウンロードできます。</string>
|
||||
<string name="storage_component_confirm">%1$s を解放します。自分でインポートしたパッケージは再ダウンロードできません。</string>
|
||||
<string name="storage_desktop_detail">%1$s(Flatpak アプリとその共有内容に使用)</string>
|
||||
<string name="storage_desktop_apps_title">アプリ</string>
|
||||
<string name="storage_desktop_none">Flatpak アプリはインストールされていません。</string>
|
||||
<string name="storage_desktop_shared_title">アプリの共有</string>
|
||||
<string name="storage_desktop_runtimes">ランタイム</string>
|
||||
<string name="storage_desktop_runtimes_detail">アプリが依存するライブラリ。未使用のものは「未使用を削除」でクリーンアップされます</string>
|
||||
<string name="storage_desktop_cache">ダウンロードリポジトリ</string>
|
||||
<string name="storage_desktop_cache_detail">Flatpak のオブジェクトリポジトリとキャッシュされたダウンロード</string>
|
||||
<string name="storage_desktop_prune">未使用を削除</string>
|
||||
<string name="storage_desktop_prune_body">インストール済みのアプリが必要としないランタイムを削除し、Flatpak のオブジェクトリポジトリをクリーンアップします。インストール済みのアプリはそのまま動作します。</string>
|
||||
<string name="storage_flatpak_uninstall_body">このアプリ(%1$s)と、他のアプリが使用していないランタイムを削除します。ホームディレクトリのデータは保持されます。</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">%d 個のパッケージ</item>
|
||||
<item quantity="other">%d 個のパッケージ</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">%d 個のアプリ</item>
|
||||
<item quantity="other">%d 個のアプリ</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">起動ムービー</string>
|
||||
<string name="boot_movie_video">ムービー</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d fps</string>
|
||||
<string name="boot_movie_note">60 fps はなめらかですが、Steam の起動中に CPU をより多く使います。Steam 本体の設定で選んだ起動ムービーが優先されます。</string>
|
||||
<string name="boot_movie_sound">サウンド</string>
|
||||
<string name="boot_movie_custom">自分のムービー</string>
|
||||
<string name="boot_movie_custom_hint">MP4、MOV、MKV、WebM。その他の形式や %1$d MB を超えるファイルは、端末上で 720p の WebM に変換されます(%2$d 秒まで)。</string>
|
||||
<string name="boot_movie_import">インポート</string>
|
||||
<string name="boot_movie_remove">削除</string>
|
||||
<string name="boot_movie_pick">起動ムービーを選択</string>
|
||||
<string name="boot_movie_imported">%1$s を起動ムービーに設定しました</string>
|
||||
<string name="boot_movie_imported_silent">%1$s を起動ムービーに設定しました(無音)</string>
|
||||
<string name="boot_movie_too_long">変換が必要なムービーは %1$d 秒以内にしてください</string>
|
||||
<string name="boot_movie_converting">変換中 %1$d%%</string>
|
||||
<string name="boot_movie_converting_started">%1$s を Steam 用に変換しています。DroidDeck を開いたままにしてください。</string>
|
||||
<string name="boot_movie_unsupported">この端末ではその動画をデコードできません。</string>
|
||||
<string name="boot_movie_import_failed">ムービーをインポートできませんでした</string>
|
||||
</resources>
|
||||
@@ -278,6 +278,9 @@
|
||||
<string name="mode_suspend_steam_note">자동: 일시 중지되고 다시 표시되면 재개됩니다. 네이티브: 먼저 Steam을 절전에 대비시킵니다. 수동: \"재개\"를 탭할 때까지 기다립니다. 안 함: 계속 실행됩니다.</string>
|
||||
<string name="mode_background_downloads">다운로드 계속 진행</string>
|
||||
<string name="mode_background_downloads_hint">Steam이 다운로드 중일 때는 백그라운드 일시 중지를 미룹니다.</string>
|
||||
<string name="mode_zram">zRAM 압축</string>
|
||||
<string name="mode_zram_hint">게임을 실행하는 동안 Steam 클라이언트의 메모리가 기기의 zram으로 압축되어 게임에 더 많은 메모리가 남습니다. 다음에 Steam을 시작할 때 적용됩니다.</string>
|
||||
<string name="mode_zram_unsupported">사용할 수 없음: 이 기기에 zram 또는 swap이 없거나 커널이 5.4 미만입니다.</string>
|
||||
<string name="mode_suspend_note">자동: 백그라운드에서 일시 중지되고 다시 표시되면 재개됩니다. 수동: \"재개\"를 탭할 때까지 기다립니다. 안 함: 계속 실행됩니다.</string>
|
||||
<string name="mode_startup">시작</string>
|
||||
<string name="mode_steam_startup">DroidDeck을 열 때 Steam 시작</string>
|
||||
@@ -573,6 +576,10 @@
|
||||
<string name="drawer_cas">샤프닝(CAS)</string>
|
||||
<string name="drawer_cas_level">샤프닝 강도</string>
|
||||
<string name="drawer_fake_hdr">가상 HDR</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">SteamVR 시작</string>
|
||||
<string name="steamvr_checking">SteamVR 확인 중…</string>
|
||||
<string name="drawer_deband">디밴딩</string>
|
||||
<string name="drawer_deband_strength">디밴딩 강도</string>
|
||||
<string name="drawer_brightness">밝기</string>
|
||||
@@ -590,6 +597,11 @@
|
||||
<string name="drawer_texture_sharpness_note">음수 밉 바이어스: 텍스처가 선명해지지만 약간 깜박일 수 있습니다. 자동으로 설정하면 세션 화면이 확대되는 정도에 맞춥니다.</string>
|
||||
<string name="drawer_touch_auto">자동(%1$s)</string>
|
||||
<string name="drawer_keyboard">키보드</string>
|
||||
<string name="drawer_latency">지연</string>
|
||||
<string name="drawer_low_latency_controller">저지연 컨트롤러</string>
|
||||
<string name="drawer_low_latency_controller_hint">컨트롤러가 250 Hz 대신 1000 Hz로 보고됩니다. CPU 사용량이 약간 늘어납니다.</string>
|
||||
<string name="drawer_low_latency_touch">저지연 터치</string>
|
||||
<string name="drawer_low_latency_touch_hint">터치가 프레임 병합을 건너뛰고 Steam 터치가 1000 Hz로 보고됩니다. CPU 사용량이 약간 늘어납니다.</string>
|
||||
<string name="drawer_pc_keyboard">PC 키보드</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">보조 화면</string>
|
||||
@@ -1011,7 +1023,6 @@
|
||||
<string name="gpu_set_with_runtime">런타임 드라이버와 함께 설정: %1$s에 둘 다 포함됩니다.</string>
|
||||
<string name="gpu_runtime_hint">게임 및 데스크톱에서 사용됩니다.</string>
|
||||
<string name="gpu_runtime_default">런타임 기본값</string>
|
||||
<string name="gpu_display_hint">화면 출력을 담당합니다. DroidDeck을 다시 시작한 후 적용됩니다.</string>
|
||||
<string name="gpu_auto_by_gpu">자동 - GPU별 선택</string>
|
||||
<string name="gpu_manage">다운로드, 가져오기 또는 삭제…</string>
|
||||
<string name="gpu_in_use_caps">사용 중</string>
|
||||
@@ -1237,10 +1248,16 @@
|
||||
<string name="drivers_downloading_pair">%1$s %2$s 다운로드 중…</string>
|
||||
<string name="drivers_updated_to">%1$s %2$s(으)로 업데이트했습니다.</string>
|
||||
<string name="drivers_updated_to_restart">%1$s %2$s(으)로 업데이트했습니다. 디스플레이 드라이버는 DroidDeck을 다시 시작한 후 적용됩니다.</string>
|
||||
<string name="drivers_auto_waits_for_session">드라이버 업데이트는 세션이 끝난 뒤 진행됩니다.</string>
|
||||
<string name="drivers_auto_waits_for_network">모바일 데이터와 절전 모드를 해제하면 다음을 내려받습니다: %1$s %2$s</string>
|
||||
<string name="drivers_skipping_bad">%1$s %2$s 드라이버가 이 기기에서 세션을 시작하지 못해 자동 모드가 사용 가능한 드라이버를 계속 사용합니다.</string>
|
||||
<string name="drivers_rolled_back">%1$s 드라이버가 두 번 세션을 시작하지 못해 자동 모드가 사용 가능한 드라이버로 되돌렸습니다.</string>
|
||||
<string name="drivers_switched_to">%1$s %2$s(으)로 전환했습니다.</string>
|
||||
<string name="drivers_switched_to_restart">%1$s %2$s(으)로 전환했습니다. 디스플레이 드라이버는 DroidDeck을 다시 시작한 후 적용됩니다.</string>
|
||||
<string name="drivers_using">%1$s %2$s 사용 중.</string>
|
||||
<string name="drivers_using_restart">%1$s %2$s 사용 중. 디스플레이 드라이버는 DroidDeck을 다시 시작한 후 적용됩니다.</string>
|
||||
<string name="drivers_using_quest">내장 Quest 디스플레이 드라이버와 런타임 기본값을 사용합니다.</string>
|
||||
<string name="drivers_using_quest_restart">내장 Quest 디스플레이 드라이버와 런타임 기본값을 사용합니다. 디스플레이 드라이버는 DroidDeck을 다시 시작한 뒤 적용됩니다.</string>
|
||||
<string name="drivers_not_a_driver">드라이버 zip이 아닙니다. AdrenoTools 드라이버, -Linux Turnip, Android + Linux 번들 중 어느 것도 아닙니다</string>
|
||||
<string name="drivers_import_failed">가져오기 실패: %1$s</string>
|
||||
<string name="drivers_imported_unnamed">가져온 드라이버</string>
|
||||
@@ -1651,7 +1668,7 @@
|
||||
<string name="adbpair_done_text">탭하여 DroidDeck으로 돌아가세요. 이제 무선 디버깅을 꺼도 됩니다.</string>
|
||||
<string name="adbpair_title_failed">무선 디버깅 설정이 중단되었습니다</string>
|
||||
|
||||
<string name="selfinst_http">다운로드 응답: HTTP %1$d</string>
|
||||
<string name="selfinst_download_failed">다운로드 실패</string>
|
||||
<string name="selfinst_size_mismatch">다운로드 크기가 배포된 APK와 일치하지 않아 삭제했습니다. 다시 시도하세요</string>
|
||||
<string name="selfinst_checksum_mismatch">다운로드한 파일이 체크섬과 일치하지 않아 삭제했습니다. 다시 시도하세요</string>
|
||||
<string name="selfinst_wrong_package">이 업데이트는 %2$s이(가) 아닌 Android 패키지 %1$s용입니다</string>
|
||||
@@ -1749,7 +1766,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">GitHub 요청 한도에 도달했습니다. 나중에 다시 시도하세요</string>
|
||||
<string name="turnip_github_http">GitHub 응답: HTTP %1$d</string>
|
||||
<string name="turnip_download_http">다운로드 응답: HTTP %1$d</string>
|
||||
<string name="turnip_download_failed">다운로드 실패</string>
|
||||
<string name="turnip_list_predates_checksums">이 드라이버 목록은 체크섬 도입 이전의 것입니다. 새로 고친 후 다시 시도하세요</string>
|
||||
<string name="turnip_checksum_mismatch">체크섬이 일치하지 않아 다운로드를 삭제했습니다</string>
|
||||
<string name="turnip_not_adrenotools">AdrenoTools 드라이버가 아닙니다: %1$s</string>
|
||||
@@ -2018,6 +2035,9 @@
|
||||
<string name="stores_install_title">%1$s 설치:</string>
|
||||
<string name="stores_target_internal">내부 저장 공간</string>
|
||||
<string name="stores_target_free">%1$s 사용 가능</string>
|
||||
<string name="stores_target_free_unknown">사용 가능한 공간 알 수 없음</string>
|
||||
<string name="stores_storage_insufficient">공간 부족: %1$s 필요, %2$s 남음</string>
|
||||
<string name="stores_storage_low">공간 부족(사용 가능 %1$s, 필요 %2$s)</string>
|
||||
<string name="stores_notification_channel">스토어 다운로드</string>
|
||||
<string name="stores_notification_channel_hint">게임 다운로드 진행률을 표시하고 백그라운드에서 다운로드를 계속 진행합니다</string>
|
||||
<string name="stores_notification_one">다운로드 중</string>
|
||||
@@ -2053,6 +2073,21 @@
|
||||
<string name="edit_other_exe">다른 exe…</string>
|
||||
<string name="edit_remove">제거</string>
|
||||
<string name="art_cover">커버</string>
|
||||
<string name="art_wide_cover">와이드 커버</string>
|
||||
<string name="art_adjust_logo">로고 조정</string>
|
||||
<string name="logo_pos_default">기본</string>
|
||||
<string name="logo_pos_upper_left">왼쪽 위</string>
|
||||
<string name="logo_pos_upper_center">가운데 위</string>
|
||||
<string name="logo_pos_center">가운데</string>
|
||||
<string name="logo_pos_bottom_left">왼쪽 아래</string>
|
||||
<string name="logo_pos_bottom_center">가운데 아래</string>
|
||||
<string name="logo_pos_position">위치</string>
|
||||
<string name="logo_pos_size">크기</string>
|
||||
<string name="logo_pos_save">저장</string>
|
||||
<string name="logo_pos_reset">기본값으로</string>
|
||||
<string name="logo_pos_hint">게임 배경에 Steam과 같은 방식으로 그려집니다. 위치가 몇 픽셀 어긋날 수 있습니다.</string>
|
||||
<string name="logo_pos_no_logo">이 게임에는 아직 로고가 없습니다. 로고에서 하나를 고르면 여기에 표시됩니다.</string>
|
||||
<string name="logo_pos_failed">로고 위치를 저장하지 못했습니다. 다시 시도하세요.</string>
|
||||
<string name="art_background">배경</string>
|
||||
<string name="art_logo">로고</string>
|
||||
<string name="art_icon">아이콘</string>
|
||||
@@ -2065,6 +2100,17 @@
|
||||
<string name="art_pick_image">이미지 선택…</string>
|
||||
<string name="art_pick_title">이미지 선택: %1$s</string>
|
||||
<string name="art_reset">자동으로 재설정</string>
|
||||
<string name="art_search_sgdb">SteamGridDB 검색…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB: %1$s</string>
|
||||
<string name="art_sgdb_search_label">게임 검색</string>
|
||||
<string name="art_sgdb_other_games">다른 결과</string>
|
||||
<string name="art_sgdb_none">찾을 수 없습니다.</string>
|
||||
<string name="art_sgdb_no_key">SteamGridDB에는 API 키가 필요합니다. 설정 › 런처에서 추가하세요.</string>
|
||||
<string name="art_sgdb_bad_key">SteamGridDB가 이 API 키를 받지 않았습니다. 설정 › 런처에서 확인하세요.</string>
|
||||
<string name="art_sgdb_limit">SteamGridDB가 현재 요청을 제한하고 있습니다. 잠시 후 다시 시도하세요.</string>
|
||||
<string name="art_sgdb_offline">SteamGridDB에 연결할 수 없습니다. 네트워크 연결을 확인하세요.</string>
|
||||
<string name="art_sgdb_server">SteamGridDB에 문제가 발생했습니다. 잠시 후 다시 시도하세요.</string>
|
||||
<string name="art_sgdb_no_game">SteamGridDB에 이 게임의 결과가 없습니다. 검색을 사용해 보세요.</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="other">게임 %1$d개 추가됨</item>
|
||||
</plurals>
|
||||
@@ -2090,6 +2136,45 @@
|
||||
<string name="gpu_bar_auto">자동 · %1$s</string>
|
||||
<string name="gpu_separately">런타임 및 디스플레이 드라이버 개별 설정</string>
|
||||
<string name="gpu_use_title">%1$s을(를) 사용하시겠습니까?</string>
|
||||
<string name="gpu_mode_title">드라이버 선택 방식</string>
|
||||
<string name="gpu_mode_auto_detail">이 GPU에 가장 좋은 조합을 자동으로 선택하고 최신으로 유지합니다</string>
|
||||
<string name="gpu_mode_pair">조합</string>
|
||||
<string name="gpu_mode_pair_detail">조합 하나를 골라 두 드라이버를 함께 설정합니다</string>
|
||||
<string name="gpu_mode_mix">개별 선택</string>
|
||||
<string name="gpu_mode_mix_detail">디스플레이 드라이버와 런타임 드라이버를 따로 선택합니다</string>
|
||||
<string name="gpu_mode_pair_short">조합</string>
|
||||
<string name="gpu_mode_mix_short">개별</string>
|
||||
<string name="gpu_src_auto">자동</string>
|
||||
<string name="gpu_src_pair">조합</string>
|
||||
<string name="gpu_src_bundle">내장</string>
|
||||
<string name="gpu_src_chosen">선택됨</string>
|
||||
<string name="gpu_src_by_gpu">GPU별</string>
|
||||
<string name="gpu_src_default">기본</string>
|
||||
<string name="gpu_display_live">그리는 중</string>
|
||||
<string name="gpu_display_pending">재시작 대기 · 현재 %1$s 그리는 중</string>
|
||||
<string name="gpu_runtime_live">실행 중</string>
|
||||
<string name="gpu_runtime_pending">다음 세션에 적용 · 이번 세션은 %1$s 사용</string>
|
||||
<string name="gpu_display_next">화면 그리기를 담당합니다. 다음 세션부터 적용됩니다.</string>
|
||||
<string name="gpu_previous_driver">이전 드라이버</string>
|
||||
<string name="gpu_restart_banner">디스플레이 드라이버를 바꾸려면 다시 시작하세요.</string>
|
||||
<string name="gpu_restart_blocked">실행 중인 세션을 중지한 뒤 디스플레이 드라이버를 바꾸려면 DroidDeck을 다시 시작하세요.</string>
|
||||
<string name="gpu_restart_now">지금 다시 시작</string>
|
||||
<string name="gpu_restart_later">나중에</string>
|
||||
<string name="gpu_auto_title">%1$s에 맞춰 이 조합을 자동으로 선택했습니다</string>
|
||||
<string name="gpu_auto_builtin_title">자동 모드가 %1$s에 내장 드라이버를 사용합니다</string>
|
||||
<string name="gpu_auto_reason">%1$s 시리즈에 맞춰 선택했습니다. 새 버전은 백그라운드에서 설치됩니다.</string>
|
||||
<string name="gpu_pair_title">조합 선택</string>
|
||||
<string name="gpu_pair_hint">조합은 디스플레이 드라이버와 런타임 드라이버를 함께 설정합니다.</string>
|
||||
<string name="gpu_mix_title">드라이버 개별 선택</string>
|
||||
<string name="gpu_mix_hint">내장 패키지는 둘 다 함께 채웁니다. 디스플레이 드라이버 변경은 다시 시작한 뒤, 런타임 드라이버는 다음 세션에 적용됩니다.</string>
|
||||
<string name="gpu_change_display">디스플레이 드라이버 변경</string>
|
||||
<string name="gpu_change_runtime">런타임 드라이버 변경</string>
|
||||
<string name="gpu_autos_pick">자동 선택</string>
|
||||
<string name="gpu_installed">설치됨</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · 재시작 후 적용</string>
|
||||
<string name="gpu_chip_restart">%1$s · 재시작 대기</string>
|
||||
<string name="comp_this_device">이 기기</string>
|
||||
<string name="proton_remove_title">%1$s을(를) 제거하시겠습니까?</string>
|
||||
<string name="setup_tab_diagnostics">진단</string>
|
||||
<string name="setup_ready">플레이 준비 완료</string>
|
||||
@@ -2101,6 +2186,14 @@
|
||||
<string name="setup_group_look">외관</string>
|
||||
<string name="setup_group_device">이 기기</string>
|
||||
<string name="setup_group_art">라이브러리 아트워크</string>
|
||||
<string name="setup_group_download">다운로드</string>
|
||||
<string name="setup_download_source">소스</string>
|
||||
<string name="setup_download_source_auto">자동</string>
|
||||
<string name="setup_download_source_direct">직접</string>
|
||||
<string name="setup_download_source_mirror">미러</string>
|
||||
<string name="setup_download_source_custom">사용자 지정 프리픽스</string>
|
||||
<string name="setup_download_mirror">사이트</string>
|
||||
<string name="setup_download_prefix">프리픽스</string>
|
||||
<string name="flathub_eyebrow">LINUX 앱</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">Linux 데스크톱용 앱으로, 데스크톱에서 엽니다.</string>
|
||||
@@ -2214,15 +2307,203 @@
|
||||
<string name="cardboard_recenter_button">재설정</string>
|
||||
<string name="cardboard_steamvr">SteamVR (실험적)</string>
|
||||
<string name="cardboard_steamvr_install">설치</string>
|
||||
<string name="cardboard_steamvr_install_hint">Valve의 ARM64용 SteamVR을 다운로드합니다(571MB).</string>
|
||||
<string name="cardboard_steamvr_installing">SteamVR 다운로드 및 압축 해제 중…</string>
|
||||
<string name="cardboard_steamvr_start">시작</string>
|
||||
<string name="cardboard_steamvr_ready_hint">머리를 헤드셋으로 삼아 SteamVR을 실행하며, 화면이 사이드 바이 사이드 3D로 전환됩니다.</string>
|
||||
<string name="cardboard_steamvr_stop">중지</string>
|
||||
<string name="cardboard_steamvr_running_hint">SteamVR이 실행 중입니다.</string>
|
||||
<string name="cardboard_steamvr_unavailable">이 세션에서는 사용할 수 없습니다.</string>
|
||||
<string name="cardboard_steamvr_failed">SteamVR이 응답하지 않았습니다. 세션 로그를 확인하세요.</string>
|
||||
<string name="drawer_component_downloads">구성 요소 다운로드</string>
|
||||
<string name="drawer_component_downloads_hint">다운로드는 구성 요소 페이지와 공유됩니다. 위에서 다운로드한 버전을 선택하면 다음 게임 실행 시 사용됩니다.</string>
|
||||
<string name="drawer_component_choose_download">다운로드할 버전 선택</string>
|
||||
<string name="display_picture_position">화면 위치</string>
|
||||
<string name="display_position_top">위</string>
|
||||
<string name="display_position_bottom">아래</string>
|
||||
<string name="display_position_center">가운데(기본)</string>
|
||||
<string name="setup_tab_storage">저장 공간</string>
|
||||
<string name="storage_measuring">저장 공간 계산 중…</string>
|
||||
<string name="storage_measuring_hint">처음 계산할 때는 몇 초 걸립니다.</string>
|
||||
<string name="storage_internal">내부 저장 공간</string>
|
||||
<string name="storage_used_of">%1$s / %2$s 사용 중</string>
|
||||
<string name="storage_free">%1$s 사용 가능</string>
|
||||
<string name="storage_droiddeck_uses">DroidDeck 사용량 %1$s. 계산 시각 %2$s.</string>
|
||||
<string name="storage_rescanning">다시 계산 중…</string>
|
||||
<string name="storage_other_apps">다른 Android 앱</string>
|
||||
<string name="storage_safe_now">지금 안전하게 확보 가능</string>
|
||||
<string name="storage_safe_hint">사용하지 않는 런타임, 잔여물, 오래된 로그</string>
|
||||
<string name="storage_clean_up">정리…</string>
|
||||
<string name="storage_session_running">세션을 중지해야 정리하거나 제거할 수 있습니다. 용량은 계속 표시됩니다.</string>
|
||||
<string name="storage_where">사용 내역</string>
|
||||
<string name="storage_games">Steam 게임</string>
|
||||
<string name="storage_game_data">게임 데이터</string>
|
||||
<string name="storage_game_data_hint">프리픽스 %1$s · 워크숍 %2$s · 셰이더 %3$s</string>
|
||||
<string name="storage_tools">Proton과 런타임</string>
|
||||
<string name="storage_tools_unused">%1$s 사용 안 함</string>
|
||||
<string name="storage_stores">GOG, Epic 및 기타 스토어</string>
|
||||
<string name="storage_stores_hint">스토어에서 제거</string>
|
||||
<string name="storage_desktop">데스크톱 앱</string>
|
||||
<string name="storage_desktop_hint">Flatpak 앱과 런타임</string>
|
||||
<string name="storage_system">Linux 시스템과 Steam 클라이언트</string>
|
||||
<string name="storage_system_hint">무엇이든 실행하는 데 필요</string>
|
||||
<string name="storage_app_data">DroidDeck 데이터</string>
|
||||
<string name="storage_app_data_hint">오래된 세션 로그 %1$s · 캐시 · 드라이버</string>
|
||||
<string name="storage_legend_games">게임</string>
|
||||
<string name="storage_legend_data">게임 데이터</string>
|
||||
<string name="storage_legend_tools">Proton과 런타임</string>
|
||||
<string name="storage_legend_stores">기타 스토어</string>
|
||||
<string name="storage_legend_desktop">데스크톱 앱</string>
|
||||
<string name="storage_legend_system">Linux 시스템</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">정리</string>
|
||||
<string name="storage_clean_up_lede">설치된 게임이나 진행 중인 다운로드에는 영향을 주지 않습니다.</string>
|
||||
<string name="storage_nothing_to_clean">지금은 정리할 항목이 없습니다.</string>
|
||||
<string name="storage_selected">선택: %1$s · %2$s → 사용 가능 %3$s</string>
|
||||
<string name="storage_cleaning">정리 중…</string>
|
||||
<string name="storage_free_button">%1$s 확보</string>
|
||||
<string name="storage_freed">%1$s 확보됨.</string>
|
||||
<string name="storage_item_runtimes">사용하지 않는 Steam 런타임과 Proton</string>
|
||||
<string name="storage_item_leftovers">제거한 게임의 잔여물</string>
|
||||
<string name="storage_item_leftovers_unsure">저장 파일이 있을 수 있는 잔여물</string>
|
||||
<string name="storage_item_logs">오래된 세션 로그</string>
|
||||
<string name="storage_item_logs_detail">현재 세션의 로그는 유지됩니다</string>
|
||||
<string name="storage_item_custom_detail">어떤 게임도 사용하지 않습니다. 사용하려면 구성 요소에서 다시 설치하세요.</string>
|
||||
<string name="storage_item_shaders">설치된 게임의 셰이더 캐시</string>
|
||||
<string name="storage_item_shaders_detail">플레이하면 다시 생성됩니다. 게임마다 첫 몇 분은 다시 끊깁니다.</string>
|
||||
<string name="storage_tag_safe">안전</string>
|
||||
<string name="storage_tag_unused">사용 안 함</string>
|
||||
<string name="storage_tag_stutter">끊김</string>
|
||||
<string name="storage_tag_saves">저장 파일 확인</string>
|
||||
<string name="storage_tag_default">기본</string>
|
||||
<string name="storage_tag_required">필수</string>
|
||||
<string name="storage_tag_not_used">사용 안 함</string>
|
||||
<string name="storage_back">저장 공간</string>
|
||||
<string name="storage_games_with_data">%1$s 및 데이터</string>
|
||||
<string name="storage_sort_largest">큰 순</string>
|
||||
<string name="storage_sort_least_played">적게 플레이한 순</string>
|
||||
<string name="storage_sort_sd">SD 카드에 있음</string>
|
||||
<string name="storage_no_games">여기에 게임이 없습니다.</string>
|
||||
<string name="storage_played">플레이 %1$s</string>
|
||||
<string name="storage_never_played">플레이한 적 없음</string>
|
||||
<string name="storage_game_parts">게임 %1$s · 데이터 %2$s</string>
|
||||
<string name="storage_uninstall">제거</string>
|
||||
<string name="storage_games_note">제거하면 Steam 기본 대화 상자가 열려 Steam에 기록됩니다. 게임을 SD 카드로 옮기려면 Steam › 설정 › 저장 공간을 사용하세요.</string>
|
||||
<string name="storage_uninstall_title">%1$s 제거할까요?</string>
|
||||
<string name="storage_uninstall_body">Steam이 열리고 확인을 요청합니다. 이 게임의 %1$s이 확보됩니다. 프리픽스는 잔여물을 정리할 때까지 남고, Steam 클라우드 저장 파일은 계정에 남습니다.</string>
|
||||
<string name="storage_open_steam">Steam 열기</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s 사용 안 함</string>
|
||||
<string name="storage_remove_unused">사용 안 함 제거 · %1$s</string>
|
||||
<string name="storage_in_use">사용 중</string>
|
||||
<string name="storage_not_used">사용 안 함</string>
|
||||
<string name="storage_remove">제거</string>
|
||||
<string name="storage_remove_title">%1$s 제거할까요?</string>
|
||||
<string name="storage_remove_body">%1$s이 확보됩니다. 나중에 게임이 필요로 하면 Steam이 다시 내려받습니다.</string>
|
||||
<string name="storage_tool_fex_runtime">Linux x86 게임에 필요(FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">ARM64 Proton에 필요</string>
|
||||
<string name="storage_tool_redist">게임이 요청한 VC++ 및 DirectX 설치 관리자</string>
|
||||
<string name="storage_tool_fex">Linux x86 게임 실행</string>
|
||||
<string name="storage_tool_steam">Steam 도구</string>
|
||||
<string name="storage_tool_old_runtime">DroidDeck은 이 런타임 없이 게임을 실행할 수 있습니다</string>
|
||||
<string name="storage_tool_x86_proton">x86 Proton이라 DroidDeck에서 실행할 수 없습니다</string>
|
||||
<string name="storage_tool_custom_default">모든 게임의 기본값</string>
|
||||
<string name="storage_tool_custom_unused">어떤 게임도 사용하지 않습니다</string>
|
||||
<string name="storage_tool_custom_unknown">Steam 설정을 읽을 수 없어 유지합니다</string>
|
||||
<string name="storage_cleanup_busy">저장 공간 정리가 끝날 때까지 기다리세요</string>
|
||||
<string name="storage_low_title">%1$s만 남았습니다</string>
|
||||
<string name="storage_low_body">Steam은 업데이트와 셰이더 캐시에 공간이 필요합니다. 10 GB 미만이면 게임 업데이트가 중간에 실패할 수 있습니다.</string>
|
||||
<string name="storage_low_safe">%1$s 안전하게 확보 가능.</string>
|
||||
<string name="storage_free_up">공간 확보</string>
|
||||
<string name="storage_play_anyway">그래도 플레이</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d개 설치됨</item>
|
||||
<item quantity="other">%d개 설치됨</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d개 SD 카드에 있음</item>
|
||||
<item quantity="other">%d개 SD 카드에 있음</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">도구 %d개</item>
|
||||
<item quantity="other">도구 %d개</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">게임 %d개의 빈 프리픽스 폴더와 셰이더 캐시</item>
|
||||
<item quantity="other">게임 %d개의 빈 프리픽스 폴더와 셰이더 캐시</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">게임 %d개의 프리픽스에 파일이 남아 있고 로컬 저장 파일이 있을 수 있습니다. 남기려면 먼저 백업하세요.</item>
|
||||
<item quantity="other">게임 %d개의 프리픽스에 파일이 남아 있고 로컬 저장 파일이 있을 수 있습니다. 남기려면 먼저 백업하세요.</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">게임 %d개</item>
|
||||
<item quantity="other">게임 %d개</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">DroidDeck 기본 Proton</string>
|
||||
<string name="storage_tool_proton">DroidDeck의 Proton 11 옵션</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">게임 %d개 사용</item>
|
||||
<item quantity="other">게임 %d개 사용</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">구성 요소</string>
|
||||
<string name="storage_components">구성 요소</string>
|
||||
<string name="storage_item_components">사용하지 않는 구성 요소</string>
|
||||
<string name="storage_component_original">원본 %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">%1$s이 확보됩니다. 더 이상 설치되지 않은 Proton 버전만 되돌립니다.</string>
|
||||
<string name="storage_leftovers_unchecked">확인하지 않음: Steam 앱 매니페스트를 읽을 수 없어 읽을 수 있을 때까지 제거한 게임의 프리픽스를 유지합니다.</string>
|
||||
<string name="storage_failed">완료하지 못했습니다: %1$s</string>
|
||||
<string name="storage_desktop_shared">공유 %1$s</string>
|
||||
<string name="storage_data_installed">설치된 게임</string>
|
||||
<string name="storage_data_parts">프리픽스 %1$s · 셰이더 %2$s · 워크숍 %3$s</string>
|
||||
<string name="storage_clear_shaders">셰이더 지우기</string>
|
||||
<string name="storage_data_leftovers">제거한 게임</string>
|
||||
<string name="storage_steam_app">Steam 앱 %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">프리픽스에 파일이 남아 있고 로컬 저장 파일이 있을 수 있습니다</string>
|
||||
<string name="storage_leftover_confirm">Wine 프리픽스와 셰이더 캐시(%1$s)를 삭제합니다. 필요한 저장 파일은 먼저 백업하세요. Steam 클라우드 저장 파일은 계정에 남습니다.</string>
|
||||
<string name="storage_data_other">유지</string>
|
||||
<string name="storage_data_other_title">Steam이 아닌 게임과 Steam의 공유 프리픽스</string>
|
||||
<string name="storage_data_other_detail">스토어에서 추가했거나 직접 추가한 게임의 프리픽스</string>
|
||||
<string name="storage_components_packages">내려받은 패키지</string>
|
||||
<string name="storage_components_originals">저장된 원본 파일</string>
|
||||
<string name="storage_components_none">내려받은 구성 요소 패키지가 없습니다.</string>
|
||||
<string name="storage_component_in_use">Proton이나 게임에서 선택됨</string>
|
||||
<string name="storage_component_unused">어디에서도 선택되지 않음</string>
|
||||
<string name="storage_original_kept">설치된 Proton 복원</string>
|
||||
<string name="storage_original_old">더 이상 설치되지 않은 Proton 버전</string>
|
||||
<string name="storage_tag_in_use">사용 중</string>
|
||||
<string name="storage_components_note">사용 중인 구성 요소는 잠깁니다. 구성 요소에서 선택하세요. 여기에서 제거한 항목은 거기에서 다시 내려받을 수 있습니다.</string>
|
||||
<string name="storage_component_confirm">%1$s이 확보됩니다. 직접 가져온 패키지는 다시 내려받을 수 없습니다.</string>
|
||||
<string name="storage_desktop_detail">Flatpak 앱과 공유 콘텐츠에 사용되는 %1$s</string>
|
||||
<string name="storage_desktop_apps_title">앱</string>
|
||||
<string name="storage_desktop_none">설치된 Flatpak 앱이 없습니다.</string>
|
||||
<string name="storage_desktop_shared_title">앱 공유</string>
|
||||
<string name="storage_desktop_runtimes">런타임</string>
|
||||
<string name="storage_desktop_runtimes_detail">앱이 의존하는 라이브러리이며, 사용하지 않는 것은 사용 안 함 제거로 정리됩니다</string>
|
||||
<string name="storage_desktop_cache">다운로드 저장소</string>
|
||||
<string name="storage_desktop_cache_detail">Flatpak 객체 저장소와 캐시된 다운로드</string>
|
||||
<string name="storage_desktop_prune">사용 안 함 제거</string>
|
||||
<string name="storage_desktop_prune_body">설치된 앱이 필요로 하지 않는 런타임을 제거하고 Flatpak 객체 저장소를 정리합니다. 설치된 앱은 그대로 동작합니다.</string>
|
||||
<string name="storage_flatpak_uninstall_body">이 앱(%1$s)과 다른 앱이 사용하지 않는 런타임을 제거합니다. 홈 디렉터리의 데이터는 유지됩니다.</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">패키지 %d개</item>
|
||||
<item quantity="other">패키지 %d개</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">앱 %d개</item>
|
||||
<item quantity="other">앱 %d개</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">부팅 동영상</string>
|
||||
<string name="boot_movie_video">동영상</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d fps</string>
|
||||
<string name="boot_movie_note">60 fps는 더 부드럽지만 Steam이 시작되는 동안 CPU를 더 사용합니다. Steam 자체 설정에서 고른 시작 동영상이 우선합니다.</string>
|
||||
<string name="boot_movie_sound">소리</string>
|
||||
<string name="boot_movie_custom">내 동영상</string>
|
||||
<string name="boot_movie_custom_hint">MP4, MOV, MKV 또는 WebM. 다른 형식과 %1$d MB를 넘는 파일은 기기에서 720p WebM으로 변환됩니다(최대 %2$d초).</string>
|
||||
<string name="boot_movie_import">가져오기</string>
|
||||
<string name="boot_movie_remove">삭제</string>
|
||||
<string name="boot_movie_pick">부팅 동영상 선택</string>
|
||||
<string name="boot_movie_imported">%1$s이(가) 이제 부팅 동영상입니다</string>
|
||||
<string name="boot_movie_imported_silent">%1$s이(가) 이제 부팅 동영상입니다(소리 없음)</string>
|
||||
<string name="boot_movie_too_long">변환이 필요한 동영상은 %1$d초 이하여야 합니다</string>
|
||||
<string name="boot_movie_converting">변환 중 %1$d%%</string>
|
||||
<string name="boot_movie_converting_started">Steam용으로 %1$s 변환 중입니다. DroidDeck을 열어 두세요.</string>
|
||||
<string name="boot_movie_unsupported">이 기기에서 해당 동영상을 디코딩할 수 없습니다.</string>
|
||||
<string name="boot_movie_import_failed">동영상을 가져오지 못했습니다</string>
|
||||
</resources>
|
||||
@@ -278,6 +278,9 @@
|
||||
<string name="mode_suspend_steam_note">Auto menjeda dan menyambung semula apabila dipaparkan. Asli menyediakan Steam untuk tidur dahulu. Manual menunggu Sambung. Jangan sekali-kali terus berjalan.</string>
|
||||
<string name="mode_background_downloads">Teruskan muat turun</string>
|
||||
<string name="mode_background_downloads_hint">Lambatkan penjedaan latar belakang semasa Steam aktif memuat turun.</string>
|
||||
<string name="mode_zram">Pemampatan zRAM</string>
|
||||
<string name="mode_zram_hint">Semasa bermain, memori klien Steam dimampatkan ke zram peranti supaya permainan mendapat lebih RAM. Berkuat kuasa pada permulaan Steam seterusnya.</string>
|
||||
<string name="mode_zram_unsupported">Tidak tersedia: peranti ini tiada zram atau swap, atau kernelnya lebih lama daripada 5.4.</string>
|
||||
<string name="mode_suspend_note">Auto menjeda di latar belakang dan menyambung semula apabila dipaparkan. Manual menunggu Sambung. Jangan sekali-kali terus berjalan.</string>
|
||||
<string name="mode_startup">Permulaan</string>
|
||||
<string name="mode_steam_startup">Mulakan Steam bersama DroidDeck</string>
|
||||
@@ -573,6 +576,10 @@
|
||||
<string name="drawer_cas">Ketajaman (CAS)</string>
|
||||
<string name="drawer_cas_level">Tahap ketajaman</string>
|
||||
<string name="drawer_fake_hdr">HDR palsu</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">Lancarkan SteamVR</string>
|
||||
<string name="steamvr_checking">Menyemak SteamVR…</string>
|
||||
<string name="drawer_deband">Pengurangan jalur</string>
|
||||
<string name="drawer_deband_strength">Kekuatan pengurangan jalur</string>
|
||||
<string name="drawer_brightness">Kecerahan</string>
|
||||
@@ -590,6 +597,11 @@
|
||||
<string name="drawer_texture_sharpness_note">Pincang mip negatif: tekstur lebih tajam, sedikit kilauan. Auto sepadan dengan sejauh mana sesi dibesarkan ke skrin.</string>
|
||||
<string name="drawer_touch_auto">Auto (%1$s)</string>
|
||||
<string name="drawer_keyboard">Papan kekunci</string>
|
||||
<string name="drawer_latency">Kependaman</string>
|
||||
<string name="drawer_low_latency_controller">Pengawal kependaman rendah</string>
|
||||
<string name="drawer_low_latency_controller_hint">Pengawal melapor pada 1000 Hz dan bukan 250 Hz. Sedikit lebih banyak CPU.</string>
|
||||
<string name="drawer_low_latency_touch">Sentuh kependaman rendah</string>
|
||||
<string name="drawer_low_latency_touch_hint">Sentuhan melangkau pengumpulan bingkai, jadi kawalan sentuh Steam berjalan pada 1000 Hz. Sedikit lebih banyak CPU.</string>
|
||||
<string name="drawer_pc_keyboard">Papan kekunci PC</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">Skrin kedua</string>
|
||||
@@ -680,6 +692,14 @@
|
||||
<string name="setup_saved_logs">Log sesi tersimpan</string>
|
||||
<string name="setup_saved_logs_hint">Menyimpan %1$d yang terbaharu</string>
|
||||
<string name="setup_clear_logs">Kosongkan log</string>
|
||||
<string name="setup_reset_settings">Set semula semua tetapan</string>
|
||||
<string name="setup_reset_settings_hint">Kembalikan setiap tetapan kepada nilai lalai</string>
|
||||
<string name="setup_reset_settings_button">Set semula</string>
|
||||
<string name="setup_reset_settings_title">Set semula semua tetapan?</string>
|
||||
<string name="setup_reset_settings_body">Tetapan paparan, prestasi, pengawal, tema, bahasa dan pelancar kembali kepada lalai. Permainan, log masuk Steam, akaun kedai, pemacu yang dimuat turun dan folder storan kekal.</string>
|
||||
<string name="setup_reset_settings_profiles">Set semula profil permainan juga (Proton, FEX, komponen dan persekitaran runtime bagi setiap permainan, serta persekitaran profil Lalai)</string>
|
||||
<string name="setup_reset_settings_session_running">Hentikan sesi sebelum menetap semula tetapan.</string>
|
||||
<string name="setup_reset_settings_partial">Tetapan telah ditetap semula, tetapi bukan semuanya. Butirannya ada dalam log.</string>
|
||||
<string name="setup_offline">Mod luar talian</string>
|
||||
<string name="setup_sign_in_first">Log masuk ke Steam dahulu</string>
|
||||
<string name="setup_launcher">Pelancar</string>
|
||||
@@ -1003,7 +1023,6 @@
|
||||
<string name="gpu_set_with_runtime">Ditetapkan bersama pemacu masa jalan: %1$s membawa kedua-duanya.</string>
|
||||
<string name="gpu_runtime_hint">Digunakan oleh permainan dan desktop.</string>
|
||||
<string name="gpu_runtime_default">Lalai masa jalan</string>
|
||||
<string name="gpu_display_hint">Melukis bingkai ke skrin. Berkuat kuasa selepas DroidDeck dimulakan semula.</string>
|
||||
<string name="gpu_auto_by_gpu">Auto - dipilih mengikut GPU</string>
|
||||
<string name="gpu_manage">Muat turun, import atau padam…</string>
|
||||
<string name="gpu_in_use_caps">SEDANG DIGUNAKAN</string>
|
||||
@@ -1229,10 +1248,16 @@
|
||||
<string name="drivers_downloading_pair">Memuat turun %1$s %2$s…</string>
|
||||
<string name="drivers_updated_to">Dikemas kini ke %1$s %2$s.</string>
|
||||
<string name="drivers_updated_to_restart">Dikemas kini ke %1$s %2$s. Pemacu paparan berkuat kuasa selepas DroidDeck dimulakan semula.</string>
|
||||
<string name="drivers_auto_waits_for_session">Kemas kini pemacu menunggu sesi berhenti.</string>
|
||||
<string name="drivers_auto_waits_for_network">%1$s %2$s akan dimuat turun selepas anda keluar daripada data mudah alih dan penjimat bateri.</string>
|
||||
<string name="drivers_skipping_bad">%1$s %2$s tidak memulakan sesi di sini, jadi Automatik kekal dengan pemacu yang berjaya.</string>
|
||||
<string name="drivers_rolled_back">%1$s gagal memulakan sesi dua kali, jadi Automatik kembali kepada pemacu yang berfungsi.</string>
|
||||
<string name="drivers_switched_to">Ditukar ke %1$s %2$s.</string>
|
||||
<string name="drivers_switched_to_restart">Ditukar ke %1$s %2$s. Pemacu paparan berkuat kuasa selepas DroidDeck dimulakan semula.</string>
|
||||
<string name="drivers_using">Menggunakan %1$s %2$s.</string>
|
||||
<string name="drivers_using_restart">Menggunakan %1$s %2$s. Pemacu paparan berkuat kuasa selepas DroidDeck dimulakan semula.</string>
|
||||
<string name="drivers_using_quest">Pemacu paparan bawaan Quest dan runtime lalai sedang digunakan.</string>
|
||||
<string name="drivers_using_quest_restart">Pemacu paparan bawaan Quest dan runtime lalai sedang digunakan. Pemacu paparan berkuat kuasa selepas DroidDeck dimulakan semula.</string>
|
||||
<string name="drivers_not_a_driver">Bukan zip pemacu: bukan pemacu AdrenoTools, Turnip -Linux, atau gabungan Android + Linux</string>
|
||||
<string name="drivers_import_failed">Import gagal: %1$s</string>
|
||||
<string name="drivers_imported_unnamed">Pemacu diimport</string>
|
||||
@@ -1643,7 +1668,7 @@
|
||||
<string name="adbpair_done_text">Ketik untuk kembali ke DroidDeck. Anda boleh matikan Debugging Wayarles sekarang.</string>
|
||||
<string name="adbpair_title_failed">Persediaan Debugging Wayarles terhenti</string>
|
||||
|
||||
<string name="selfinst_http">Muat turun menjawab HTTP %1$d</string>
|
||||
<string name="selfinst_download_failed">Muat turun gagal</string>
|
||||
<string name="selfinst_size_mismatch">Saiz muat turun tidak sepadan dengan APK yang diterbitkan dan telah dibuang - cuba lagi</string>
|
||||
<string name="selfinst_checksum_mismatch">Muat turun tidak sepadan dengan checksumnya dan telah dibuang - cuba lagi</string>
|
||||
<string name="selfinst_wrong_package">Kemas kini ini untuk pakej Android %1$s, bukan %2$s</string>
|
||||
@@ -1741,7 +1766,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">Had kadar GitHub dicapai - cuba lagi kemudian</string>
|
||||
<string name="turnip_github_http">GitHub menjawab HTTP %1$d</string>
|
||||
<string name="turnip_download_http">muat turun menjawab HTTP %1$d</string>
|
||||
<string name="turnip_download_failed">Muat turun gagal</string>
|
||||
<string name="turnip_list_predates_checksums">Senarai pemacu ini mendahului checksum - segarkan dan cuba lagi</string>
|
||||
<string name="turnip_checksum_mismatch">Checksum tidak sepadan - muat turun telah dibuang</string>
|
||||
<string name="turnip_not_adrenotools">Bukan pemacu AdrenoTools: %1$s</string>
|
||||
@@ -2010,6 +2035,9 @@
|
||||
<string name="stores_install_title">Pasang %1$s:</string>
|
||||
<string name="stores_target_internal">Dalaman</string>
|
||||
<string name="stores_target_free">%1$s bebas</string>
|
||||
<string name="stores_target_free_unknown">Ruang kosong tidak diketahui</string>
|
||||
<string name="stores_storage_insufficient">Ruang kosong tidak cukup: perlu %1$s, hanya %2$s tersedia</string>
|
||||
<string name="stores_storage_low">Ruang semakin sedikit (%1$s kosong, perlu %2$s)</string>
|
||||
<string name="stores_notification_channel">Muat turun kedai</string>
|
||||
<string name="stores_notification_channel_hint">Memaparkan kemajuan muat turun permainan dan mengekalkan muat turun berjalan di latar belakang</string>
|
||||
<string name="stores_notification_one">Memuat turun</string>
|
||||
@@ -2045,6 +2073,21 @@
|
||||
<string name="edit_other_exe">Exe lain…</string>
|
||||
<string name="edit_remove">Alih keluar</string>
|
||||
<string name="art_cover">Kulit</string>
|
||||
<string name="art_wide_cover">Kulit lebar</string>
|
||||
<string name="art_adjust_logo">Laras logo</string>
|
||||
<string name="logo_pos_default">Lalai</string>
|
||||
<string name="logo_pos_upper_left">Kiri atas</string>
|
||||
<string name="logo_pos_upper_center">Tengah atas</string>
|
||||
<string name="logo_pos_center">Tengah</string>
|
||||
<string name="logo_pos_bottom_left">Kiri bawah</string>
|
||||
<string name="logo_pos_bottom_center">Tengah bawah</string>
|
||||
<string name="logo_pos_position">Kedudukan</string>
|
||||
<string name="logo_pos_size">Saiz</string>
|
||||
<string name="logo_pos_save">Simpan</string>
|
||||
<string name="logo_pos_reset">Kembali kepada lalai</string>
|
||||
<string name="logo_pos_hint">Dilukis di atas latar belakang permainan, seperti yang dibuat oleh Steam. Kedudukan tepatnya mungkin berbeza beberapa piksel.</string>
|
||||
<string name="logo_pos_no_logo">Permainan ini belum ada logo. Pilih satu di Logo untuk melihatnya di sini.</string>
|
||||
<string name="logo_pos_failed">Kedudukan logo tidak dapat disimpan. Cuba lagi.</string>
|
||||
<string name="art_background">Latar belakang</string>
|
||||
<string name="art_logo">Logo</string>
|
||||
<string name="art_icon">Ikon</string>
|
||||
@@ -2057,6 +2100,17 @@
|
||||
<string name="art_pick_image">Pilih imej…</string>
|
||||
<string name="art_pick_title">Pilih imej: %1$s</string>
|
||||
<string name="art_reset">Tetapkan semula ke automatik</string>
|
||||
<string name="art_search_sgdb">Cari di SteamGridDB…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB: %1$s</string>
|
||||
<string name="art_sgdb_search_label">Cari permainan</string>
|
||||
<string name="art_sgdb_other_games">Hasil lain</string>
|
||||
<string name="art_sgdb_none">Tiada hasil.</string>
|
||||
<string name="art_sgdb_no_key">SteamGridDB memerlukan kunci API. Tambah milik anda di Tetapan › Pelancar.</string>
|
||||
<string name="art_sgdb_bad_key">SteamGridDB menolak kunci API. Semak di Tetapan › Pelancar.</string>
|
||||
<string name="art_sgdb_limit">SteamGridDB sedang mengehadkan permintaan. Cuba sebentar lagi.</string>
|
||||
<string name="art_sgdb_offline">SteamGridDB tidak dapat dihubungi. Semak sambungan.</string>
|
||||
<string name="art_sgdb_server">Ada masalah di SteamGridDB. Cuba kemudian.</string>
|
||||
<string name="art_sgdb_no_game">SteamGridDB tiada hasil untuk permainan ini. Cuba cari sendiri.</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="other">%1$d permainan ditambah</item>
|
||||
</plurals>
|
||||
@@ -2082,6 +2136,45 @@
|
||||
<string name="gpu_bar_auto">Auto · %1$s</string>
|
||||
<string name="gpu_separately">Pemacu masa jalan dan paparan secara berasingan</string>
|
||||
<string name="gpu_use_title">Gunakan %1$s?</string>
|
||||
<string name="gpu_mode_title">Cara pemacu dipilih</string>
|
||||
<string name="gpu_mode_auto_detail">Pasangan terbaik untuk GPU ini, sentiasa dikemas kini</string>
|
||||
<string name="gpu_mode_pair">Pasangan tetap</string>
|
||||
<string name="gpu_mode_pair_detail">Pilih pasangan; menetapkan kedua-dua pemacu sekali</string>
|
||||
<string name="gpu_mode_mix">Campur bebas</string>
|
||||
<string name="gpu_mode_mix_detail">Pilih paparan dan runtime secara berasingan</string>
|
||||
<string name="gpu_mode_pair_short">Pasangan</string>
|
||||
<string name="gpu_mode_mix_short">Campur</string>
|
||||
<string name="gpu_src_auto">AUTO</string>
|
||||
<string name="gpu_src_pair">PASANGAN</string>
|
||||
<string name="gpu_src_bundle">BUNDEL</string>
|
||||
<string name="gpu_src_chosen">DIPILIH</string>
|
||||
<string name="gpu_src_by_gpu">MENGIKUT GPU</string>
|
||||
<string name="gpu_src_default">LALAI</string>
|
||||
<string name="gpu_display_live">Sedang melukis</string>
|
||||
<string name="gpu_display_pending">Perlu mula semula · sehingga itu %1$s yang melukis</string>
|
||||
<string name="gpu_runtime_live">Aktif</string>
|
||||
<string name="gpu_runtime_pending">Sesi seterusnya · sesi ini menggunakan %1$s</string>
|
||||
<string name="gpu_display_next">Melukis skrin. Berkuat kuasa mulai sesi seterusnya.</string>
|
||||
<string name="gpu_previous_driver">pemacu sebelum ini</string>
|
||||
<string name="gpu_restart_banner">Mula semula untuk menukar pemacu paparan.</string>
|
||||
<string name="gpu_restart_blocked">Hentikan sesi semasa dan mula semula DroidDeck untuk menukar pemacu paparan.</string>
|
||||
<string name="gpu_restart_now">Mula semula sekarang</string>
|
||||
<string name="gpu_restart_later">Kemudian</string>
|
||||
<string name="gpu_auto_title">Automatik memilih pasangan ini untuk %1$s anda</string>
|
||||
<string name="gpu_auto_builtin_title">Automatik menggunakan pemacu terbina dalam untuk %1$s anda</string>
|
||||
<string name="gpu_auto_reason">Dipilih untuk keluarga %1$s. Versi baharu dipasang di latar belakang.</string>
|
||||
<string name="gpu_pair_title">Pilih pasangan</string>
|
||||
<string name="gpu_pair_hint">Pasangan menetapkan pemacu paparan dan pemacu runtime serentak.</string>
|
||||
<string name="gpu_mix_title">Pilih setiap pemacu</string>
|
||||
<string name="gpu_mix_hint">Bundel mengisi kedua-duanya. Perubahan paparan berkuat kuasa selepas mula semula, perubahan runtime mulai sesi seterusnya.</string>
|
||||
<string name="gpu_change_display">Tukar pemacu paparan</string>
|
||||
<string name="gpu_change_runtime">Tukar pemacu runtime</string>
|
||||
<string name="gpu_autos_pick">PILIHAN AUTOMATIK</string>
|
||||
<string name="gpu_installed">Dipasang</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · mula semula untuk menggunakan</string>
|
||||
<string name="gpu_chip_restart">%1$s · perlu mula semula</string>
|
||||
<string name="comp_this_device">Peranti ini</string>
|
||||
<string name="proton_remove_title">Alih keluar %1$s?</string>
|
||||
<string name="setup_tab_diagnostics">Diagnostik</string>
|
||||
<string name="setup_ready">Sedia untuk bermain</string>
|
||||
@@ -2093,6 +2186,14 @@
|
||||
<string name="setup_group_look">Rupa</string>
|
||||
<string name="setup_group_device">Peranti ini</string>
|
||||
<string name="setup_group_art">Seni pustaka</string>
|
||||
<string name="setup_group_download">Muat turun</string>
|
||||
<string name="setup_download_source">Sumber</string>
|
||||
<string name="setup_download_source_auto">Automatik</string>
|
||||
<string name="setup_download_source_direct">Terus</string>
|
||||
<string name="setup_download_source_mirror">Cermin</string>
|
||||
<string name="setup_download_source_custom">Awalan sendiri</string>
|
||||
<string name="setup_download_mirror">Laman</string>
|
||||
<string name="setup_download_prefix">Awalan</string>
|
||||
<string name="flathub_eyebrow">APL LINUX</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">Apl desktop untuk desktop Linux, dibuka daripada Desktop.</string>
|
||||
@@ -2198,12 +2299,9 @@
|
||||
<string name="cardboard_recenter_button">Pusatkan semula</string>
|
||||
<string name="cardboard_steamvr">SteamVR (eksperimen)</string>
|
||||
<string name="cardboard_steamvr_install">Pasang</string>
|
||||
<string name="cardboard_steamvr_install_hint">Memuat turun SteamVR ARM64 daripada Valve (571 MB).</string>
|
||||
<string name="cardboard_steamvr_installing">Memuat turun dan menyahmampat SteamVR…</string>
|
||||
<string name="cardboard_steamvr_start">Mula</string>
|
||||
<string name="cardboard_steamvr_ready_hint">Menjalankan SteamVR dengan kepala anda sebagai set kepala; gambar bertukar kepada 3D sebelah-menyebelah.</string>
|
||||
<string name="cardboard_steamvr_stop">Henti</string>
|
||||
<string name="cardboard_steamvr_running_hint">SteamVR sedang berjalan.</string>
|
||||
<string name="cardboard_steamvr_unavailable">Tidak tersedia dalam sesi ini.</string>
|
||||
<string name="cardboard_steamvr_failed">SteamVR tidak memberi respons; semak log sesi.</string>
|
||||
<string name="drawer_component_downloads">Muat turun komponen</string>
|
||||
@@ -2217,4 +2315,195 @@
|
||||
<string name="stores_hltb_completionist">Lengkap</string>
|
||||
<string name="stores_hltb_hours">%1$s j</string>
|
||||
<string name="stores_hltb_minutes">%1$d min</string>
|
||||
<string name="display_picture_position">Kedudukan gambar</string>
|
||||
<string name="display_position_top">Atas</string>
|
||||
<string name="display_position_bottom">Bawah</string>
|
||||
<string name="display_position_center">Tengah (lalai)</string>
|
||||
<string name="setup_tab_storage">Storan</string>
|
||||
<string name="storage_measuring">Mengukur storan…</string>
|
||||
<string name="storage_measuring_hint">Mengambil beberapa saat pada kali pertama.</string>
|
||||
<string name="storage_internal">Storan dalaman</string>
|
||||
<string name="storage_used_of">%1$s daripada %2$s digunakan</string>
|
||||
<string name="storage_free">%1$s kosong</string>
|
||||
<string name="storage_droiddeck_uses">DroidDeck menggunakan %1$s. Diukur pada %2$s.</string>
|
||||
<string name="storage_rescanning">mengimbas semula…</string>
|
||||
<string name="storage_other_apps">Aplikasi Android lain</string>
|
||||
<string name="storage_safe_now">Boleh dikosongkan sekarang</string>
|
||||
<string name="storage_safe_hint">Runtime tidak digunakan, sisa, log lama</string>
|
||||
<string name="storage_clean_up">Membersihkan…</string>
|
||||
<string name="storage_session_running">Hentikan sesi untuk membersihkan atau memadam apa-apa. Saiz kekal kelihatan.</string>
|
||||
<string name="storage_where">Ke mana perginya</string>
|
||||
<string name="storage_games">Permainan Steam</string>
|
||||
<string name="storage_game_data">Data permainan</string>
|
||||
<string name="storage_game_data_hint">Awalan %1$s · workshop %2$s · shader %3$s</string>
|
||||
<string name="storage_tools">Proton dan runtime</string>
|
||||
<string name="storage_tools_unused">%1$s tidak digunakan</string>
|
||||
<string name="storage_stores">GOG, Epic dan kedai lain</string>
|
||||
<string name="storage_stores_hint">Alih keluar di Kedai</string>
|
||||
<string name="storage_desktop">Aplikasi desktop</string>
|
||||
<string name="storage_desktop_hint">Aplikasi dan runtime Flatpak</string>
|
||||
<string name="storage_system">Sistem Linux dan klien Steam</string>
|
||||
<string name="storage_system_hint">Diperlukan untuk apa-apa berjalan</string>
|
||||
<string name="storage_app_data">Data DroidDeck</string>
|
||||
<string name="storage_app_data_hint">Log sesi lama %1$s · cache · pemacu</string>
|
||||
<string name="storage_legend_games">Permainan</string>
|
||||
<string name="storage_legend_data">Data permainan</string>
|
||||
<string name="storage_legend_tools">Proton dan runtime</string>
|
||||
<string name="storage_legend_stores">Kedai lain</string>
|
||||
<string name="storage_legend_desktop">Aplikasi desktop</string>
|
||||
<string name="storage_legend_system">Sistem Linux</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">Bersihkan</string>
|
||||
<string name="storage_clean_up_lede">Tiada apa di sini yang menyentuh permainan yang dipasang atau muat turun yang sedang berjalan.</string>
|
||||
<string name="storage_nothing_to_clean">Tiada apa untuk dibersihkan sekarang.</string>
|
||||
<string name="storage_selected">Dipilih: %1$s · %2$s → %3$s akan kosong</string>
|
||||
<string name="storage_cleaning">Membersihkan…</string>
|
||||
<string name="storage_free_button">Kosongkan %1$s</string>
|
||||
<string name="storage_freed">%1$s dikosongkan.</string>
|
||||
<string name="storage_item_runtimes">Runtime Steam dan Proton yang tidak digunakan</string>
|
||||
<string name="storage_item_leftovers">Sisa permainan yang dialih keluar</string>
|
||||
<string name="storage_item_leftovers_unsure">Sisa yang mungkin mengandungi simpanan</string>
|
||||
<string name="storage_item_logs">Log sesi lama</string>
|
||||
<string name="storage_item_logs_detail">Log sesi semasa kekal disimpan</string>
|
||||
<string name="storage_item_custom_detail">Tiada permainan menggunakannya. Pasang semula di Komponen jika perlu.</string>
|
||||
<string name="storage_item_shaders">Cache shader permainan yang dipasang</string>
|
||||
<string name="storage_item_shaders_detail">Dibina semula semasa bermain. Beberapa minit pertama setiap permainan akan tersekat-sekat semula.</string>
|
||||
<string name="storage_tag_safe">Selamat</string>
|
||||
<string name="storage_tag_unused">Tidak digunakan</string>
|
||||
<string name="storage_tag_stutter">Tersekat</string>
|
||||
<string name="storage_tag_saves">Semak simpanan</string>
|
||||
<string name="storage_tag_default">Lalai</string>
|
||||
<string name="storage_tag_required">Diperlukan</string>
|
||||
<string name="storage_tag_not_used">Tidak digunakan</string>
|
||||
<string name="storage_back">Storan</string>
|
||||
<string name="storage_games_with_data">%1$s berserta datanya</string>
|
||||
<string name="storage_sort_largest">Terbesar</string>
|
||||
<string name="storage_sort_least_played">Paling jarang dimainkan</string>
|
||||
<string name="storage_sort_sd">Pada kad SD</string>
|
||||
<string name="storage_no_games">Tiada permainan di sini.</string>
|
||||
<string name="storage_played">dimainkan %1$s</string>
|
||||
<string name="storage_never_played">Belum pernah dimainkan</string>
|
||||
<string name="storage_game_parts">Permainan %1$s · data %2$s</string>
|
||||
<string name="storage_uninstall">Nyahpasang</string>
|
||||
<string name="storage_games_note">Menyahpasang membuka tetingkap Steam supaya Steam merekodkannya. Untuk memindahkan permainan ke kad SD, gunakan Steam › Tetapan › Storan.</string>
|
||||
<string name="storage_uninstall_title">Nyahpasang %1$s?</string>
|
||||
<string name="storage_uninstall_body">Steam akan terbuka dan meminta pengesahan. Ini mengosongkan %1$s permainan. Awalan kekal sehingga sisa dibersihkan, dan simpanan Steam Cloud kekal dalam akaun anda.</string>
|
||||
<string name="storage_open_steam">Buka Steam</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s tidak digunakan</string>
|
||||
<string name="storage_remove_unused">Alih keluar yang tidak digunakan · %1$s</string>
|
||||
<string name="storage_in_use">Sedang digunakan</string>
|
||||
<string name="storage_not_used">Tidak digunakan</string>
|
||||
<string name="storage_remove">Alih keluar</string>
|
||||
<string name="storage_remove_title">Alih keluar %1$s?</string>
|
||||
<string name="storage_remove_body">Mengosongkan %1$s. Steam akan memuat turunnya semula jika ada permainan memerlukannya.</string>
|
||||
<string name="storage_tool_fex_runtime">Diperlukan untuk permainan Linux x86 (FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">Diperlukan untuk Proton ARM64</string>
|
||||
<string name="storage_tool_redist">Pemasangan VC++ dan DirectX yang diperlukan permainan</string>
|
||||
<string name="storage_tool_fex">Menjalankan permainan Linux x86</string>
|
||||
<string name="storage_tool_steam">Alat Steam</string>
|
||||
<string name="storage_tool_old_runtime">DroidDeck menjalankan permainan tanpa runtime ini</string>
|
||||
<string name="storage_tool_x86_proton">Proton x86, yang tidak dapat dijalankan oleh DroidDeck</string>
|
||||
<string name="storage_tool_custom_default">Lalai untuk semua permainan</string>
|
||||
<string name="storage_tool_custom_unused">Tiada permainan menggunakannya</string>
|
||||
<string name="storage_tool_custom_unknown">Tetapan Steam tidak dapat dibaca, jadi ini disimpan</string>
|
||||
<string name="storage_cleanup_busy">Tunggu pembersihan storan selesai</string>
|
||||
<string name="storage_low_title">Hanya %1$s kosong</string>
|
||||
<string name="storage_low_body">Steam memerlukan ruang untuk kemas kini dan cache shader. Bawah 10 GB, kemas kini permainan boleh gagal di tengah jalan.</string>
|
||||
<string name="storage_low_safe">%1$s boleh dikosongkan dengan selamat.</string>
|
||||
<string name="storage_free_up">Kosongkan ruang</string>
|
||||
<string name="storage_play_anyway">Main juga</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d dipasang</item>
|
||||
<item quantity="other">%d dipasang</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d pada kad SD</item>
|
||||
<item quantity="other">%d pada kad SD</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">%d alat</item>
|
||||
<item quantity="other">%d alat</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">Folder awalan dan cache shader kosong daripada %d permainan</item>
|
||||
<item quantity="other">Folder awalan dan cache shader kosong daripada %d permainan</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">Awalan %d permainan masih mengandungi fail, termasuk simpanan setempat. Salin dahulu jika perlu.</item>
|
||||
<item quantity="other">Awalan %d permainan masih mengandungi fail, termasuk simpanan setempat. Salin dahulu jika perlu.</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">%d permainan</item>
|
||||
<item quantity="other">%d permainan</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">Proton lalai DroidDeck</string>
|
||||
<string name="storage_tool_proton">Pilihan Proton 11 DroidDeck</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">digunakan %d permainan</item>
|
||||
<item quantity="other">digunakan %d permainan</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">Komponen</string>
|
||||
<string name="storage_components">Komponen</string>
|
||||
<string name="storage_item_components">Komponen yang tidak digunakan</string>
|
||||
<string name="storage_component_original">Asal %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">Mengosongkan %1$s. Hanya memulihkan versi Proton yang tidak dipasang lagi.</string>
|
||||
<string name="storage_leftovers_unchecked">Belum disemak: manifes aplikasi Steam tidak dapat dibaca, jadi awalan permainan yang dialih keluar kekal sehingga itu mungkin.</string>
|
||||
<string name="storage_failed">Gagal: %1$s</string>
|
||||
<string name="storage_desktop_shared">%1$s dikongsi</string>
|
||||
<string name="storage_data_installed">Permainan dipasang</string>
|
||||
<string name="storage_data_parts">Awalan %1$s · shader %2$s · workshop %3$s</string>
|
||||
<string name="storage_clear_shaders">Kosongkan shader</string>
|
||||
<string name="storage_data_leftovers">Permainan dialih keluar</string>
|
||||
<string name="storage_steam_app">Aplikasi Steam %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">Awalan masih mengandungi fail, mungkin simpanan setempat</string>
|
||||
<string name="storage_leftover_confirm">Memadam awalan Wine dan cache shader (%1$s). Salin dahulu simpanan penting; simpanan Steam Cloud kekal dalam akaun anda.</string>
|
||||
<string name="storage_data_other">Disimpan</string>
|
||||
<string name="storage_data_other_title">Permainan bukan Steam dan awalan Steam dikongsi</string>
|
||||
<string name="storage_data_other_detail">Awalan permainan daripada kedai atau yang ditambah sendiri</string>
|
||||
<string name="storage_components_packages">Pakej yang dimuat turun</string>
|
||||
<string name="storage_components_originals">Fail asal yang disimpan</string>
|
||||
<string name="storage_components_none">Tiada pakej komponen dimuat turun.</string>
|
||||
<string name="storage_component_in_use">Dipilih untuk Proton atau permainan</string>
|
||||
<string name="storage_component_unused">Tidak dipilih di mana-mana</string>
|
||||
<string name="storage_original_kept">Memulihkan Proton yang dipasang</string>
|
||||
<string name="storage_original_old">Versi Proton yang tidak dipasang lagi</string>
|
||||
<string name="storage_tag_in_use">Digunakan</string>
|
||||
<string name="storage_components_note">Komponen yang digunakan kekal dikunci. Pilih di Komponen; yang dialih keluar di sini boleh dimuat turun semula di sana.</string>
|
||||
<string name="storage_component_confirm">Mengosongkan %1$s. Pakej yang anda import sendiri tidak boleh dimuat turun semula.</string>
|
||||
<string name="storage_desktop_detail">%1$s untuk aplikasi Flatpak dan yang dikongsi</string>
|
||||
<string name="storage_desktop_apps_title">Aplikasi</string>
|
||||
<string name="storage_desktop_none">Tiada aplikasi Flatpak dipasang.</string>
|
||||
<string name="storage_desktop_shared_title">Dikongsi antara aplikasi</string>
|
||||
<string name="storage_desktop_runtimes">Runtime</string>
|
||||
<string name="storage_desktop_runtimes_detail">Pustaka tempat aplikasi berjalan; yang tidak digunakan hilang melalui Alih keluar yang tidak digunakan</string>
|
||||
<string name="storage_desktop_cache">Storan muat turun</string>
|
||||
<string name="storage_desktop_cache_detail">Object store dan cache muat turun Flatpak</string>
|
||||
<string name="storage_desktop_prune">Alih keluar yang tidak digunakan</string>
|
||||
<string name="storage_desktop_prune_body">Mengalih keluar runtime yang tidak diperlukan mana-mana aplikasi dipasang dan mengemas object store Flatpak. Aplikasi dipasang terus berjalan.</string>
|
||||
<string name="storage_flatpak_uninstall_body">Mengalih keluar aplikasi (%1$s) dan runtime yang tidak digunakan oleh yang lain. Data dalam folder rumah anda kekal.</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">%d pakej</item>
|
||||
<item quantity="other">%d pakej</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">%d aplikasi</item>
|
||||
<item quantity="other">%d aplikasi</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">Video but</string>
|
||||
<string name="boot_movie_video">Video</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d fps</string>
|
||||
<string name="boot_movie_note">60 fps kelihatan lebih lancar tetapi menggunakan lebih banyak CPU semasa Steam bermula. Video permulaan yang dipilih dalam tetapan Steam sendiri diutamakan.</string>
|
||||
<string name="boot_movie_sound">Bunyi</string>
|
||||
<string name="boot_movie_custom">Video anda sendiri</string>
|
||||
<string name="boot_movie_custom_hint">MP4, MOV, MKV atau WebM. Format lain dan fail melebihi %1$d MB ditukar kepada WebM 720p pada peranti, sehingga %2$d saat.</string>
|
||||
<string name="boot_movie_import">Import</string>
|
||||
<string name="boot_movie_remove">Alih keluar</string>
|
||||
<string name="boot_movie_pick">Pilih video but</string>
|
||||
<string name="boot_movie_imported">%1$s kini video but</string>
|
||||
<string name="boot_movie_imported_silent">%1$s kini video but, tanpa bunyi</string>
|
||||
<string name="boot_movie_too_long">Video yang perlu ditukar mestilah %1$d saat atau kurang</string>
|
||||
<string name="boot_movie_converting">Menukar %1$d%%</string>
|
||||
<string name="boot_movie_converting_started">Menukar %1$s untuk Steam. Biarkan DroidDeck terbuka.</string>
|
||||
<string name="boot_movie_unsupported">Peranti ini tidak dapat menyahkod video itu.</string>
|
||||
<string name="boot_movie_import_failed">Tidak dapat mengimport video itu</string>
|
||||
</resources>
|
||||
@@ -278,6 +278,9 @@
|
||||
<string name="mode_suspend_steam_note">Auto setter på pause og gjenopptar når den vises. Nativ forbereder Steam på hvilemodus først. Manuell venter på Gjenoppta. Aldri lar den fortsette å kjøre.</string>
|
||||
<string name="mode_background_downloads">La nedlastinger fortsette</string>
|
||||
<string name="mode_background_downloads_hint">Utsetter bakgrunnspause mens Steam laster aktivt ned.</string>
|
||||
<string name="mode_zram">zRAM-komprimering</string>
|
||||
<string name="mode_zram_hint">Mens et spill kjører, komprimeres minnet til Steam-klienten til enhetens zram, slik at spillet får mer RAM. Gjelder neste gang Steam starter.</string>
|
||||
<string name="mode_zram_unsupported">Ikke tilgjengelig: enheten har ingen zram eller swap, eller kjernen er eldre enn 5.4.</string>
|
||||
<string name="mode_suspend_note">Auto setter på pause i bakgrunnen og gjenopptar når den vises. Manuell venter på Gjenoppta. Aldri lar den fortsette å kjøre.</string>
|
||||
<string name="mode_startup">Oppstart</string>
|
||||
<string name="mode_steam_startup">Start Steam med DroidDeck</string>
|
||||
@@ -575,6 +578,10 @@
|
||||
<string name="drawer_cas">Skarphet (CAS)</string>
|
||||
<string name="drawer_cas_level">Skarphetsnivå</string>
|
||||
<string name="drawer_fake_hdr">Falsk HDR</string>
|
||||
<string name="vr_title">VR</string>
|
||||
<string name="steamvr_title">SteamVR</string>
|
||||
<string name="steamvr_enter">Start SteamVR</string>
|
||||
<string name="steamvr_checking">Sjekker SteamVR…</string>
|
||||
<string name="drawer_deband">Fargebåndfjerning</string>
|
||||
<string name="drawer_deband_strength">Styrke på fargebåndfjerning</string>
|
||||
<string name="drawer_brightness">Lysstyrke</string>
|
||||
@@ -592,6 +599,11 @@
|
||||
<string name="drawer_texture_sharpness_note">En negativ mip-skjevhet: skarpere teksturer, litt glitring. Auto følger hvor mye sesjonen forstørres til skjermen.</string>
|
||||
<string name="drawer_touch_auto">Auto (%1$s)</string>
|
||||
<string name="drawer_keyboard">Tastatur</string>
|
||||
<string name="drawer_latency">Forsinkelse</string>
|
||||
<string name="drawer_low_latency_controller">Kontroller med lav forsinkelse</string>
|
||||
<string name="drawer_low_latency_controller_hint">Kontrolleren rapporterer ved 1000 Hz i stedet for 250 Hz. Litt mer CPU.</string>
|
||||
<string name="drawer_low_latency_touch">Berøring med lav forsinkelse</string>
|
||||
<string name="drawer_low_latency_touch_hint">Berøringer hopper over bildeinnsamling; Steams berøringsstyring går ved 1000 Hz. Litt mer CPU.</string>
|
||||
<string name="drawer_pc_keyboard">PC-tastatur</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">Andre skjerm</string>
|
||||
@@ -683,6 +695,14 @@
|
||||
<string name="setup_saved_logs">Lagrede sesjonslogger</string>
|
||||
<string name="setup_saved_logs_hint">Beholder de %1$d nyeste</string>
|
||||
<string name="setup_clear_logs">Tøm logger</string>
|
||||
<string name="setup_reset_settings">Tilbakestill alle innstillinger</string>
|
||||
<string name="setup_reset_settings_hint">Sett hver innstilling tilbake til standardverdien</string>
|
||||
<string name="setup_reset_settings_button">Tilbakestill</string>
|
||||
<string name="setup_reset_settings_title">Tilbakestille alle innstillinger?</string>
|
||||
<string name="setup_reset_settings_body">Innstillinger for bilde, ytelse, kontroller, tema, språk og launcher går tilbake til standard. Spillene dine, Steam-påloggingen, butikkontoene, nedlastede drivere og lagringsmappene beholdes.</string>
|
||||
<string name="setup_reset_settings_profiles">Tilbakestill også spillprofiler (Proton, FEX, komponenter og kjøretidsmiljø per spill, samt kjøretidsmiljøet for Standard-profilen)</string>
|
||||
<string name="setup_reset_settings_session_running">Stopp økten før du tilbakestiller innstillinger.</string>
|
||||
<string name="setup_reset_settings_partial">Innstillingene ble tilbakestilt, men ikke alle. Detaljene står i loggen.</string>
|
||||
<string name="setup_offline">Frakoblet modus</string>
|
||||
<string name="setup_sign_in_first">Logg inn på Steam først</string>
|
||||
<string name="setup_launcher">Launcher</string>
|
||||
@@ -843,7 +863,7 @@
|
||||
<string name="gate_eyebrow">Steam</string>
|
||||
<string name="gate_lede">Android stopper apper som starter mange bakgrunnsprosesser, og Steam starter dusinvis. Til dette endres, avsluttes spill uten en feilmelding.</string>
|
||||
<string name="gate_fix_for_me">Fiks det for meg</string>
|
||||
<string name="gate_toggle_steps">Slå på «Disable child process restrictions», og kom så tilbake. La Utvikleralternativer stå på, ellers slår bryteren seg av igjen.</string>
|
||||
<string name="gate_toggle_steps">Slå på «Slå av begrensninger for underordnede prosesser», og kom så tilbake. La Utvikleralternativer stå på, ellers slår bryteren seg av igjen.</string>
|
||||
<string name="gate_open_dev">Åpne Utvikleralternativer</string>
|
||||
<string name="gate_no_toggle">Denne Android-versjonen har ingen bryter for det, så DroidDeck endrer det via trådløs feilsøking. Tar omtrent ett minutt, ingen datamaskin nødvendig.</string>
|
||||
<string name="gate_needs_wifi">Trådløs feilsøking krever Wi-Fi. Hvilket nettverk som helst fungerer; det krever ikke internett.</string>
|
||||
@@ -1009,7 +1029,6 @@
|
||||
<string name="gpu_set_with_runtime">Angis sammen med kjøretidsdriveren: %1$s bærer begge.</string>
|
||||
<string name="gpu_runtime_hint">Brukes av spill og skrivebordet.</string>
|
||||
<string name="gpu_runtime_default">Kjøretidsstandard</string>
|
||||
<string name="gpu_display_hint">Tegner bilder til skjermen. Gjelder etter at DroidDeck starter på nytt.</string>
|
||||
<string name="gpu_auto_by_gpu">Auto – valgt etter GPU</string>
|
||||
<string name="gpu_manage">Last ned, importer eller slett…</string>
|
||||
<string name="gpu_in_use_caps">I BRUK</string>
|
||||
@@ -1239,10 +1258,16 @@
|
||||
<string name="drivers_downloading_pair">Laster ned %1$s %2$s…</string>
|
||||
<string name="drivers_updated_to">Oppdatert til %1$s %2$s.</string>
|
||||
<string name="drivers_updated_to_restart">Oppdatert til %1$s %2$s. Skjermdriveren gjelder etter at DroidDeck starter på nytt.</string>
|
||||
<string name="drivers_auto_waits_for_session">Driveroppdateringer venter til økten stoppes.</string>
|
||||
<string name="drivers_auto_waits_for_network">%1$s %2$s lastes ned når du går av mobildata og batterisparing.</string>
|
||||
<string name="drivers_skipping_bad">%1$s %2$s startet ingen økt her, så Automatisk holder seg til driverne som gjorde det.</string>
|
||||
<string name="drivers_rolled_back">%1$s startet ingen økt to ganger, så Automatisk gikk tilbake til driverne som virket.</string>
|
||||
<string name="drivers_switched_to">Byttet til %1$s %2$s.</string>
|
||||
<string name="drivers_switched_to_restart">Byttet til %1$s %2$s. Skjermdriveren gjelder etter at DroidDeck starter på nytt.</string>
|
||||
<string name="drivers_using">Bruker %1$s %2$s.</string>
|
||||
<string name="drivers_using_restart">Bruker %1$s %2$s. Skjermdriveren gjelder etter at DroidDeck starter på nytt.</string>
|
||||
<string name="drivers_using_quest">Den medfølgende bildedriveren for Quest og standard kjøretidsmiljø brukes.</string>
|
||||
<string name="drivers_using_quest_restart">Den medfølgende bildedriveren for Quest og standard kjøretidsmiljø brukes. Bildedriveren gjelder etter en omstart av DroidDeck.</string>
|
||||
<string name="drivers_not_a_driver">Ikke en driver-zip: verken en AdrenoTools-driver, en -Linux Turnip eller en Android + Linux-pakke</string>
|
||||
<string name="drivers_import_failed">Import mislyktes: %1$s</string>
|
||||
<string name="drivers_imported_unnamed">Importert driver</string>
|
||||
@@ -1664,7 +1689,7 @@
|
||||
<string name="adbpair_done_text">Trykk for å gå tilbake til DroidDeck. Du kan slå av trådløs feilsøking nå.</string>
|
||||
<string name="adbpair_title_failed">Oppsettet for trådløs feilsøking stoppet</string>
|
||||
|
||||
<string name="selfinst_http">Nedlastingen svarte HTTP %1$d</string>
|
||||
<string name="selfinst_download_failed">Nedlastingen mislyktes</string>
|
||||
<string name="selfinst_size_mismatch">Nedlastingens størrelse stemte ikke med den publiserte APK-en og ble forkastet – prøv igjen</string>
|
||||
<string name="selfinst_checksum_mismatch">Nedlastingen stemte ikke med sjekksummen og ble forkastet – prøv igjen</string>
|
||||
<string name="selfinst_wrong_package">Oppdateringen er for Android-pakken %1$s, ikke %2$s</string>
|
||||
@@ -1762,7 +1787,7 @@
|
||||
|
||||
<string name="turnip_rate_limited">GitHubs hastighetsgrense ble nådd – prøv igjen senere</string>
|
||||
<string name="turnip_github_http">GitHub svarte HTTP %1$d</string>
|
||||
<string name="turnip_download_http">nedlastingen svarte HTTP %1$d</string>
|
||||
<string name="turnip_download_failed">Nedlastingen mislyktes</string>
|
||||
<string name="turnip_list_predates_checksums">Denne driverlisten er eldre enn sjekksummene – oppdater den og prøv igjen</string>
|
||||
<string name="turnip_checksum_mismatch">Sjekksummen stemmer ikke – nedlastingen ble forkastet</string>
|
||||
<string name="turnip_not_adrenotools">Ikke en AdrenoTools-driver: %1$s</string>
|
||||
@@ -1799,7 +1824,7 @@
|
||||
<string name="phantom_instructions_fix">%1$s Hvis det ikke er mulig, koble denne enheten til en datamaskin med ADB og kjør kommandoen under. Kom tilbake hit og kontroller igjen.</string>
|
||||
<string name="phantom_instructions_off">Steam-sesjoner kan starte.</string>
|
||||
<string name="phantom_instructions_not_applicable">Denne Android-versjonen bruker ikke denne grensen.</string>
|
||||
<string name="phantom_fix_toggle">Slå på «Disable child process restrictions» i Utvikleralternativer, og la Utvikleralternativer stå på etterpå.</string>
|
||||
<string name="phantom_fix_toggle">Slå på «Slå av begrensninger for underordnede prosesser» i Utvikleralternativer, og la Utvikleralternativer stå på etterpå.</string>
|
||||
<string name="phantom_fix_no_toggle">Denne Android-versjonen har ingen bryter for det. Bruk trådløs feilsøking for å endre det fra denne enheten.</string>
|
||||
<string name="upd_block_no_apk">Denne builden har ingen nedlasting for denne kopien av DroidDeck.</string>
|
||||
<string name="upd_block_version_code">Android vil ikke installere den over denne builden fordi dens versionCode %1$d er under den installerte %2$d.</string>
|
||||
@@ -2031,6 +2056,9 @@
|
||||
<string name="stores_install_title">Installer %1$s:</string>
|
||||
<string name="stores_target_internal">Intern</string>
|
||||
<string name="stores_target_free">%1$s ledig</string>
|
||||
<string name="stores_target_free_unknown">Ledig plass ukjent</string>
|
||||
<string name="stores_storage_insufficient">For lite ledig plass: trenger %1$s, bare %2$s ledig</string>
|
||||
<string name="stores_storage_low">For lite plass (%1$s ledig, %2$s trengs)</string>
|
||||
<string name="stores_notification_channel">Butikknedlastinger</string>
|
||||
<string name="stores_notification_channel_hint">Viser nedlastingsfremgang for spill og holder nedlastinger i gang i bakgrunnen</string>
|
||||
<string name="stores_notification_one">Laster ned</string>
|
||||
@@ -2066,6 +2094,21 @@
|
||||
<string name="edit_other_exe">Annen exe…</string>
|
||||
<string name="edit_remove">Fjern</string>
|
||||
<string name="art_cover">Omslag</string>
|
||||
<string name="art_wide_cover">Bredt omslag</string>
|
||||
<string name="art_adjust_logo">Juster logo</string>
|
||||
<string name="logo_pos_default">Standard</string>
|
||||
<string name="logo_pos_upper_left">Øvre venstre</string>
|
||||
<string name="logo_pos_upper_center">Øvre midt</string>
|
||||
<string name="logo_pos_center">Midten</string>
|
||||
<string name="logo_pos_bottom_left">Nedre venstre</string>
|
||||
<string name="logo_pos_bottom_center">Nedre midt</string>
|
||||
<string name="logo_pos_position">Posisjon</string>
|
||||
<string name="logo_pos_size">Størrelse</string>
|
||||
<string name="logo_pos_save">Lagre</string>
|
||||
<string name="logo_pos_reset">Tilbake til standard</string>
|
||||
<string name="logo_pos_hint">Tegnes oppå spillets bakgrunn, slik Steam gjør. Den nøyaktige posisjonen kan avvike noen piksler.</string>
|
||||
<string name="logo_pos_no_logo">Dette spillet har ingen logo ennå. Velg en under Logo for å se den her.</string>
|
||||
<string name="logo_pos_failed">Logoposisjonen kunne ikke lagres. Prøv igjen.</string>
|
||||
<string name="art_background">Bakgrunn</string>
|
||||
<string name="art_logo">Logo</string>
|
||||
<string name="art_icon">Ikon</string>
|
||||
@@ -2078,6 +2121,17 @@
|
||||
<string name="art_pick_image">Velg bilde…</string>
|
||||
<string name="art_pick_title">Velg et bilde: %1$s</string>
|
||||
<string name="art_reset">Tilbakestill til automatisk</string>
|
||||
<string name="art_search_sgdb">Søk på SteamGridDB…</string>
|
||||
<string name="art_sgdb_title">SteamGridDB: %1$s</string>
|
||||
<string name="art_sgdb_search_label">Søk etter et spill</string>
|
||||
<string name="art_sgdb_other_games">Andre treff</string>
|
||||
<string name="art_sgdb_none">Ingen treff.</string>
|
||||
<string name="art_sgdb_no_key">SteamGridDB krever en API-nøkkel. Legg til din under Innstillinger › Launcher.</string>
|
||||
<string name="art_sgdb_bad_key">SteamGridDB godtok ikke API-nøkkelen. Sjekk den under Innstillinger › Launcher.</string>
|
||||
<string name="art_sgdb_limit">SteamGridDB begrenser antall forespørsler akkurat nå. Prøv igjen om et minutt.</string>
|
||||
<string name="art_sgdb_offline">Får ikke kontakt med SteamGridDB. Sjekk tilkoblingen.</string>
|
||||
<string name="art_sgdb_server">Noe gikk galt hos SteamGridDB. Prøv igjen senere.</string>
|
||||
<string name="art_sgdb_no_game">SteamGridDB har ingen treff for dette spillet. Prøv å søke på det.</string>
|
||||
<plurals name="games_added_n">
|
||||
<item quantity="one">%1$d spill lagt til</item>
|
||||
<item quantity="other">%1$d spill lagt til</item>
|
||||
@@ -2105,6 +2159,45 @@
|
||||
<string name="gpu_bar_auto">Auto · %1$s</string>
|
||||
<string name="gpu_separately">Kjøretids- og skjermdrivere separat</string>
|
||||
<string name="gpu_use_title">Bruke %1$s?</string>
|
||||
<string name="gpu_mode_title">Slik velges drivere</string>
|
||||
<string name="gpu_mode_auto_detail">Det beste paret for denne GPU-en, alltid oppdatert</string>
|
||||
<string name="gpu_mode_pair">Matchet par</string>
|
||||
<string name="gpu_mode_pair_detail">Velg et par; setter begge driverne</string>
|
||||
<string name="gpu_mode_mix">Fri blanding</string>
|
||||
<string name="gpu_mode_mix_detail">Velg bilde og kjøretidsmiljø hver for seg</string>
|
||||
<string name="gpu_mode_pair_short">Par</string>
|
||||
<string name="gpu_mode_mix_short">Blandet</string>
|
||||
<string name="gpu_src_auto">AUTO</string>
|
||||
<string name="gpu_src_pair">PAR</string>
|
||||
<string name="gpu_src_bundle">PAKKE</string>
|
||||
<string name="gpu_src_chosen">VALGT</string>
|
||||
<string name="gpu_src_by_gpu">PER GPU</string>
|
||||
<string name="gpu_src_default">STANDARD</string>
|
||||
<string name="gpu_display_live">Tegner nå</string>
|
||||
<string name="gpu_display_pending">Krever omstart · til da tegner %1$s</string>
|
||||
<string name="gpu_runtime_live">Aktiv</string>
|
||||
<string name="gpu_runtime_pending">Neste økt · denne beholder %1$s</string>
|
||||
<string name="gpu_display_next">Tegner skjermen. Gjelder fra neste økt.</string>
|
||||
<string name="gpu_previous_driver">forrige driver</string>
|
||||
<string name="gpu_restart_banner">Start på nytt for å bytte bildedriver.</string>
|
||||
<string name="gpu_restart_blocked">Stopp den gjeldende økten og start DroidDeck på nytt for å bytte bildedriver.</string>
|
||||
<string name="gpu_restart_now">Start på nytt nå</string>
|
||||
<string name="gpu_restart_later">Senere</string>
|
||||
<string name="gpu_auto_title">Automatisk valgte dette paret for din %1$s</string>
|
||||
<string name="gpu_auto_builtin_title">Automatisk bruker de innebygde driverne for din %1$s</string>
|
||||
<string name="gpu_auto_reason">Valgt for %1$s-familien. Nye versjoner installeres i bakgrunnen.</string>
|
||||
<string name="gpu_pair_title">Velg et par</string>
|
||||
<string name="gpu_pair_hint">Et par setter bildedriveren og kjøretidsdriveren sammen.</string>
|
||||
<string name="gpu_mix_title">Velg hver driver</string>
|
||||
<string name="gpu_mix_hint">En pakke fyller begge plassene. Bildeendringer gjelder etter en omstart, kjøretidsendringer fra neste økt.</string>
|
||||
<string name="gpu_change_display">Bytt bildedriver</string>
|
||||
<string name="gpu_change_runtime">Bytt kjøretidsdriver</string>
|
||||
<string name="gpu_autos_pick">AUTOMATISK VALG</string>
|
||||
<string name="gpu_installed">Installert</string>
|
||||
<string name="gpu_summary_mix">%1$s + %2$s</string>
|
||||
<string name="gpu_summary_restart">%1$s · start på nytt for å bruke</string>
|
||||
<string name="gpu_chip_restart">%1$s · krever omstart</string>
|
||||
<string name="comp_this_device">Denne enheten</string>
|
||||
<string name="proton_remove_title">Fjerne %1$s?</string>
|
||||
<string name="setup_tab_diagnostics">Diagnostikk</string>
|
||||
<string name="setup_ready">Klar til å spille</string>
|
||||
@@ -2116,6 +2209,14 @@
|
||||
<string name="setup_group_look">Utseende</string>
|
||||
<string name="setup_group_device">Denne enheten</string>
|
||||
<string name="setup_group_art">Biblioteksgrafikk</string>
|
||||
<string name="setup_group_download">Nedlasting</string>
|
||||
<string name="setup_download_source">Kilde</string>
|
||||
<string name="setup_download_source_auto">Automatisk</string>
|
||||
<string name="setup_download_source_direct">Direkte</string>
|
||||
<string name="setup_download_source_mirror">Speil</string>
|
||||
<string name="setup_download_source_custom">Eget prefiks</string>
|
||||
<string name="setup_download_mirror">Nettsted</string>
|
||||
<string name="setup_download_prefix">Prefiks</string>
|
||||
<string name="flathub_eyebrow">LINUX-APPER</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">Skrivebordsapper for Linux-skrivebordet, åpnet fra Skrivebord.</string>
|
||||
@@ -2223,12 +2324,9 @@
|
||||
<string name="cardboard_recenter_button">Sentrer</string>
|
||||
<string name="cardboard_steamvr">SteamVR (eksperimentell)</string>
|
||||
<string name="cardboard_steamvr_install">Installer</string>
|
||||
<string name="cardboard_steamvr_install_hint">Laster ned Valves SteamVR for ARM64 (571 MB).</string>
|
||||
<string name="cardboard_steamvr_installing">Laster ned og pakker ut SteamVR …</string>
|
||||
<string name="cardboard_steamvr_start">Start</string>
|
||||
<string name="cardboard_steamvr_ready_hint">Kjører SteamVR med hodet ditt som headset; bildet bytter til side-ved-side-3D.</string>
|
||||
<string name="cardboard_steamvr_stop">Stopp</string>
|
||||
<string name="cardboard_steamvr_running_hint">SteamVR kjører.</string>
|
||||
<string name="cardboard_steamvr_unavailable">Ikke tilgjengelig i denne økten.</string>
|
||||
<string name="cardboard_steamvr_failed">SteamVR svarte ikke; sjekk sesjonsloggen.</string>
|
||||
<string name="drawer_component_downloads">Last ned komponenter</string>
|
||||
@@ -2242,4 +2340,195 @@
|
||||
<string name="stores_hltb_completionist">Komplettering</string>
|
||||
<string name="stores_hltb_hours">%1$s t</string>
|
||||
<string name="stores_hltb_minutes">%1$d min</string>
|
||||
<string name="display_picture_position">Bildets posisjon</string>
|
||||
<string name="display_position_top">Øverst</string>
|
||||
<string name="display_position_bottom">Nederst</string>
|
||||
<string name="display_position_center">Midten (standard)</string>
|
||||
<string name="setup_tab_storage">Lagring</string>
|
||||
<string name="storage_measuring">Måler lagring…</string>
|
||||
<string name="storage_measuring_hint">Første gang tar noen sekunder.</string>
|
||||
<string name="storage_internal">Intern lagring</string>
|
||||
<string name="storage_used_of">%1$s av %2$s brukt</string>
|
||||
<string name="storage_free">%1$s ledig</string>
|
||||
<string name="storage_droiddeck_uses">DroidDeck bruker %1$s. Målt %2$s.</string>
|
||||
<string name="storage_rescanning">måler på nytt…</string>
|
||||
<string name="storage_other_apps">Andre Android-apper</string>
|
||||
<string name="storage_safe_now">Kan frigjøres nå</string>
|
||||
<string name="storage_safe_hint">Ubrukte kjøretidsmiljøer, rester, gamle logger</string>
|
||||
<string name="storage_clean_up">Rydder…</string>
|
||||
<string name="storage_session_running">Stopp økten for å rydde eller slette noe. Størrelsene vises fortsatt.</string>
|
||||
<string name="storage_where">Hvor det ender</string>
|
||||
<string name="storage_games">Steam-spill</string>
|
||||
<string name="storage_game_data">Spilldata</string>
|
||||
<string name="storage_game_data_hint">Prefikser %1$s · workshop %2$s · shaders %3$s</string>
|
||||
<string name="storage_tools">Proton og kjøretidsmiljøer</string>
|
||||
<string name="storage_tools_unused">%1$s ubrukt</string>
|
||||
<string name="storage_stores">GOG, Epic og andre butikker</string>
|
||||
<string name="storage_stores_hint">Fjern dem under Butikker</string>
|
||||
<string name="storage_desktop">Skrivebordsapper</string>
|
||||
<string name="storage_desktop_hint">Flatpak-apper og kjøretidsmiljøer</string>
|
||||
<string name="storage_system">Linux-systemet og Steam-klienten</string>
|
||||
<string name="storage_system_hint">Nødvendig for at noe skal kunne kjøre</string>
|
||||
<string name="storage_app_data">DroidDeck-data</string>
|
||||
<string name="storage_app_data_hint">Logger fra gamle økter %1$s · hurtiglager · drivere</string>
|
||||
<string name="storage_legend_games">Spill</string>
|
||||
<string name="storage_legend_data">Spilldata</string>
|
||||
<string name="storage_legend_tools">Proton og kjøretidsmiljøer</string>
|
||||
<string name="storage_legend_stores">Andre butikker</string>
|
||||
<string name="storage_legend_desktop">Skrivebordsapper</string>
|
||||
<string name="storage_legend_system">Linux-systemet</string>
|
||||
<string name="storage_legend_app">DroidDeck</string>
|
||||
<string name="storage_clean_up_title">Rydd</string>
|
||||
<string name="storage_clean_up_lede">Ingenting her rører installerte spill eller pågående nedlastinger.</string>
|
||||
<string name="storage_nothing_to_clean">Det er ingenting å rydde akkurat nå.</string>
|
||||
<string name="storage_selected">Valgt: %1$s · %2$s → %3$s frigjøres</string>
|
||||
<string name="storage_cleaning">Rydder…</string>
|
||||
<string name="storage_free_button">Frigjør %1$s</string>
|
||||
<string name="storage_freed">Frigjorde %1$s.</string>
|
||||
<string name="storage_item_runtimes">Ubrukte Steam-kjøretidsmiljøer og Proton</string>
|
||||
<string name="storage_item_leftovers">Rester etter slettede spill</string>
|
||||
<string name="storage_item_leftovers_unsure">Rester som kan inneholde lagrede spill</string>
|
||||
<string name="storage_item_logs">Logger fra gamle økter</string>
|
||||
<string name="storage_item_logs_detail">Logger fra denne økten beholdes</string>
|
||||
<string name="storage_item_custom_detail">Ingen spill bruker det. Installer det på nytt under Komponenter hvis du trenger det.</string>
|
||||
<string name="storage_item_shaders">Shadercache for installerte spill</string>
|
||||
<string name="storage_item_shaders_detail">Bygges opp igjen mens du spiller. De første minuttene av hvert spill hakker igjen.</string>
|
||||
<string name="storage_tag_safe">Trygt</string>
|
||||
<string name="storage_tag_unused">Ubrukt</string>
|
||||
<string name="storage_tag_stutter">Hakking</string>
|
||||
<string name="storage_tag_saves">Sjekk lagrede spill</string>
|
||||
<string name="storage_tag_default">Standard</string>
|
||||
<string name="storage_tag_required">Nødvendig</string>
|
||||
<string name="storage_tag_not_used">Ubrukt</string>
|
||||
<string name="storage_back">Lagring</string>
|
||||
<string name="storage_games_with_data">%1$s med dataene sine</string>
|
||||
<string name="storage_sort_largest">Størst</string>
|
||||
<string name="storage_sort_least_played">Minst spilt</string>
|
||||
<string name="storage_sort_sd">På SD-kort</string>
|
||||
<string name="storage_no_games">Det er ingen spill her.</string>
|
||||
<string name="storage_played">spilt %1$s</string>
|
||||
<string name="storage_never_played">Aldri spilt</string>
|
||||
<string name="storage_game_parts">Spill %1$s · data %2$s</string>
|
||||
<string name="storage_uninstall">Avinstaller</string>
|
||||
<string name="storage_games_note">Avinstallering åpner Steam-vinduet, slik at Steam holder orden. For å flytte et spill til SD-kortet bruker du Steam › Innstillinger › Lagring.</string>
|
||||
<string name="storage_uninstall_title">Avinstallere %1$s?</string>
|
||||
<string name="storage_uninstall_body">Steam åpnes og ber om bekreftelse. Det frigjør spillets %1$s. Prefikset blir til du rydder restene, og Steam Cloud-lagringer blir på kontoen.</string>
|
||||
<string name="storage_open_steam">Åpne Steam</string>
|
||||
<string name="storage_tools_detail">%1$s · %2$s ubrukt</string>
|
||||
<string name="storage_remove_unused">Fjern ubrukte · %1$s</string>
|
||||
<string name="storage_in_use">I bruk</string>
|
||||
<string name="storage_not_used">Ubrukt</string>
|
||||
<string name="storage_remove">Fjern</string>
|
||||
<string name="storage_remove_title">Fjerne %1$s?</string>
|
||||
<string name="storage_remove_body">Frigjør %1$s. Steam laster det ned igjen hvis et spill trenger det.</string>
|
||||
<string name="storage_tool_fex_runtime">Nødvendig for Linux x86-spill (FEX)</string>
|
||||
<string name="storage_tool_arm_runtime">Nødvendig for ARM64-Proton</string>
|
||||
<string name="storage_tool_redist">VC++- og DirectX-installasjoner spill krever</string>
|
||||
<string name="storage_tool_fex">Kjører Linux x86-spill</string>
|
||||
<string name="storage_tool_steam">Steam-verktøy</string>
|
||||
<string name="storage_tool_old_runtime">DroidDeck kjører spill uten dette kjøretidsmiljøet</string>
|
||||
<string name="storage_tool_x86_proton">Proton x86, som DroidDeck ikke kan kjøre</string>
|
||||
<string name="storage_tool_custom_default">Standard for alle spill</string>
|
||||
<string name="storage_tool_custom_unused">Ingen spill bruker det</string>
|
||||
<string name="storage_tool_custom_unknown">Steam-innstillingene kan ikke leses, så det beholdes</string>
|
||||
<string name="storage_cleanup_busy">Vent til lagringsoppryddingen er ferdig</string>
|
||||
<string name="storage_low_title">Bare %1$s ledig</string>
|
||||
<string name="storage_low_body">Steam trenger plass til oppdateringer og shadercache. Under 10 GB kan spilloppdateringer mislykkes halvveis.</string>
|
||||
<string name="storage_low_safe">%1$s kan frigjøres trygt.</string>
|
||||
<string name="storage_free_up">Frigjør plass</string>
|
||||
<string name="storage_play_anyway">Spill likevel</string>
|
||||
<plurals name="storage_games_installed">
|
||||
<item quantity="one">%d installert</item>
|
||||
<item quantity="other">%d installerte</item>
|
||||
</plurals>
|
||||
<plurals name="storage_games_on_sd">
|
||||
<item quantity="one">%d på SD-kort</item>
|
||||
<item quantity="other">%d på SD-kort</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tools_count">
|
||||
<item quantity="one">%d verktøy</item>
|
||||
<item quantity="other">%d verktøy</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_detail">
|
||||
<item quantity="one">Tomme prefiksmapper og shadercache fra %d spill</item>
|
||||
<item quantity="other">Tomme prefiksmapper og shadercache fra %d spill</item>
|
||||
</plurals>
|
||||
<plurals name="storage_item_leftovers_unsure_detail">
|
||||
<item quantity="one">Prefikset til %d spill inneholder fortsatt filer, blant annet lokale lagringer. Ta en kopi først hvis du trenger dem.</item>
|
||||
<item quantity="other">Prefiksene til %d spill inneholder fortsatt filer, blant annet lokale lagringer. Ta en kopi først hvis du trenger dem.</item>
|
||||
</plurals>
|
||||
<plurals name="storage_tag_games">
|
||||
<item quantity="one">%d spill</item>
|
||||
<item quantity="other">%d spill</item>
|
||||
</plurals>
|
||||
<string name="storage_tool_default_proton">DroidDecks standard-Proton</string>
|
||||
<string name="storage_tool_proton">DroidDecks Proton 11-alternativ</string>
|
||||
<plurals name="storage_tool_custom_games">
|
||||
<item quantity="one">Brukes av %d spill</item>
|
||||
<item quantity="other">Brukes av %d spill</item>
|
||||
</plurals>
|
||||
<string name="storage_legend_components">Komponenter</string>
|
||||
<string name="storage_components">Komponenter</string>
|
||||
<string name="storage_item_components">Ubrukte komponenter</string>
|
||||
<string name="storage_component_original">Original %1$s · %2$s</string>
|
||||
<string name="storage_original_confirm">Frigjør %1$s. Legger bare tilbake en Proton-versjon som ikke lenger er installert.</string>
|
||||
<string name="storage_leftovers_unchecked">Ikke sjekket: manifestet til en Steam-app kan ikke leses, så prefikser for slettede spill blir til det går.</string>
|
||||
<string name="storage_failed">Mislyktes: %1$s</string>
|
||||
<string name="storage_desktop_shared">%1$s delt</string>
|
||||
<string name="storage_data_installed">Installerte spill</string>
|
||||
<string name="storage_data_parts">Prefiks %1$s · shaders %2$s · workshop %3$s</string>
|
||||
<string name="storage_clear_shaders">Rydd shaders</string>
|
||||
<string name="storage_data_leftovers">Slettede spill</string>
|
||||
<string name="storage_steam_app">Steam-app %1$d</string>
|
||||
<string name="storage_leftover_unsure_detail">Prefikset inneholder fortsatt filer, kanskje lokale lagringer</string>
|
||||
<string name="storage_leftover_confirm">Rydder Wine-prefikset og shadercachen (%1$s). Ta først en kopi av lagringene du trenger; Steam Cloud-lagringer blir på kontoen.</string>
|
||||
<string name="storage_data_other">Beholdes</string>
|
||||
<string name="storage_data_other_title">Ikke-Steam-spill og delt Steam-prefiks</string>
|
||||
<string name="storage_data_other_detail">Prefikser for spill lagt til via butikker eller manuelt</string>
|
||||
<string name="storage_components_packages">Nedlastede pakker</string>
|
||||
<string name="storage_components_originals">Beholdte originaler</string>
|
||||
<string name="storage_components_none">Ingen komponentpakker lastet ned.</string>
|
||||
<string name="storage_component_in_use">Valgt for en Proton eller et spill</string>
|
||||
<string name="storage_component_unused">Ikke valgt noe sted</string>
|
||||
<string name="storage_original_kept">Legger tilbake den installerte Proton-en</string>
|
||||
<string name="storage_original_old">Proton-versjon som ikke lenger er installert</string>
|
||||
<string name="storage_tag_in_use">I bruk</string>
|
||||
<string name="storage_components_note">Komponenter i bruk er låst. Velg dem under Komponenter; det du fjerner her, kan lastes ned igjen der.</string>
|
||||
<string name="storage_component_confirm">Frigjør %1$s. Pakker du importerte selv, kan ikke lastes ned igjen.</string>
|
||||
<string name="storage_desktop_detail">%1$s for Flatpak-apper og det de deler</string>
|
||||
<string name="storage_desktop_apps_title">Apper</string>
|
||||
<string name="storage_desktop_none">Ingen Flatpak-apper installert.</string>
|
||||
<string name="storage_desktop_shared_title">Deles av appene</string>
|
||||
<string name="storage_desktop_runtimes">Kjøretidsmiljøer</string>
|
||||
<string name="storage_desktop_runtimes_detail">Bibliotekene appene kjører på; ubrukte forsvinner med Fjern ubrukte</string>
|
||||
<string name="storage_desktop_cache">Nedlastingslager</string>
|
||||
<string name="storage_desktop_cache_detail">Flatpaks objektlager og nedlastet hurtiglager</string>
|
||||
<string name="storage_desktop_prune">Fjern ubrukte</string>
|
||||
<string name="storage_desktop_prune_body">Fjerner kjøretidsmiljøer ingen installert app trenger, og rydder i Flatpaks objektlager. Installerte apper virker fortsatt.</string>
|
||||
<string name="storage_flatpak_uninstall_body">Fjerner appen (%1$s) og kjøretidsmiljøene ingen andre bruker. Dataene i hjemmappen din blir.</string>
|
||||
<plurals name="storage_components_count">
|
||||
<item quantity="one">%d pakke</item>
|
||||
<item quantity="other">%d pakker</item>
|
||||
</plurals>
|
||||
<plurals name="storage_desktop_apps">
|
||||
<item quantity="one">%d app</item>
|
||||
<item quantity="other">%d apper</item>
|
||||
</plurals>
|
||||
<string name="boot_movie">Oppstartsvideo</string>
|
||||
<string name="boot_movie_video">Video</string>
|
||||
<string name="boot_movie_droiddeck">DroidDeck · %1$d fps</string>
|
||||
<string name="boot_movie_note">60 fps ser jevnere ut, men bruker mer CPU mens Steam starter. En oppstartsvideo valgt i Steams egne innstillinger har forrang.</string>
|
||||
<string name="boot_movie_sound">Lyd</string>
|
||||
<string name="boot_movie_custom">Din egen video</string>
|
||||
<string name="boot_movie_custom_hint">MP4, MOV, MKV eller WebM. Andre formater og filer over %1$d MB konverteres til 720p-WebM på enheten, opptil %2$d sekunder.</string>
|
||||
<string name="boot_movie_import">Importer</string>
|
||||
<string name="boot_movie_remove">Fjern</string>
|
||||
<string name="boot_movie_pick">Velg en oppstartsvideo</string>
|
||||
<string name="boot_movie_imported">%1$s er nå oppstartsvideoen</string>
|
||||
<string name="boot_movie_imported_silent">%1$s er nå oppstartsvideoen, uten lyd</string>
|
||||
<string name="boot_movie_too_long">Videoer som må konverteres kan vare maks %1$d sekunder</string>
|
||||
<string name="boot_movie_converting">Konverterer %1$d %%</string>
|
||||
<string name="boot_movie_converting_started">Konverterer %1$s for Steam. Hold DroidDeck åpen.</string>
|
||||
<string name="boot_movie_unsupported">Enheten kan ikke dekode videoen.</string>
|
||||
<string name="boot_movie_import_failed">Kunne ikke importere videoen</string>
|
||||
</resources>
|
||||
Loaded 100 of 200 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user