mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 04:43:36 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 07b16c95af | |||
| 8a4172c14d | |||
| eeb0f64c8d | |||
| 5fcc6f2bbe | |||
| 3b1c3fc54b | |||
| 14052ef747 | |||
| 9a030a4e34 | |||
| 0ef4f59ce5 | |||
| b5163fba24 | |||
| 161e8ba847 | |||
| e421a2ccc5 | |||
| 4816844bf0 | |||
| 184e573caa | |||
| 9f035ea354 | |||
| 0a9699e158 | |||
| f903dd98f4 | |||
| 4cd4f25c36 | |||
| db02efeab5 | |||
| 8ff0f2ef24 | |||
| f73d623352 |
@@ -30,7 +30,7 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: useblacksmith/setup-docker-builder@v1
|
||||
|
||||
- name: Determine tags
|
||||
id: tags
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push multi-arch image
|
||||
uses: docker/build-push-action@v7
|
||||
uses: useblacksmith/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
@@ -79,13 +79,14 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ env.ALL_TAGS }}
|
||||
build-args: |
|
||||
BUILDKIT_INLINE_CACHE=1
|
||||
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
||||
labels: |
|
||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
org.opencontainers.image.created=${{ github.run_id }}
|
||||
outputs: type=registry,compression=gzip,compression-level=9
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
outputs: type=registry,compression=zstd
|
||||
|
||||
- name: Cleanup Docker
|
||||
if: always()
|
||||
|
||||
@@ -26,7 +26,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
runs-on: blacksmith-4vcpu-windows-2025
|
||||
if: (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == '') && github.event.inputs.artifact_destination != 'submit'
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -44,22 +44,7 @@ jobs:
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
$maxAttempts = 3
|
||||
$attempt = 1
|
||||
while ($attempt -le $maxAttempts) {
|
||||
try {
|
||||
npm ci
|
||||
break
|
||||
} catch {
|
||||
if ($attempt -eq $maxAttempts) {
|
||||
Write-Error "npm ci failed after $maxAttempts attempts"
|
||||
exit 1
|
||||
}
|
||||
Start-Sleep -Seconds 10
|
||||
$attempt++
|
||||
}
|
||||
}
|
||||
run: npm ci
|
||||
|
||||
- name: Get version
|
||||
id: package-version
|
||||
@@ -131,7 +116,7 @@ jobs:
|
||||
retention-days: 30
|
||||
|
||||
build-linux:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
if: (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == '') && github.event.inputs.artifact_destination != 'submit'
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -148,24 +133,14 @@ jobs:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install system dependencies for AppImage
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libfuse2
|
||||
sudo apt-get install -y libfuse2 flatpak flatpak-builder imagemagick
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
for i in 1 2 3;
|
||||
do
|
||||
if npm ci; then
|
||||
break
|
||||
else
|
||||
if [ $i -eq 3 ]; then
|
||||
exit 1
|
||||
fi
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
npm ci
|
||||
npm install --force @rollup/rollup-linux-x64-gnu
|
||||
npm install --force @rollup/rollup-linux-arm64-gnu
|
||||
npm install --force @rollup/rollup-linux-arm-gnueabihf
|
||||
@@ -267,11 +242,6 @@ jobs:
|
||||
path: release/termix_linux_armv7l_portable.tar.gz
|
||||
retention-days: 30
|
||||
|
||||
- name: Install Flatpak builder and dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y flatpak flatpak-builder imagemagick
|
||||
|
||||
- name: Add Flathub repository
|
||||
run: |
|
||||
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
@@ -353,7 +323,7 @@ jobs:
|
||||
retention-days: 30
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
runs-on: blacksmith-6vcpu-macos-latest
|
||||
if: (github.event.inputs.build_type == 'macos' || github.event.inputs.build_type == 'all') && github.event.inputs.artifact_destination != 'submit'
|
||||
needs: []
|
||||
permissions:
|
||||
@@ -373,17 +343,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
for i in 1 2 3;
|
||||
do
|
||||
if npm ci; then
|
||||
break
|
||||
else
|
||||
if [ $i -eq 3 ]; then
|
||||
exit 1
|
||||
fi
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
npm ci
|
||||
npm install --force @rollup/rollup-darwin-arm64
|
||||
npm install dmg-license
|
||||
|
||||
@@ -490,11 +450,11 @@ jobs:
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [ "${{ steps.check_certs.outputs.has_certs }}" != "true" ]; then
|
||||
npm run build
|
||||
fi
|
||||
export GH_TOKEN="${{ secrets.GITHUB_TOKEN }}"
|
||||
npx electron-builder --mac dmg --universal --x64 --arm64 --publish never
|
||||
|
||||
- name: Upload macOS MAS PKG
|
||||
@@ -583,7 +543,7 @@ jobs:
|
||||
security delete-keychain $RUNNER_TEMP/dev-signing.keychain-db || true
|
||||
|
||||
submit-to-chocolatey:
|
||||
runs-on: windows-latest
|
||||
runs-on: blacksmith-4vcpu-windows-2025
|
||||
if: github.event.inputs.artifact_destination == 'submit' && (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == '')
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -688,7 +648,7 @@ jobs:
|
||||
retention-days: 30
|
||||
|
||||
submit-to-flatpak:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
if: github.event.inputs.artifact_destination == 'submit' && (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == '')
|
||||
needs: []
|
||||
permissions:
|
||||
@@ -735,7 +695,7 @@ jobs:
|
||||
echo "appimage_arm64_name=$APPIMAGE_ARM64_NAME" >> $GITHUB_OUTPUT
|
||||
echo "checksum_arm64=$CHECKSUM_ARM64" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install ImageMagick for icon generation
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y imagemagick
|
||||
@@ -775,7 +735,7 @@ jobs:
|
||||
retention-days: 30
|
||||
|
||||
submit-to-homebrew:
|
||||
runs-on: macos-latest
|
||||
runs-on: blacksmith-6vcpu-macos-latest
|
||||
if: github.event.inputs.artifact_destination == 'submit' && (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'macos')
|
||||
needs: []
|
||||
permissions:
|
||||
@@ -844,7 +804,7 @@ jobs:
|
||||
retention-days: 30
|
||||
|
||||
upload-to-release:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
if: github.event.inputs.artifact_destination == 'release'
|
||||
needs: [build-windows, build-linux, build-macos]
|
||||
permissions:
|
||||
@@ -880,7 +840,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
submit-to-testflight:
|
||||
runs-on: macos-latest
|
||||
runs-on: blacksmith-6vcpu-macos-latest
|
||||
if: github.event.inputs.artifact_destination == 'submit' && (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'macos')
|
||||
needs: []
|
||||
permissions:
|
||||
@@ -900,17 +860,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
for i in 1 2 3;
|
||||
do
|
||||
if npm ci; then
|
||||
break
|
||||
else
|
||||
if [ $i -eq 3 ]; then
|
||||
exit 1
|
||||
fi
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
npm ci
|
||||
npm install --force @rollup/rollup-darwin-arm64
|
||||
npm install dmg-license
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
cask "termix" do
|
||||
version "2.2.1"
|
||||
sha256 "7fe7bde53df568c3212a212e6dc0ca4b77c24a3d3d9b740dddadcd765330e93d"
|
||||
version "2.3.0"
|
||||
sha256 "42a42ac42938e5929ee67c71f970a82e37058b26cdd50fb3503f54aea7085463"
|
||||
|
||||
url "https://github.com/Termix-SSH/Termix/releases/download/release-#{version}-tag/termix_macos_universal_dmg.dmg"
|
||||
name "Termix"
|
||||
|
||||
@@ -14,16 +14,10 @@
|
||||
<projectSourceUrl>https://github.com/Termix-SSH/Termix</projectSourceUrl>
|
||||
<docsUrl>https://docs.termix.site/install</docsUrl>
|
||||
<bugTrackerUrl>https://github.com/Termix-SSH/Support/issues</bugTrackerUrl>
|
||||
<tags>docker ssh self-hosted file-management ssh-tunnel termix server-management terminal</tags>
|
||||
<summary>Termix is a web-based server management platform with SSH terminal, tunneling, and file editing capabilities.</summary>
|
||||
<tags>docker ssh terminal telnet self-hosted rdp file-management vnc ssh-tunnel server-stats termix</tags>
|
||||
<summary>Self-hosted SSH and remote desktop management.</summary>
|
||||
<description>
|
||||
Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a web-based solution for managing your servers and infrastructure through a single, intuitive interface.
|
||||
|
||||
Termix offers:
|
||||
- SSH terminal access
|
||||
- SSH tunneling capabilities
|
||||
- Remote file management
|
||||
- Server monitoring and management
|
||||
Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a multi-platform solution for managing your servers and infrastructure through a single, intuitive interface. Termix offers SSH terminal access, remote desktop control (RDP, VNC, Telnet), SSH tunneling capabilities, remote file management, and many other tools. Termix is the perfect free and self-hosted alternative to Termius available for all platforms.
|
||||
|
||||
This package installs the desktop application version of Termix.
|
||||
</description>
|
||||
|
||||
@@ -138,7 +138,14 @@ nginx -c /tmp/nginx/nginx.conf
|
||||
# Inject runtime BASE_PATH into frontend if configured
|
||||
if [ -n "$BASE_PATH" ]; then
|
||||
echo "Injecting BASE_PATH: $BASE_PATH"
|
||||
find /app/html -name "index.html" -exec sed -i "s|window.__TERMIX_BASE_PATH__ = \"\"|window.__TERMIX_BASE_PATH__ = \"$BASE_PATH\"|g" {} \;
|
||||
# Strip trailing slash for use as a path prefix
|
||||
CLEAN_BASE_PATH="${BASE_PATH%/}"
|
||||
find /app/html -name "index.html" -exec sed -i "s|window.__TERMIX_BASE_PATH__ = \"\"|window.__TERMIX_BASE_PATH__ = \"$CLEAN_BASE_PATH\"|g" {} \;
|
||||
# Patch sw.js static asset paths with the base path prefix
|
||||
find /app/html -name "sw.js" -exec sed -i "s|__TERMIX_SW_BASE_PATH__|$CLEAN_BASE_PATH|g" {} \;
|
||||
else
|
||||
# No base path - replace placeholder with empty string so paths stay absolute from root
|
||||
find /app/html -name "sw.js" -exec sed -i "s|__TERMIX_SW_BASE_PATH__||g" {} \;
|
||||
fi
|
||||
|
||||
echo "Starting backend services..."
|
||||
|
||||
@@ -196,6 +196,24 @@ http {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ~ ^/open-tabs(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ~ ^/user-preferences(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ~ ^/database(/.*)?$ {
|
||||
client_max_body_size 5G;
|
||||
client_body_timeout 300s;
|
||||
|
||||
@@ -185,6 +185,24 @@ http {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ~ ^/open-tabs(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ~ ^/user-preferences(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ~ ^/database(/.*)?$ {
|
||||
client_max_body_size 5G;
|
||||
client_body_timeout 300s;
|
||||
|
||||
@@ -90,8 +90,8 @@
|
||||
"desktop": {
|
||||
"entry": {
|
||||
"Name": "Termix",
|
||||
"Comment": "A web-based server management platform",
|
||||
"Keywords": "terminal;ssh;server;management;",
|
||||
"Comment": "Self-hosted SSH and remote desktop management. ",
|
||||
"Keywords": "docker;ssh;terminal;telnet;self-hosted;rdp;file-management;vnc;ssh-tunnel;server-stats;termix",
|
||||
"StartupWMClass": "termix"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -933,6 +933,7 @@ function createWindow() {
|
||||
|
||||
mainWindow.once("ready-to-show", () => {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -2602,6 +2603,9 @@ app.on("window-all-closed", () => {
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
} else if (mainWindow) {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[Desktop Entry]
|
||||
Name=Termix
|
||||
Comment=Web-based server management platform with SSH terminal, tunneling, and file editing
|
||||
Comment=Self-hosted SSH and remote desktop management.
|
||||
Exec=run.sh %U
|
||||
Icon=com.karmaa.termix
|
||||
Terminal=false
|
||||
|
||||
@@ -5,7 +5,7 @@ Title=Termix - SSH Server Management Platform
|
||||
IsRuntime=false
|
||||
Url=https://github.com/Termix-SSH/Termix/releases/download/VERSION_PLACEHOLDER/termix_linux_flatpak.flatpak
|
||||
RuntimeRepo=https://flathub.org/repo/flathub.flatpakrepo
|
||||
Comment=Web-based server management platform with SSH terminal, tunneling, and file editing
|
||||
Description=Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides SSH terminal access, tunneling capabilities, and remote file management.
|
||||
Comment=Self-hosted SSH and remote desktop management.
|
||||
Description=Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a multi-platform solution for managing your servers and infrastructure through a single, intuitive interface. Termix offers SSH terminal access, remote desktop control (RDP, VNC, Telnet), SSH tunneling capabilities, remote file management, and many other tools. Termix is the perfect free and self-hosted alternative to Termius available for all platforms.
|
||||
Icon=https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/icon.png
|
||||
Homepage=https://github.com/Termix-SSH/Termix
|
||||
|
||||
@@ -1,77 +1,77 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop-application">
|
||||
<id>com.karmaa.termix</id>
|
||||
<name>Termix</name>
|
||||
<summary>Web-based server management platform with SSH terminal, tunneling, and file editing</summary>
|
||||
<id>com.karmaa.termix</id>
|
||||
<name>Termix</name>
|
||||
<summary>Self-hosted SSH and remote desktop management.</summary>
|
||||
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>Apache-2.0</project_license>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>Apache-2.0</project_license>
|
||||
|
||||
<developer_name>bugattiguy527</developer_name>
|
||||
<developer_name>bugattiguy527</developer_name>
|
||||
|
||||
<description>
|
||||
<p>
|
||||
Termix is an open-source, forever-free, self-hosted all-in-one server management platform.
|
||||
It provides a web-based solution for managing your servers and infrastructure through a single, intuitive interface.
|
||||
</p>
|
||||
<p>Features:</p>
|
||||
<ul>
|
||||
<li>SSH terminal access with full terminal emulation</li>
|
||||
<li>SSH tunneling capabilities for secure port forwarding</li>
|
||||
<li>Remote file management with editor support</li>
|
||||
<li>Server monitoring and management tools</li>
|
||||
<li>Self-hosted solution - keep your data private</li>
|
||||
<li>Modern, intuitive web interface</li>
|
||||
</ul>
|
||||
</description>
|
||||
<description>
|
||||
<p>
|
||||
Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a
|
||||
multi-platform solution for managing your servers and infrastructure through a single, intuitive interface.
|
||||
Termix offers SSH terminal access, remote desktop control (RDP, VNC, Telnet), SSH tunneling capabilities,
|
||||
remote file management, and many other tools. Termix is the perfect free and self-hosted alternative to
|
||||
Termius available for all platforms.
|
||||
</p>
|
||||
</description>
|
||||
|
||||
<launchable type="desktop-id">com.karmaa.termix.desktop</launchable>
|
||||
<launchable type="desktop-id">com.karmaa.termix.desktop</launchable>
|
||||
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/screenshots/terminal.png</image>
|
||||
<caption>SSH Terminal Interface</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/screenshots/terminal.png</image>
|
||||
<caption>SSH Terminal Interface</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
|
||||
<url type="homepage">https://github.com/Termix-SSH/Termix</url>
|
||||
<url type="bugtracker">https://github.com/Termix-SSH/Support/issues</url>
|
||||
<url type="help">https://docs.termix.site</url>
|
||||
<url type="vcs-browser">https://github.com/Termix-SSH/Termix</url>
|
||||
<url type="homepage">https://github.com/Termix-SSH/Termix</url>
|
||||
<url type="bugtracker">https://github.com/Termix-SSH/Support/issues</url>
|
||||
<url type="help">https://docs.termix.site</url>
|
||||
<url type="vcs-browser">https://github.com/Termix-SSH/Termix</url>
|
||||
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="social-info">moderate</content_attribute>
|
||||
</content_rating>
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="social-info">moderate</content_attribute>
|
||||
</content_rating>
|
||||
|
||||
<releases>
|
||||
<release version="VERSION_PLACEHOLDER" date="DATE_PLACEHOLDER">
|
||||
<description>
|
||||
<p>Latest release of Termix</p>
|
||||
</description>
|
||||
<url>https://github.com/Termix-SSH/Termix/releases</url>
|
||||
</release>
|
||||
</releases>
|
||||
<releases>
|
||||
<release version="VERSION_PLACEHOLDER" date="DATE_PLACEHOLDER">
|
||||
<description>
|
||||
<p>Latest release of Termix</p>
|
||||
</description>
|
||||
<url>https://github.com/Termix-SSH/Termix/releases</url>
|
||||
</release>
|
||||
</releases>
|
||||
|
||||
<categories>
|
||||
<category>Development</category>
|
||||
<category>Network</category>
|
||||
<category>System</category>
|
||||
</categories>
|
||||
<categories>
|
||||
<category>Development</category>
|
||||
<category>Network</category>
|
||||
<category>System</category>
|
||||
</categories>
|
||||
|
||||
<keywords>
|
||||
<keyword>ssh</keyword>
|
||||
<keyword>terminal</keyword>
|
||||
<keyword>server</keyword>
|
||||
<keyword>management</keyword>
|
||||
<keyword>tunnel</keyword>
|
||||
<keyword>file-manager</keyword>
|
||||
</keywords>
|
||||
<keywords>
|
||||
<keyword>docker</keyword>
|
||||
<keyword>ssh</keyword>
|
||||
<keyword>terminal</keyword>
|
||||
<keyword>telnet</keyword>
|
||||
<keyword>self-hosted</keyword>
|
||||
<keyword>rdp</keyword>
|
||||
<keyword>file-management</keyword>
|
||||
<keyword>vnc</keyword>
|
||||
<keyword>ssh-tunnel</keyword>
|
||||
<keyword>server-stats</keyword>
|
||||
<keyword>termix</keyword>
|
||||
|
||||
<provides>
|
||||
<binary>termix</binary>
|
||||
</provides>
|
||||
</keywords>
|
||||
|
||||
<requires>
|
||||
<internet>always</internet>
|
||||
</requires>
|
||||
<provides>
|
||||
<binary>termix</binary>
|
||||
</provides>
|
||||
|
||||
<requires>
|
||||
<internet>always</internet>
|
||||
</requires>
|
||||
</component>
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<meta name="theme-color" content="#09090b" />
|
||||
@@ -12,8 +12,8 @@
|
||||
content="black-translucent"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-title" content="Termix" />
|
||||
<link rel="apple-touch-icon" href="/icons/512x512.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" href="icons/512x512.png" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<title>Termix</title>
|
||||
<style>
|
||||
.hide-scrollbar {
|
||||
|
||||
Generated
+83
-32
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "termix",
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "termix",
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@tanstack/react-virtual": "^3.13.26",
|
||||
@@ -33,6 +33,7 @@
|
||||
"socks": "^2.8.7",
|
||||
"speakeasy": "^2.0.0",
|
||||
"ssh2": "^1.17.0",
|
||||
"swagger-jsdoc": "^6.3.0",
|
||||
"undici": "^7.0.0",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
@@ -143,7 +144,6 @@
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz",
|
||||
"integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15",
|
||||
@@ -160,7 +160,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
|
||||
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -170,14 +169,12 @@
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
|
||||
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-parser": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz",
|
||||
"integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": "14.0.1",
|
||||
@@ -2520,7 +2517,6 @@
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
|
||||
"integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -6127,7 +6123,6 @@
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jsonwebtoken": {
|
||||
@@ -6844,7 +6839,6 @@
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -6861,7 +6855,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
|
||||
"integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.5.0"
|
||||
@@ -7246,7 +7239,6 @@
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
@@ -7452,7 +7444,6 @@
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
@@ -7674,7 +7665,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
||||
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
@@ -8356,7 +8346,6 @@
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
@@ -8644,6 +8633,18 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"esutils": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dot-case": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
|
||||
@@ -9498,7 +9499,6 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -9640,7 +9640,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
@@ -9667,7 +9666,6 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -9864,7 +9862,6 @@
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
@@ -10832,14 +10829,12 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
|
||||
"integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^9.0.0"
|
||||
@@ -10945,7 +10940,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
@@ -11561,6 +11555,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.mergewith": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
|
||||
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
@@ -12803,7 +12803,6 @@
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
@@ -12828,7 +12827,6 @@
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -13259,7 +13257,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/pako": {
|
||||
@@ -13349,7 +13346,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -13359,7 +13355,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
|
||||
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.0.0",
|
||||
@@ -13376,7 +13371,6 @@
|
||||
"version": "11.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz",
|
||||
"integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
@@ -14555,7 +14549,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -14866,7 +14859,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
@@ -14879,7 +14871,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -14974,7 +14965,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -15386,6 +15376,68 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/swagger-jsdoc": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.3.0.tgz",
|
||||
"integrity": "sha512-I+iQjVGV3t28pOkQUJv2MncthvOtkEactOn8R76SvSYhxgtIn7FoqfDHwQaN+GBnQdXQLrhgDXseKitmJcHMsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^12.1.0",
|
||||
"commander": "6.2.0",
|
||||
"doctrine": "3.0.0",
|
||||
"glob": "11.1.0",
|
||||
"lodash.mergewith": "^4.6.2",
|
||||
"yaml": "2.0.0-1"
|
||||
},
|
||||
"bin": {
|
||||
"swagger-jsdoc": "bin/swagger-jsdoc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-jsdoc/node_modules/commander": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
|
||||
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-jsdoc/node_modules/glob": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
|
||||
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.1.1",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-jsdoc/node_modules/yaml": {
|
||||
"version": "2.0.0-1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
|
||||
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
|
||||
@@ -16184,7 +16236,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
|
||||
+3
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "termix",
|
||||
"private": true,
|
||||
"version": "2.3.0",
|
||||
"description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
|
||||
"version": "2.3.1",
|
||||
"description": "Self-hosted SSH and remote desktop management.",
|
||||
"author": "Karmaa",
|
||||
"main": "electron/main.cjs",
|
||||
"engines": {
|
||||
@@ -61,6 +61,7 @@
|
||||
"socks": "^2.8.7",
|
||||
"speakeasy": "^2.0.0",
|
||||
"ssh2": "^1.17.0",
|
||||
"swagger-jsdoc": "^6.3.0",
|
||||
"undici": "^7.0.0",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Termix",
|
||||
"short_name": "Termix",
|
||||
"description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
|
||||
"description": "Self-hosted SSH and remote desktop management.",
|
||||
"theme_color": "#09090b",
|
||||
"background_color": "#09090b",
|
||||
"display": "standalone",
|
||||
|
||||
+6
-5
@@ -1,10 +1,11 @@
|
||||
const CACHE_NAME = "termix-static-v2";
|
||||
const BASE_PATH = "__TERMIX_SW_BASE_PATH__";
|
||||
const STATIC_ASSETS = [
|
||||
"/favicon.ico",
|
||||
"/icons/48x48.png",
|
||||
"/icons/128x128.png",
|
||||
"/icons/256x256.png",
|
||||
"/icons/512x512.png",
|
||||
`${BASE_PATH}/favicon.ico`,
|
||||
`${BASE_PATH}/icons/48x48.png`,
|
||||
`${BASE_PATH}/icons/128x128.png`,
|
||||
`${BASE_PATH}/icons/256x256.png`,
|
||||
`${BASE_PATH}/icons/512x512.png`,
|
||||
];
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
|
||||
@@ -17,22 +17,41 @@ if (!fs.existsSync(filePath)) {
|
||||
|
||||
let content = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
const oldCheck = "if (version === '1_0_0' || version === '1_1_0') {";
|
||||
const newCheck =
|
||||
// Patch 1: version acceptance list
|
||||
const oldVersionCheck = "if (version === '1_0_0' || version === '1_1_0') {";
|
||||
const newVersionCheck =
|
||||
"if (version === '1_0_0' || version === '1_1_0' || version === '1_3_0' || version === '1_5_0') {";
|
||||
|
||||
if (content.includes(newCheck)) {
|
||||
// Patch 2: timezone instruction must be sent for all protocols >= 1.1.0, not just 1.1.0
|
||||
const oldTimezone = "if (protocolVersion === '1_1_0') {";
|
||||
const newTimezone = "if (protocolVersion !== '1_0_0') {";
|
||||
|
||||
let patched = false;
|
||||
|
||||
if (!content.includes(newVersionCheck)) {
|
||||
if (!content.includes(oldVersionCheck)) {
|
||||
console.log("[patch-guacamole-lite] Version check target not found, skipping");
|
||||
process.exit(0);
|
||||
}
|
||||
content = content.replace(oldVersionCheck, newVersionCheck);
|
||||
patched = true;
|
||||
}
|
||||
|
||||
if (!content.includes(newTimezone)) {
|
||||
if (!content.includes(oldTimezone)) {
|
||||
console.log("[patch-guacamole-lite] Timezone target not found, skipping");
|
||||
process.exit(0);
|
||||
}
|
||||
content = content.replace(oldTimezone, newTimezone);
|
||||
patched = true;
|
||||
}
|
||||
|
||||
if (!patched) {
|
||||
console.log("[patch-guacamole-lite] Already patched");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!content.includes(oldCheck)) {
|
||||
console.log("[patch-guacamole-lite] Target code not found, skipping");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
content = content.replace(oldCheck, newCheck);
|
||||
fs.writeFileSync(filePath, content);
|
||||
console.log(
|
||||
"[patch-guacamole-lite] Patched to support protocol VERSION_1_3_0 and VERSION_1_5_0",
|
||||
"[patch-guacamole-lite] Patched to support protocol VERSION_1_3_0 and VERSION_1_5_0 with correct timezone handshake",
|
||||
);
|
||||
|
||||
@@ -13,6 +13,8 @@ import terminalRoutes from "./routes/terminal.js";
|
||||
import guacamoleRoutes from "../guacamole/routes.js";
|
||||
import networkTopologyRoutes from "./routes/network-topology.js";
|
||||
import rbacRoutes from "./routes/rbac.js";
|
||||
import openTabsRoutes from "./routes/open-tabs.js";
|
||||
import userPreferencesRoutes from "./routes/user-preferences.js";
|
||||
import { createCorsMiddleware } from "../utils/cors-config.js";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
@@ -1762,6 +1764,8 @@ app.use("/terminal", terminalRoutes);
|
||||
app.use("/guacamole", guacamoleRoutes);
|
||||
app.use("/network-topology", networkTopologyRoutes);
|
||||
app.use("/rbac", rbacRoutes);
|
||||
app.use("/open-tabs", openTabsRoutes);
|
||||
app.use("/user-preferences", userPreferencesRoutes);
|
||||
|
||||
const frontendDistPaths = [
|
||||
path.join(__dirname, "../../../dist"),
|
||||
|
||||
@@ -450,8 +450,41 @@ async function initializeCompleteDatabase(): Promise<void> {
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_open_tabs (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
tab_type TEXT NOT NULL,
|
||||
host_id INTEGER,
|
||||
label TEXT NOT NULL,
|
||||
tab_order INTEGER NOT NULL DEFAULT 0,
|
||||
backend_session_id TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_preferences (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
reopen_tabs_on_login INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
`);
|
||||
|
||||
try {
|
||||
sqlite.prepare("DELETE FROM user_open_tabs").run();
|
||||
databaseLogger.info("Open tabs cleared on startup", {
|
||||
operation: "db_init_open_tabs_cleanup",
|
||||
});
|
||||
} catch (e) {
|
||||
databaseLogger.warn("Could not clear open tabs on startup", {
|
||||
operation: "db_init_open_tabs_cleanup_failed",
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const result = sqlite
|
||||
.prepare("DELETE FROM sessions WHERE expires_at <= ?")
|
||||
|
||||
@@ -632,3 +632,33 @@ export const apiKeys = sqliteTable("api_keys", {
|
||||
lastUsedAt: text("last_used_at"),
|
||||
isActive: integer("is_active", { mode: "boolean" }).notNull().default(true),
|
||||
});
|
||||
|
||||
export const userOpenTabs = sqliteTable("user_open_tabs", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
tabType: text("tab_type").notNull(),
|
||||
hostId: integer("host_id").references(() => hosts.id, { onDelete: "cascade" }),
|
||||
label: text("label").notNull(),
|
||||
tabOrder: integer("tab_order").notNull().default(0),
|
||||
backendSessionId: text("backend_session_id"),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
|
||||
export const userPreferences = sqliteTable("user_preferences", {
|
||||
userId: text("user_id")
|
||||
.primaryKey()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
reopenTabsOnLogin: integer("reopen_tabs_on_login", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
updatedAt: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
import type { AuthenticatedRequest } from "../../../types/index.js";
|
||||
import express from "express";
|
||||
import { db } from "../db/index.js";
|
||||
import { userOpenTabs } from "../db/schema.js";
|
||||
import { eq, and, sql } from "drizzle-orm";
|
||||
import type { Request, Response } from "express";
|
||||
import { databaseLogger } from "../../utils/logger.js";
|
||||
import { AuthManager } from "../../utils/auth-manager.js";
|
||||
import { sessionManager } from "../../ssh/terminal-session-manager.js";
|
||||
|
||||
const router = express.Router();
|
||||
const authManager = AuthManager.getInstance();
|
||||
const authenticateJWT = authManager.createAuthMiddleware();
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /open-tabs:
|
||||
* get:
|
||||
* summary: Get all open tabs for the current user
|
||||
* tags:
|
||||
* - Open Tabs
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of open tabs ordered by tab_order.
|
||||
*/
|
||||
const TAB_TTL_MS = 30 * 60 * 1000;
|
||||
|
||||
router.get("/", authenticateJWT, async (req: Request, res: Response) => {
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
try {
|
||||
const cutoff = new Date(Date.now() - TAB_TTL_MS).toISOString();
|
||||
const tabs = db
|
||||
.select()
|
||||
.from(userOpenTabs)
|
||||
.where(and(eq(userOpenTabs.userId, userId), sql`${userOpenTabs.updatedAt} > ${cutoff}`))
|
||||
.orderBy(userOpenTabs.tabOrder)
|
||||
.all();
|
||||
return res.json(tabs);
|
||||
} catch (e) {
|
||||
databaseLogger.error("Failed to get open tabs", e, { operation: "get_open_tabs", userId });
|
||||
return res.status(500).json({ error: "Failed to get open tabs" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /open-tabs:
|
||||
* post:
|
||||
* summary: Upsert a single open tab for the current user
|
||||
* tags:
|
||||
* - Open Tabs
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [id, tabType, label, tabOrder]
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* tabType:
|
||||
* type: string
|
||||
* hostId:
|
||||
* type: integer
|
||||
* nullable: true
|
||||
* label:
|
||||
* type: string
|
||||
* tabOrder:
|
||||
* type: integer
|
||||
* backendSessionId:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Tab upserted successfully.
|
||||
*/
|
||||
router.post("/", authenticateJWT, async (req: Request, res: Response) => {
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { id, tabType, hostId, label, tabOrder, backendSessionId } = req.body as {
|
||||
id: string;
|
||||
tabType: string;
|
||||
hostId?: number | null;
|
||||
label: string;
|
||||
tabOrder: number;
|
||||
backendSessionId?: string | null;
|
||||
};
|
||||
|
||||
if (!id || !tabType || !label) {
|
||||
return res.status(400).json({ error: "id, tabType, and label are required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
const existing = db.select().from(userOpenTabs).where(and(eq(userOpenTabs.id, id), eq(userOpenTabs.userId, userId))).all();
|
||||
if (existing.length > 0) {
|
||||
// Preserve existing backendSessionId when not explicitly provided
|
||||
const sessionId = backendSessionId !== undefined ? backendSessionId : existing[0].backendSessionId;
|
||||
db.update(userOpenTabs)
|
||||
.set({ tabType, hostId: hostId ?? null, label, tabOrder, backendSessionId: sessionId ?? null, updatedAt: now })
|
||||
.where(and(eq(userOpenTabs.id, id), eq(userOpenTabs.userId, userId)))
|
||||
.run();
|
||||
} else {
|
||||
db.insert(userOpenTabs).values({ id, userId, tabType, hostId: hostId ?? null, label, tabOrder, backendSessionId: backendSessionId ?? null, updatedAt: now }).run();
|
||||
}
|
||||
return res.json({ success: true });
|
||||
} catch (e) {
|
||||
databaseLogger.error("Failed to upsert open tab", e, { operation: "upsert_open_tab", userId, id });
|
||||
return res.status(500).json({ error: "Failed to upsert open tab" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /open-tabs:
|
||||
* put:
|
||||
* summary: Bulk replace all open tabs for the current user
|
||||
* tags:
|
||||
* - Open Tabs
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* tabs:
|
||||
* type: array
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Tabs updated successfully.
|
||||
*/
|
||||
router.put("/", authenticateJWT, async (req: Request, res: Response) => {
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { tabs } = req.body as {
|
||||
tabs: Array<{
|
||||
id: string;
|
||||
tabType: string;
|
||||
hostId?: number | null;
|
||||
label: string;
|
||||
tabOrder: number;
|
||||
backendSessionId?: string | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
if (!Array.isArray(tabs)) {
|
||||
return res.status(400).json({ error: "tabs must be an array" });
|
||||
}
|
||||
|
||||
try {
|
||||
db.delete(userOpenTabs).where(eq(userOpenTabs.userId, userId)).run();
|
||||
if (tabs.length > 0) {
|
||||
const now = new Date().toISOString();
|
||||
db.insert(userOpenTabs).values(
|
||||
tabs.map((t) => ({
|
||||
id: t.id,
|
||||
userId,
|
||||
tabType: t.tabType,
|
||||
hostId: t.hostId ?? null,
|
||||
label: t.label,
|
||||
tabOrder: t.tabOrder,
|
||||
backendSessionId: t.backendSessionId ?? null,
|
||||
updatedAt: now,
|
||||
})),
|
||||
).run();
|
||||
}
|
||||
return res.json({ success: true });
|
||||
} catch (e) {
|
||||
databaseLogger.error("Failed to sync open tabs", e, { operation: "sync_open_tabs", userId });
|
||||
return res.status(500).json({ error: "Failed to sync open tabs" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /open-tabs/{id}:
|
||||
* patch:
|
||||
* summary: Update a single open tab
|
||||
* tags:
|
||||
* - Open Tabs
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Tab updated successfully.
|
||||
* 404:
|
||||
* description: Tab not found.
|
||||
*/
|
||||
router.patch("/:id", authenticateJWT, async (req: Request, res: Response) => {
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const id = String(req.params.id);
|
||||
const updates = req.body as Partial<{
|
||||
label: string;
|
||||
tabOrder: number;
|
||||
backendSessionId: string | null;
|
||||
}>;
|
||||
|
||||
try {
|
||||
const result = db
|
||||
.update(userOpenTabs)
|
||||
.set({ ...updates, updatedAt: new Date().toISOString() })
|
||||
.where(and(eq(userOpenTabs.id, id), eq(userOpenTabs.userId, userId)))
|
||||
.run();
|
||||
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({ error: "Tab not found" });
|
||||
}
|
||||
return res.json({ success: true });
|
||||
} catch (e) {
|
||||
databaseLogger.error("Failed to update open tab", e, { operation: "update_open_tab", userId, id });
|
||||
return res.status(500).json({ error: "Failed to update open tab" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /open-tabs/{id}:
|
||||
* delete:
|
||||
* summary: Delete a single open tab
|
||||
* tags:
|
||||
* - Open Tabs
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Tab deleted successfully.
|
||||
*/
|
||||
router.delete("/:id", authenticateJWT, async (req: Request, res: Response) => {
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const id = String(req.params.id);
|
||||
|
||||
try {
|
||||
db.delete(userOpenTabs)
|
||||
.where(and(eq(userOpenTabs.id, id), eq(userOpenTabs.userId, userId)))
|
||||
.run();
|
||||
return res.json({ success: true });
|
||||
} catch (e) {
|
||||
databaseLogger.error("Failed to delete open tab", e, { operation: "delete_open_tab", userId, id });
|
||||
return res.status(500).json({ error: "Failed to delete open tab" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /open-tabs/active-sessions:
|
||||
* get:
|
||||
* summary: Get all active backend sessions for the current user
|
||||
* description: Returns live terminal sessions from the session manager. Used by the Active Connections panel and tab restore logic.
|
||||
* tags:
|
||||
* - Open Tabs
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of active sessions.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* sessionId:
|
||||
* type: string
|
||||
* hostId:
|
||||
* type: integer
|
||||
* hostName:
|
||||
* type: string
|
||||
* tabInstanceId:
|
||||
* type: string
|
||||
* isConnected:
|
||||
* type: boolean
|
||||
* createdAt:
|
||||
* type: number
|
||||
*/
|
||||
router.get("/active-sessions", authenticateJWT, async (req: Request, res: Response) => {
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
try {
|
||||
const sessions = sessionManager.getUserSessions(userId);
|
||||
return res.json(
|
||||
sessions.map((s) => ({
|
||||
sessionId: s.id,
|
||||
hostId: s.hostId,
|
||||
hostName: s.hostName,
|
||||
tabInstanceId: s.attachedTabInstanceId ?? s.tabInstanceId ?? null,
|
||||
isConnected: s.isConnected,
|
||||
createdAt: s.createdAt,
|
||||
})),
|
||||
);
|
||||
} catch (e) {
|
||||
databaseLogger.error("Failed to get active sessions", e, { operation: "get_active_sessions", userId });
|
||||
return res.status(500).json({ error: "Failed to get active sessions" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { AuthenticatedRequest } from "../../../types/index.js";
|
||||
import express from "express";
|
||||
import { db } from "../db/index.js";
|
||||
import { userPreferences } from "../db/schema.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { Request, Response } from "express";
|
||||
import { databaseLogger } from "../../utils/logger.js";
|
||||
import { AuthManager } from "../../utils/auth-manager.js";
|
||||
|
||||
const router = express.Router();
|
||||
const authManager = AuthManager.getInstance();
|
||||
const authenticateJWT = authManager.createAuthMiddleware();
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /user-preferences:
|
||||
* get:
|
||||
* summary: Get preferences for the current user
|
||||
* tags:
|
||||
* - User Preferences
|
||||
* responses:
|
||||
* 200:
|
||||
* description: User preferences.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* reopenTabsOnLogin:
|
||||
* type: boolean
|
||||
*/
|
||||
router.get("/", authenticateJWT, (req: Request, res: Response) => {
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
try {
|
||||
const rows = db
|
||||
.select()
|
||||
.from(userPreferences)
|
||||
.where(eq(userPreferences.userId, userId))
|
||||
.all();
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.json({ reopenTabsOnLogin: false });
|
||||
}
|
||||
return res.json({ reopenTabsOnLogin: rows[0].reopenTabsOnLogin });
|
||||
} catch (e) {
|
||||
databaseLogger.error("Failed to get user preferences", e, { operation: "get_user_preferences", userId });
|
||||
return res.status(500).json({ error: "Failed to get user preferences" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /user-preferences:
|
||||
* put:
|
||||
* summary: Update preferences for the current user
|
||||
* tags:
|
||||
* - User Preferences
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* reopenTabsOnLogin:
|
||||
* type: boolean
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Preferences updated successfully.
|
||||
*/
|
||||
router.put("/", authenticateJWT, (req: Request, res: Response) => {
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { reopenTabsOnLogin } = req.body as { reopenTabsOnLogin?: boolean };
|
||||
|
||||
if (typeof reopenTabsOnLogin !== "boolean") {
|
||||
return res.status(400).json({ error: "reopenTabsOnLogin must be a boolean" });
|
||||
}
|
||||
|
||||
try {
|
||||
const existing = db
|
||||
.select()
|
||||
.from(userPreferences)
|
||||
.where(eq(userPreferences.userId, userId))
|
||||
.all();
|
||||
|
||||
if (existing.length === 0) {
|
||||
db.insert(userPreferences).values({
|
||||
userId,
|
||||
reopenTabsOnLogin,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}).run();
|
||||
} else {
|
||||
db.update(userPreferences)
|
||||
.set({ reopenTabsOnLogin, updatedAt: new Date().toISOString() })
|
||||
.where(eq(userPreferences.userId, userId))
|
||||
.run();
|
||||
}
|
||||
|
||||
return res.json({ success: true, reopenTabsOnLogin });
|
||||
} catch (e) {
|
||||
databaseLogger.error("Failed to update user preferences", e, { operation: "update_user_preferences", userId });
|
||||
return res.status(500).json({ error: "Failed to update user preferences" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
dashboardPreferences,
|
||||
opksshTokens,
|
||||
apiKeys,
|
||||
userOpenTabs,
|
||||
userPreferences,
|
||||
} from "../db/schema.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
import bcrypt from "bcryptjs";
|
||||
@@ -284,6 +286,8 @@ async function deleteUserAndRelatedData(userId: string): Promise<void> {
|
||||
.delete(dashboardPreferences)
|
||||
.where(eq(dashboardPreferences.userId, userId));
|
||||
await db.delete(opksshTokens).where(eq(opksshTokens.userId, userId));
|
||||
await db.delete(userOpenTabs).where(eq(userOpenTabs.userId, userId));
|
||||
await db.delete(userPreferences).where(eq(userPreferences.userId, userId));
|
||||
|
||||
db.$client
|
||||
.prepare("DELETE FROM settings WHERE key LIKE ?")
|
||||
@@ -1353,6 +1357,7 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
await authManager.authenticateOIDCUser(userRecord.id, deviceInfo.type);
|
||||
} catch (setupError) {
|
||||
authLogger.error("Failed to setup OIDC user encryption", setupError, {
|
||||
operation: "oidc_user_encryption_setup_failed",
|
||||
@@ -2062,6 +2067,8 @@ router.patch("/password-login-allowed", authenticateJWT, async (req, res) => {
|
||||
"INSERT OR REPLACE INTO settings (key, value) VALUES ('allow_password_login', ?)",
|
||||
)
|
||||
.run(allowed ? "true" : "false");
|
||||
const { saveMemoryDatabaseToFile } = await import("../db/index.js");
|
||||
await saveMemoryDatabaseToFile();
|
||||
res.json({ allowed });
|
||||
} catch (err) {
|
||||
authLogger.error("Failed to set password login allowed", err);
|
||||
|
||||
@@ -144,6 +144,17 @@ router.post("/token", async (req, res) => {
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Host ID to connect to
|
||||
* requestBody:
|
||||
* required: false
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* protocol:
|
||||
* type: string
|
||||
* enum: [rdp, vnc, telnet]
|
||||
* description: Override the host's default connection type
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Connection token generated successfully
|
||||
@@ -205,13 +216,34 @@ router.post(
|
||||
}
|
||||
}
|
||||
|
||||
const connectionType = (host.connectionType as string) || "ssh";
|
||||
const requestedProtocol = req.body?.protocol as string | undefined;
|
||||
const connectionType =
|
||||
requestedProtocol || (host.connectionType as string);
|
||||
|
||||
if (!["rdp", "vnc", "telnet"].includes(connectionType)) {
|
||||
return res.status(400).json({
|
||||
error: `Connection type '${connectionType}' is not supported for remote desktop. Only RDP, VNC, and Telnet are supported.`,
|
||||
});
|
||||
}
|
||||
|
||||
// Old hosts only had connectionType set; enableRdp/enableVnc/enableTelnet defaulted to false.
|
||||
// Apply the same migration fallback used in host.ts GET routes.
|
||||
const ct = host.connectionType as string;
|
||||
const rdpRaw = !!host.enableRdp;
|
||||
const vncRaw = !!host.enableVnc;
|
||||
const telRaw = !!host.enableTelnet;
|
||||
const isMigratedNonSsh = !rdpRaw && !vncRaw && !telRaw && ct && ct !== "ssh";
|
||||
const protocolEnabledMap: Record<string, boolean> = {
|
||||
rdp: isMigratedNonSsh ? ct === "rdp" : rdpRaw,
|
||||
vnc: isMigratedNonSsh ? ct === "vnc" : vncRaw,
|
||||
telnet: isMigratedNonSsh ? ct === "telnet" : telRaw,
|
||||
};
|
||||
if (!protocolEnabledMap[connectionType]) {
|
||||
return res.status(400).json({
|
||||
error: `${connectionType.toUpperCase()} is not enabled for this host.`,
|
||||
});
|
||||
}
|
||||
|
||||
let guacConfig: Record<string, unknown> = {};
|
||||
if (host.guacamoleConfig) {
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Client as SSHClient } from "ssh2";
|
||||
import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js";
|
||||
import { WebSocketServer, WebSocket } from "ws";
|
||||
import { AuthManager } from "../utils/auth-manager.js";
|
||||
import { hosts, sshCredentials } from "../database/db/schema.js";
|
||||
@@ -155,12 +156,50 @@ async function createJumpHostChain(
|
||||
host: jumpHost.ip?.replace(/^\[|\]$/g, "") || jumpHost.ip,
|
||||
port: jumpHost.port || 22,
|
||||
username: jumpHost.username,
|
||||
tryKeyboard: true,
|
||||
tryKeyboard: resolvedCredentials.authType !== "none",
|
||||
readyTimeout: 60000,
|
||||
keepaliveInterval: 30000,
|
||||
keepaliveCountMax: 120,
|
||||
tcpKeepAlive: true,
|
||||
tcpKeepAliveInitialDelay: 30000,
|
||||
algorithms: {
|
||||
kex: [
|
||||
"curve25519-sha256",
|
||||
"curve25519-sha256@libssh.org",
|
||||
"ecdh-sha2-nistp521",
|
||||
"ecdh-sha2-nistp384",
|
||||
"ecdh-sha2-nistp256",
|
||||
"diffie-hellman-group-exchange-sha256",
|
||||
"diffie-hellman-group18-sha512",
|
||||
"diffie-hellman-group17-sha512",
|
||||
"diffie-hellman-group16-sha512",
|
||||
"diffie-hellman-group15-sha512",
|
||||
"diffie-hellman-group14-sha256",
|
||||
"diffie-hellman-group14-sha1",
|
||||
"diffie-hellman-group-exchange-sha1",
|
||||
"diffie-hellman-group1-sha1",
|
||||
],
|
||||
serverHostKey: [
|
||||
"ssh-ed25519",
|
||||
"ecdsa-sha2-nistp521",
|
||||
"ecdsa-sha2-nistp384",
|
||||
"ecdsa-sha2-nistp256",
|
||||
"rsa-sha2-512",
|
||||
"rsa-sha2-256",
|
||||
"ssh-rsa",
|
||||
"ssh-dss",
|
||||
],
|
||||
cipher: SSH_ALGORITHMS.cipher,
|
||||
hmac: [
|
||||
"hmac-sha2-512-etm@openssh.com",
|
||||
"hmac-sha2-256-etm@openssh.com",
|
||||
"hmac-sha2-512",
|
||||
"hmac-sha2-256",
|
||||
"hmac-sha1",
|
||||
"hmac-md5",
|
||||
],
|
||||
compress: ["none", "zlib@openssh.com", "zlib"],
|
||||
},
|
||||
};
|
||||
|
||||
if (
|
||||
@@ -182,6 +221,25 @@ async function createJumpHostChain(
|
||||
}
|
||||
}
|
||||
|
||||
client.on(
|
||||
"keyboard-interactive",
|
||||
(
|
||||
_name: string,
|
||||
_instructions: string,
|
||||
_lang: string,
|
||||
prompts: Array<{ prompt: string; echo: boolean }>,
|
||||
finish: (responses: string[]) => void,
|
||||
) => {
|
||||
const responses = prompts.map((p) => {
|
||||
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
|
||||
return resolvedCredentials.password as string;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
finish(responses);
|
||||
},
|
||||
);
|
||||
|
||||
if (currentClient) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
currentClient!.forwardOut(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createCorsMiddleware } from "../utils/cors-config.js";
|
||||
import cookieParser from "cookie-parser";
|
||||
import axios from "axios";
|
||||
import { Client as SSHClient } from "ssh2";
|
||||
import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import { hosts, sshCredentials } from "../database/db/schema.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
@@ -312,9 +313,47 @@ async function createJumpHostChain(
|
||||
host: jumpHostConfig.ip?.replace(/^\[|\]$/g, "") || jumpHostConfig.ip,
|
||||
port: jumpHostConfig.port || 22,
|
||||
username: jumpHostConfig.username,
|
||||
tryKeyboard: true,
|
||||
readyTimeout: 30000,
|
||||
tryKeyboard: jumpHostConfig.authType !== "none",
|
||||
readyTimeout: 60000,
|
||||
hostVerifier: jumpHostVerifier,
|
||||
algorithms: {
|
||||
kex: [
|
||||
"curve25519-sha256",
|
||||
"curve25519-sha256@libssh.org",
|
||||
"ecdh-sha2-nistp521",
|
||||
"ecdh-sha2-nistp384",
|
||||
"ecdh-sha2-nistp256",
|
||||
"diffie-hellman-group-exchange-sha256",
|
||||
"diffie-hellman-group18-sha512",
|
||||
"diffie-hellman-group17-sha512",
|
||||
"diffie-hellman-group16-sha512",
|
||||
"diffie-hellman-group15-sha512",
|
||||
"diffie-hellman-group14-sha256",
|
||||
"diffie-hellman-group14-sha1",
|
||||
"diffie-hellman-group-exchange-sha1",
|
||||
"diffie-hellman-group1-sha1",
|
||||
],
|
||||
serverHostKey: [
|
||||
"ssh-ed25519",
|
||||
"ecdsa-sha2-nistp521",
|
||||
"ecdsa-sha2-nistp384",
|
||||
"ecdsa-sha2-nistp256",
|
||||
"rsa-sha2-512",
|
||||
"rsa-sha2-256",
|
||||
"ssh-rsa",
|
||||
"ssh-dss",
|
||||
],
|
||||
cipher: SSH_ALGORITHMS.cipher,
|
||||
hmac: [
|
||||
"hmac-sha2-512-etm@openssh.com",
|
||||
"hmac-sha2-256-etm@openssh.com",
|
||||
"hmac-sha2-512",
|
||||
"hmac-sha2-256",
|
||||
"hmac-sha1",
|
||||
"hmac-md5",
|
||||
],
|
||||
compress: ["none", "zlib@openssh.com", "zlib"],
|
||||
},
|
||||
};
|
||||
|
||||
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
|
||||
@@ -330,6 +369,25 @@ async function createJumpHostChain(
|
||||
}
|
||||
}
|
||||
|
||||
jumpClient.on(
|
||||
"keyboard-interactive",
|
||||
(
|
||||
_name: string,
|
||||
_instructions: string,
|
||||
_lang: string,
|
||||
prompts: Array<{ prompt: string; echo: boolean }>,
|
||||
finish: (responses: string[]) => void,
|
||||
) => {
|
||||
const responses = prompts.map((p) => {
|
||||
if (/password/i.test(p.prompt) && jumpHostConfig.password) {
|
||||
return jumpHostConfig.password as string;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
finish(responses);
|
||||
},
|
||||
);
|
||||
|
||||
if (currentClient) {
|
||||
currentClient.forwardOut(
|
||||
"127.0.0.1",
|
||||
@@ -627,6 +685,13 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
||||
host.jumpHosts = [];
|
||||
}
|
||||
}
|
||||
if (typeof host.terminalConfig === "string" && host.terminalConfig) {
|
||||
try {
|
||||
host.terminalConfig = JSON.parse(host.terminalConfig as string);
|
||||
} catch {
|
||||
host.terminalConfig = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (!host.enableDocker) {
|
||||
sshLogger.warn("Docker not enabled for host", {
|
||||
@@ -757,8 +822,14 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
||||
port: host.port || 22,
|
||||
username: host.username,
|
||||
tryKeyboard: true,
|
||||
keepaliveInterval: 30000,
|
||||
keepaliveCountMax: 3,
|
||||
keepaliveInterval:
|
||||
typeof host.terminalConfig?.keepaliveInterval === "number"
|
||||
? host.terminalConfig.keepaliveInterval * 1000
|
||||
: 60000,
|
||||
keepaliveCountMax:
|
||||
typeof host.terminalConfig?.keepaliveCountMax === "number"
|
||||
? host.terminalConfig.keepaliveCountMax
|
||||
: 5,
|
||||
readyTimeout: 60000,
|
||||
tcpKeepAlive: true,
|
||||
tcpKeepAliveInitialDelay: 30000,
|
||||
|
||||
@@ -327,9 +327,47 @@ async function createJumpHostChain(
|
||||
host: jumpHostConfig.ip?.replace(/^\[|\]$/g, "") || jumpHostConfig.ip,
|
||||
port: jumpHostConfig.port || 22,
|
||||
username: jumpHostConfig.username,
|
||||
tryKeyboard: true,
|
||||
readyTimeout: 30000,
|
||||
tryKeyboard: jumpHostConfig.authType !== "none",
|
||||
readyTimeout: 60000,
|
||||
hostVerifier: jumpHostVerifier,
|
||||
algorithms: {
|
||||
kex: [
|
||||
"curve25519-sha256",
|
||||
"curve25519-sha256@libssh.org",
|
||||
"ecdh-sha2-nistp521",
|
||||
"ecdh-sha2-nistp384",
|
||||
"ecdh-sha2-nistp256",
|
||||
"diffie-hellman-group-exchange-sha256",
|
||||
"diffie-hellman-group18-sha512",
|
||||
"diffie-hellman-group17-sha512",
|
||||
"diffie-hellman-group16-sha512",
|
||||
"diffie-hellman-group15-sha512",
|
||||
"diffie-hellman-group14-sha256",
|
||||
"diffie-hellman-group14-sha1",
|
||||
"diffie-hellman-group-exchange-sha1",
|
||||
"diffie-hellman-group1-sha1",
|
||||
],
|
||||
serverHostKey: [
|
||||
"ssh-ed25519",
|
||||
"ecdsa-sha2-nistp521",
|
||||
"ecdsa-sha2-nistp384",
|
||||
"ecdsa-sha2-nistp256",
|
||||
"rsa-sha2-512",
|
||||
"rsa-sha2-256",
|
||||
"ssh-rsa",
|
||||
"ssh-dss",
|
||||
],
|
||||
cipher: SSH_ALGORITHMS.cipher,
|
||||
hmac: [
|
||||
"hmac-sha2-512-etm@openssh.com",
|
||||
"hmac-sha2-256-etm@openssh.com",
|
||||
"hmac-sha2-512",
|
||||
"hmac-sha2-256",
|
||||
"hmac-sha1",
|
||||
"hmac-md5",
|
||||
],
|
||||
compress: ["none", "zlib@openssh.com", "zlib"],
|
||||
},
|
||||
};
|
||||
|
||||
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
|
||||
@@ -345,6 +383,25 @@ async function createJumpHostChain(
|
||||
}
|
||||
}
|
||||
|
||||
jumpClient.on(
|
||||
"keyboard-interactive",
|
||||
(
|
||||
_name: string,
|
||||
_instructions: string,
|
||||
_lang: string,
|
||||
prompts: Array<{ prompt: string; echo: boolean }>,
|
||||
finish: (responses: string[]) => void,
|
||||
) => {
|
||||
const responses = prompts.map((p) => {
|
||||
if (/password/i.test(p.prompt) && jumpHostConfig.password) {
|
||||
return jumpHostConfig.password as string;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
finish(responses);
|
||||
},
|
||||
);
|
||||
|
||||
if (currentClient) {
|
||||
currentClient.forwardOut(
|
||||
"127.0.0.1",
|
||||
@@ -917,6 +974,8 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
authType,
|
||||
sudoPassword: undefined as string | undefined,
|
||||
};
|
||||
let hostKeepaliveInterval: number | undefined;
|
||||
let hostKeepaliveCountMax: number | undefined;
|
||||
if (hostId && userId && !password && !sshKey) {
|
||||
try {
|
||||
const { resolveHostById } = await import("./host-resolver.js");
|
||||
@@ -929,6 +988,8 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
authType: resolvedHost.authType,
|
||||
sudoPassword: resolvedHost.sudoPassword as string | undefined,
|
||||
};
|
||||
hostKeepaliveInterval = resolvedHost.terminalConfig?.keepaliveInterval;
|
||||
hostKeepaliveCountMax = resolvedHost.terminalConfig?.keepaliveCountMax;
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"info",
|
||||
@@ -957,6 +1018,8 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
authType: resolvedHost.authType,
|
||||
sudoPassword: resolvedHost.sudoPassword as string | undefined,
|
||||
};
|
||||
hostKeepaliveInterval = resolvedHost.terminalConfig?.keepaliveInterval;
|
||||
hostKeepaliveCountMax = resolvedHost.terminalConfig?.keepaliveCountMax;
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"info",
|
||||
@@ -980,11 +1043,15 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
port,
|
||||
username,
|
||||
tryKeyboard: true,
|
||||
keepaliveInterval: 10000,
|
||||
keepaliveCountMax: 5,
|
||||
keepaliveInterval:
|
||||
typeof hostKeepaliveInterval === "number"
|
||||
? hostKeepaliveInterval * 1000
|
||||
: 60000,
|
||||
keepaliveCountMax:
|
||||
typeof hostKeepaliveCountMax === "number" ? hostKeepaliveCountMax : 5,
|
||||
readyTimeout: 60000,
|
||||
tcpKeepAlive: true,
|
||||
tcpKeepAliveInitialDelay: 5000,
|
||||
tcpKeepAliveInitialDelay: 30000,
|
||||
hostVerifier: await SSHHostKeyVerifier.createHostVerifier(
|
||||
hostId,
|
||||
ip,
|
||||
|
||||
@@ -251,9 +251,47 @@ async function createJumpHostChain(
|
||||
host: jumpHostConfig.ip?.replace(/^\[|\]$/g, "") || jumpHostConfig.ip,
|
||||
port: jumpHostConfig.port || 22,
|
||||
username: jumpHostConfig.username,
|
||||
tryKeyboard: true,
|
||||
readyTimeout: 30000,
|
||||
tryKeyboard: jumpHostConfig.authType !== "none",
|
||||
readyTimeout: 60000,
|
||||
hostVerifier: jumpHostVerifier,
|
||||
algorithms: {
|
||||
kex: [
|
||||
"curve25519-sha256",
|
||||
"curve25519-sha256@libssh.org",
|
||||
"ecdh-sha2-nistp521",
|
||||
"ecdh-sha2-nistp384",
|
||||
"ecdh-sha2-nistp256",
|
||||
"diffie-hellman-group-exchange-sha256",
|
||||
"diffie-hellman-group18-sha512",
|
||||
"diffie-hellman-group17-sha512",
|
||||
"diffie-hellman-group16-sha512",
|
||||
"diffie-hellman-group15-sha512",
|
||||
"diffie-hellman-group14-sha256",
|
||||
"diffie-hellman-group14-sha1",
|
||||
"diffie-hellman-group-exchange-sha1",
|
||||
"diffie-hellman-group1-sha1",
|
||||
],
|
||||
serverHostKey: [
|
||||
"ssh-ed25519",
|
||||
"ecdsa-sha2-nistp521",
|
||||
"ecdsa-sha2-nistp384",
|
||||
"ecdsa-sha2-nistp256",
|
||||
"rsa-sha2-512",
|
||||
"rsa-sha2-256",
|
||||
"ssh-rsa",
|
||||
"ssh-dss",
|
||||
],
|
||||
cipher: SSH_ALGORITHMS.cipher,
|
||||
hmac: [
|
||||
"hmac-sha2-512-etm@openssh.com",
|
||||
"hmac-sha2-256-etm@openssh.com",
|
||||
"hmac-sha2-512",
|
||||
"hmac-sha2-256",
|
||||
"hmac-sha1",
|
||||
"hmac-md5",
|
||||
],
|
||||
compress: ["none", "zlib@openssh.com", "zlib"],
|
||||
},
|
||||
};
|
||||
|
||||
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
|
||||
@@ -269,6 +307,25 @@ async function createJumpHostChain(
|
||||
}
|
||||
}
|
||||
|
||||
jumpClient.on(
|
||||
"keyboard-interactive",
|
||||
(
|
||||
_name: string,
|
||||
_instructions: string,
|
||||
_lang: string,
|
||||
prompts: Array<{ prompt: string; echo: boolean }>,
|
||||
finish: (responses: string[]) => void,
|
||||
) => {
|
||||
const responses = prompts.map((p) => {
|
||||
if (/password/i.test(p.prompt) && jumpHostConfig.password) {
|
||||
return jumpHostConfig.password as string;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
finish(responses);
|
||||
},
|
||||
);
|
||||
|
||||
if (currentClient) {
|
||||
currentClient.forwardOut(
|
||||
"127.0.0.1",
|
||||
@@ -1493,8 +1550,8 @@ async function buildSshConfig(
|
||||
port: host.port,
|
||||
username: host.username,
|
||||
tryKeyboard: true,
|
||||
keepaliveInterval: 30000,
|
||||
keepaliveCountMax: 3,
|
||||
keepaliveInterval: 60000,
|
||||
keepaliveCountMax: 5,
|
||||
readyTimeout: 60000,
|
||||
tcpKeepAlive: true,
|
||||
tcpKeepAliveInitialDelay: 30000,
|
||||
|
||||
@@ -79,13 +79,27 @@ class TerminalSessionManager {
|
||||
const tabSessions = userSessions.filter(
|
||||
(s) => s.tabInstanceId === tabInstanceId,
|
||||
);
|
||||
if (tabSessions.length > 0) {
|
||||
for (const existing of tabSessions) {
|
||||
const isLiveSession =
|
||||
existing.isConnected &&
|
||||
existing.sshStream != null &&
|
||||
!existing.sshStream.destroyed;
|
||||
if (isLiveSession) {
|
||||
// Don't destroy a live session (even if detached) — the caller should attach instead
|
||||
sshLogger.warn("Tab instance has live session, skipping duplicate create", {
|
||||
operation: "session_tab_duplicate_skip",
|
||||
existingSessionId: existing.id,
|
||||
tabInstanceId,
|
||||
hasAttachedWs: existing.attachedWs !== null,
|
||||
});
|
||||
return existing.id;
|
||||
}
|
||||
sshLogger.warn("Tab instance already has session, destroying old", {
|
||||
operation: "session_tab_duplicate_cleanup",
|
||||
existingSessionId: tabSessions[0].id,
|
||||
existingSessionId: existing.id,
|
||||
tabInstanceId,
|
||||
});
|
||||
this.destroySession(tabSessions[0].id);
|
||||
this.destroySession(existing.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +193,7 @@ class TerminalSessionManager {
|
||||
|
||||
const isDetached =
|
||||
!session.attachedWs || session.attachedWs.readyState !== WebSocket.OPEN;
|
||||
const isOriginalTab = session.tabInstanceId === tabInstanceId;
|
||||
const isOriginalTab = (session.attachedTabInstanceId ?? session.tabInstanceId) === tabInstanceId;
|
||||
|
||||
if (
|
||||
!isDetached &&
|
||||
|
||||
+116
-4
@@ -373,8 +373,46 @@ async function createJumpHostChain(
|
||||
port: jumpHostConfig.port || 22,
|
||||
username: jumpHostConfig.username,
|
||||
tryKeyboard: jumpHostConfig.authType !== "none",
|
||||
readyTimeout: 30000,
|
||||
readyTimeout: 60000,
|
||||
hostVerifier: jumpHostVerifier,
|
||||
algorithms: {
|
||||
kex: [
|
||||
"curve25519-sha256",
|
||||
"curve25519-sha256@libssh.org",
|
||||
"ecdh-sha2-nistp521",
|
||||
"ecdh-sha2-nistp384",
|
||||
"ecdh-sha2-nistp256",
|
||||
"diffie-hellman-group-exchange-sha256",
|
||||
"diffie-hellman-group18-sha512",
|
||||
"diffie-hellman-group17-sha512",
|
||||
"diffie-hellman-group16-sha512",
|
||||
"diffie-hellman-group15-sha512",
|
||||
"diffie-hellman-group14-sha256",
|
||||
"diffie-hellman-group14-sha1",
|
||||
"diffie-hellman-group-exchange-sha1",
|
||||
"diffie-hellman-group1-sha1",
|
||||
],
|
||||
serverHostKey: [
|
||||
"ssh-ed25519",
|
||||
"ecdsa-sha2-nistp521",
|
||||
"ecdsa-sha2-nistp384",
|
||||
"ecdsa-sha2-nistp256",
|
||||
"rsa-sha2-512",
|
||||
"rsa-sha2-256",
|
||||
"ssh-rsa",
|
||||
"ssh-dss",
|
||||
],
|
||||
cipher: SSH_ALGORITHMS.cipher,
|
||||
hmac: [
|
||||
"hmac-sha2-512-etm@openssh.com",
|
||||
"hmac-sha2-256-etm@openssh.com",
|
||||
"hmac-sha2-512",
|
||||
"hmac-sha2-256",
|
||||
"hmac-sha1",
|
||||
"hmac-md5",
|
||||
],
|
||||
compress: ["none", "zlib@openssh.com", "zlib"],
|
||||
},
|
||||
};
|
||||
|
||||
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
|
||||
@@ -390,6 +428,25 @@ async function createJumpHostChain(
|
||||
}
|
||||
}
|
||||
|
||||
jumpClient.on(
|
||||
"keyboard-interactive",
|
||||
(
|
||||
_name: string,
|
||||
_instructions: string,
|
||||
_lang: string,
|
||||
prompts: Array<{ prompt: string; echo: boolean }>,
|
||||
finish: (responses: string[]) => void,
|
||||
) => {
|
||||
const responses = prompts.map((p) => {
|
||||
if (/password/i.test(p.prompt) && jumpHostConfig.password) {
|
||||
return jumpHostConfig.password as string;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
finish(responses);
|
||||
},
|
||||
);
|
||||
|
||||
if (currentClient) {
|
||||
currentClient.forwardOut(
|
||||
"127.0.0.1",
|
||||
@@ -1380,6 +1437,61 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
tabInstanceId,
|
||||
);
|
||||
|
||||
// If createSession returned an existing live session (duplicate tabInstanceId),
|
||||
// close the newly-established SSH connection and attach this WS to the live session instead.
|
||||
const existingSession = sessionManager.getSession(currentSessionId);
|
||||
if (
|
||||
existingSession &&
|
||||
existingSession.sshStream &&
|
||||
!existingSession.sshStream.destroyed &&
|
||||
existingSession.sshConn !== sshConn
|
||||
) {
|
||||
sshLogger.info(
|
||||
"Reusing existing live session after duplicate connectToHost, closing new SSH conn",
|
||||
{
|
||||
operation: "terminal_reuse_existing_session",
|
||||
sessionId: currentSessionId,
|
||||
tabInstanceId,
|
||||
userId,
|
||||
},
|
||||
);
|
||||
try {
|
||||
sshConn?.end();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
sshConn = null;
|
||||
|
||||
sshStream = existingSession.sshStream;
|
||||
sshConn = existingSession.sshConn;
|
||||
isConnecting = false;
|
||||
isConnected = true;
|
||||
sessionManager.attachWs(currentSessionId, userId, ws, tabInstanceId);
|
||||
|
||||
const buffered = sessionManager.getBuffer(existingSession);
|
||||
if (buffered) {
|
||||
ws.send(JSON.stringify({ type: "data", data: buffered }));
|
||||
}
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "sessionCreated",
|
||||
sessionId: currentSessionId,
|
||||
}),
|
||||
);
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "sessionAttached",
|
||||
sessionId: currentSessionId,
|
||||
}),
|
||||
);
|
||||
ws.send(
|
||||
JSON.stringify({ type: "connected", message: "Session reattached" }),
|
||||
);
|
||||
|
||||
cleanupAuthState(connectionTimeout);
|
||||
return;
|
||||
}
|
||||
|
||||
sshLogger.info("Terminal session created after SSH ready", {
|
||||
operation: "terminal_session_created",
|
||||
sessionId: currentSessionId,
|
||||
@@ -2170,10 +2282,10 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
tryKeyboard: resolvedCredentials.authType !== "none",
|
||||
keepaliveInterval:
|
||||
typeof hostKeepaliveInterval === "number"
|
||||
? hostKeepaliveInterval
|
||||
: 30000,
|
||||
? hostKeepaliveInterval * 1000
|
||||
: 60000,
|
||||
keepaliveCountMax:
|
||||
typeof hostKeepaliveCountMax === "number" ? hostKeepaliveCountMax : 3,
|
||||
typeof hostKeepaliveCountMax === "number" ? hostKeepaliveCountMax : 5,
|
||||
readyTimeout: 120000,
|
||||
tcpKeepAlive: true,
|
||||
tcpKeepAliveInitialDelay: 30000,
|
||||
|
||||
@@ -943,8 +943,8 @@ async function connectEndpointThroughSource(
|
||||
sock: endpointSock,
|
||||
username: tunnelConfig.endpointUsername,
|
||||
tryKeyboard: true,
|
||||
keepaliveInterval: 30000,
|
||||
keepaliveCountMax: 3,
|
||||
keepaliveInterval: tunnelConfig.keepaliveInterval ?? 60000,
|
||||
keepaliveCountMax: tunnelConfig.keepaliveCountMax ?? 5,
|
||||
readyTimeout: 60000,
|
||||
tcpKeepAlive: true,
|
||||
tcpKeepAliveInitialDelay: 30000,
|
||||
@@ -1150,6 +1150,14 @@ async function resolveC2STunnelSource(
|
||||
socks5Username: resolvedHost.socks5Username,
|
||||
socks5Password: resolvedHost.socks5Password,
|
||||
socks5ProxyChain: resolvedHost.socks5ProxyChain,
|
||||
keepaliveInterval:
|
||||
typeof resolvedHost.terminalConfig?.keepaliveInterval === "number"
|
||||
? resolvedHost.terminalConfig.keepaliveInterval * 1000
|
||||
: 60000,
|
||||
keepaliveCountMax:
|
||||
typeof resolvedHost.terminalConfig?.keepaliveCountMax === "number"
|
||||
? resolvedHost.terminalConfig.keepaliveCountMax
|
||||
: 5,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1162,8 +1170,8 @@ async function connectC2SSourceClient(
|
||||
port: tunnelConfig.sourceSSHPort,
|
||||
username: tunnelConfig.sourceUsername,
|
||||
tryKeyboard: true,
|
||||
keepaliveInterval: 30000,
|
||||
keepaliveCountMax: 3,
|
||||
keepaliveInterval: tunnelConfig.keepaliveInterval ?? 60000,
|
||||
keepaliveCountMax: tunnelConfig.keepaliveCountMax ?? 5,
|
||||
readyTimeout: 60000,
|
||||
tcpKeepAlive: true,
|
||||
tcpKeepAliveInitialDelay: 30000,
|
||||
@@ -1609,6 +1617,18 @@ async function connectSSHTunnel(
|
||||
keyType: resolvedHost.keyType,
|
||||
authMethod: resolvedHost.authType,
|
||||
};
|
||||
if (tunnelConfig.keepaliveInterval === undefined) {
|
||||
tunnelConfig.keepaliveInterval =
|
||||
typeof resolvedHost.terminalConfig?.keepaliveInterval === "number"
|
||||
? resolvedHost.terminalConfig.keepaliveInterval * 1000
|
||||
: 60000;
|
||||
}
|
||||
if (tunnelConfig.keepaliveCountMax === undefined) {
|
||||
tunnelConfig.keepaliveCountMax =
|
||||
typeof resolvedHost.terminalConfig?.keepaliveCountMax === "number"
|
||||
? resolvedHost.terminalConfig.keepaliveCountMax
|
||||
: 5;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
tunnelLogger.warn("Failed to resolve source host credentials", {
|
||||
@@ -1936,8 +1956,8 @@ async function connectSSHTunnel(
|
||||
port: tunnelConfig.sourceSSHPort,
|
||||
username: tunnelConfig.sourceUsername,
|
||||
tryKeyboard: true,
|
||||
keepaliveInterval: 30000,
|
||||
keepaliveCountMax: 3,
|
||||
keepaliveInterval: tunnelConfig.keepaliveInterval ?? 60000,
|
||||
keepaliveCountMax: tunnelConfig.keepaliveCountMax ?? 5,
|
||||
readyTimeout: 60000,
|
||||
tcpKeepAlive: true,
|
||||
tcpKeepAliveInitialDelay: 30000,
|
||||
@@ -2192,11 +2212,11 @@ async function killRemoteTunnelByMarker(
|
||||
tunnelConfig.sourceIP?.replace(/^\[|\]$/g, "") || tunnelConfig.sourceIP,
|
||||
port: tunnelConfig.sourceSSHPort,
|
||||
username: tunnelConfig.sourceUsername,
|
||||
keepaliveInterval: 30000,
|
||||
keepaliveCountMax: 3,
|
||||
keepaliveInterval: tunnelConfig.keepaliveInterval ?? 60000,
|
||||
keepaliveCountMax: tunnelConfig.keepaliveCountMax ?? 5,
|
||||
readyTimeout: 60000,
|
||||
tcpKeepAlive: true,
|
||||
tcpKeepAliveInitialDelay: 15000,
|
||||
tcpKeepAliveInitialDelay: 30000,
|
||||
algorithms: {
|
||||
kex: [
|
||||
"diffie-hellman-group14-sha256",
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import swaggerJSDoc from "swagger-jsdoc";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { promises as fs } from "fs";
|
||||
import { systemLogger } from "./utils/logger.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const swaggerOptions: swaggerJSDoc.Options = {
|
||||
definition: {
|
||||
openapi: "3.0.3",
|
||||
info: {
|
||||
title: "Termix API",
|
||||
version: "0.0.0",
|
||||
description: "Termix Backend API Reference",
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: "http://localhost:30001",
|
||||
description: "Main database and authentication server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30003",
|
||||
description: "SSH tunnel management server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30004",
|
||||
description: "SSH file manager server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30005",
|
||||
description: "Server statistics and monitoring server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30006",
|
||||
description: "Dashboard server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30007",
|
||||
description: "Docker management server",
|
||||
},
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: "http",
|
||||
scheme: "bearer",
|
||||
bearerFormat: "JWT",
|
||||
},
|
||||
},
|
||||
schemas: {
|
||||
Error: {
|
||||
type: "object",
|
||||
properties: {
|
||||
error: { type: "string" },
|
||||
details: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
security: [
|
||||
{
|
||||
bearerAuth: [],
|
||||
},
|
||||
],
|
||||
tags: [
|
||||
{
|
||||
name: "Alerts",
|
||||
description: "System alerts and notifications management",
|
||||
},
|
||||
{
|
||||
name: "Credentials",
|
||||
description: "SSH credential management",
|
||||
},
|
||||
{
|
||||
name: "Network Topology",
|
||||
description: "Network topology visualization and management",
|
||||
},
|
||||
{
|
||||
name: "RBAC",
|
||||
description: "Role-based access control for host sharing",
|
||||
},
|
||||
{
|
||||
name: "Snippets",
|
||||
description: "Command snippet management",
|
||||
},
|
||||
{
|
||||
name: "Terminal",
|
||||
description: "Terminal command history",
|
||||
},
|
||||
{
|
||||
name: "Users",
|
||||
description: "User management and authentication",
|
||||
},
|
||||
{
|
||||
name: "Dashboard",
|
||||
description: "Dashboard statistics and activity",
|
||||
},
|
||||
{
|
||||
name: "Docker",
|
||||
description: "Docker container management",
|
||||
},
|
||||
{
|
||||
name: "SSH Tunnels",
|
||||
description: "SSH tunnel connection management",
|
||||
},
|
||||
{
|
||||
name: "Server Stats",
|
||||
description: "Server status monitoring and metrics collection",
|
||||
},
|
||||
{
|
||||
name: "File Manager",
|
||||
description: "SSH file management operations",
|
||||
},
|
||||
],
|
||||
},
|
||||
apis: [
|
||||
path.join(__dirname, "database", "routes", "*.js").replace(/\\/g, "/"),
|
||||
path.join(__dirname, "dashboard.js").replace(/\\/g, "/"),
|
||||
path.join(__dirname, "ssh", "*.js").replace(/\\/g, "/"),
|
||||
path.join(__dirname, "guacamole", "routes.js").replace(/\\/g, "/"),
|
||||
],
|
||||
};
|
||||
|
||||
async function generateOpenAPISpec() {
|
||||
try {
|
||||
systemLogger.info("Generating OpenAPI specification", {
|
||||
operation: "openapi_generate_start",
|
||||
});
|
||||
|
||||
const swaggerSpec = swaggerJSDoc(swaggerOptions);
|
||||
|
||||
const outputPath = path.join(__dirname, "..", "..", "..", "openapi.json");
|
||||
|
||||
await fs.writeFile(
|
||||
outputPath,
|
||||
JSON.stringify(swaggerSpec, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
systemLogger.success("OpenAPI specification generated", {
|
||||
operation: "openapi_generate_success",
|
||||
});
|
||||
} catch (error) {
|
||||
systemLogger.error("Failed to generate OpenAPI specification", error, {
|
||||
operation: "openapi_generation",
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
generateOpenAPISpec();
|
||||
|
||||
export { swaggerOptions, generateOpenAPISpec };
|
||||
@@ -365,6 +365,9 @@ export interface TunnelConfig {
|
||||
socks5Username?: string;
|
||||
socks5Password?: string;
|
||||
socks5ProxyChain?: ProxyNode[];
|
||||
|
||||
keepaliveInterval?: number;
|
||||
keepaliveCountMax?: number;
|
||||
}
|
||||
|
||||
export interface C2STunnelPreset {
|
||||
|
||||
@@ -182,9 +182,12 @@ export type Tunnel = {
|
||||
|
||||
export type Tab = {
|
||||
id: string;
|
||||
instanceId: string;
|
||||
type: TabType;
|
||||
label: string;
|
||||
host?: Host;
|
||||
openedAt: number;
|
||||
restoredSessionId?: string | null;
|
||||
terminalRef?: import("react").RefObject<{
|
||||
sendInput?: (data: string) => void;
|
||||
reconnect?: () => void;
|
||||
@@ -286,6 +289,7 @@ export type Snippet = {
|
||||
description?: string;
|
||||
content: string;
|
||||
folder: string | null;
|
||||
order: number;
|
||||
};
|
||||
|
||||
export const FOLDER_ICONS = [
|
||||
|
||||
+486
-97
@@ -4,14 +4,8 @@ import { Separator } from "@/components/separator";
|
||||
import { Button } from "@/components/button";
|
||||
import { Sheet, SheetContent } from "@/components/sheet";
|
||||
import { ChevronLeft, ChevronRight, Maximize2 } from "lucide-react";
|
||||
import {
|
||||
useState,
|
||||
useRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
createRef,
|
||||
createPortal,
|
||||
} from "react";
|
||||
import { useState, useRef, useCallback, useEffect, createRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { MobileBottomBar } from "@/shell/MobileBottomBar";
|
||||
import { CommandPalette } from "@/shell/CommandPalette";
|
||||
@@ -36,9 +30,22 @@ import type {
|
||||
SplitMode,
|
||||
HostFolder,
|
||||
} from "@/types/ui-types";
|
||||
import { getSSHHosts, getUserInfo } from "@/main-axios";
|
||||
import { PANE_COUNTS } from "@/lib/theme";
|
||||
import {
|
||||
getSSHHosts,
|
||||
getUserInfo,
|
||||
getOpenTabs,
|
||||
addOpenTab,
|
||||
deleteOpenTab,
|
||||
patchOpenTab,
|
||||
getActiveSessions,
|
||||
getUserPreferences,
|
||||
type UserPreferences,
|
||||
type OpenTabRecord,
|
||||
} from "@/main-axios";
|
||||
import { dbHealthMonitor } from "@/lib/db-health-monitor";
|
||||
import type { SSHHostWithStatus } from "@/main-axios";
|
||||
import { ConnectionsPanel } from "@/sidebar/ConnectionsPanel";
|
||||
|
||||
function sshHostToHost(h: SSHHostWithStatus): Host {
|
||||
return {
|
||||
@@ -78,6 +85,9 @@ function sshHostToHost(h: SSHHostWithStatus): Host {
|
||||
name: a.name,
|
||||
snippetId: String(a.snippetId),
|
||||
})),
|
||||
jumpHosts: (h.jumpHosts ?? []).map((j) => ({
|
||||
hostId: String(j.hostId),
|
||||
})),
|
||||
serverTunnels: [],
|
||||
defaultPath: h.defaultPath,
|
||||
terminalConfig: h.terminalConfig as Host["terminalConfig"],
|
||||
@@ -86,6 +96,7 @@ function sshHostToHost(h: SSHHostWithStatus): Host {
|
||||
socks5Port: h.socks5Port,
|
||||
socks5Username: h.socks5Username,
|
||||
socks5Password: h.socks5Password,
|
||||
socks5ProxyChain: h.socks5ProxyChain ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -131,26 +142,62 @@ export function AppShell({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [tabs, setTabs] = useState<Tab[]>([
|
||||
{ id: "dashboard", type: "dashboard", label: t("nav.dashboard") },
|
||||
{
|
||||
id: "dashboard",
|
||||
instanceId: "dashboard",
|
||||
type: "dashboard",
|
||||
label: t("nav.dashboard"),
|
||||
openedAt: Date.now(),
|
||||
},
|
||||
]);
|
||||
const [activeTabId, setActiveTabId] = useState("dashboard");
|
||||
const [userPrefs, setUserPrefs] = useState<UserPreferences>({
|
||||
reopenTabsOnLogin: false,
|
||||
});
|
||||
const [userPrefsLoaded, setUserPrefsLoaded] = useState(false);
|
||||
const [hostsLoaded, setHostsLoaded] = useState(false);
|
||||
// Flips to true once the initial DB read (restore or skip) is done — sync must not fire before this
|
||||
const [tabsReady, setTabsReady] = useState(false);
|
||||
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
||||
const [splitMode, setSplitMode] = useState<SplitMode>("none");
|
||||
const [paneTabIds, setPaneTabIds] = useState<(string | null)[]>(
|
||||
Array(6).fill(null),
|
||||
const [splitMode, setSplitMode] = useState<SplitMode>(
|
||||
() => (localStorage.getItem("termix_splitMode") as SplitMode) ?? "none",
|
||||
);
|
||||
const [paneTabIds, setPaneTabIds] = useState<(string | null)[]>(
|
||||
() =>
|
||||
JSON.parse(localStorage.getItem("termix_paneTabIds") ?? "null") ??
|
||||
Array(6).fill(null),
|
||||
);
|
||||
const [focusedPaneIndex, setFocusedPaneIndex] = useState<number | null>(null);
|
||||
const [realHostTree, setRealHostTree] = useState<HostFolder | null>(null);
|
||||
const [hostsLoading, setHostsLoading] = useState(true);
|
||||
const [allHosts, setAllHosts] = useState<Host[]>([]);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [backgroundTabRecords, setBackgroundTabRecords] = useState<
|
||||
OpenTabRecord[]
|
||||
>([]);
|
||||
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [railView, setRailView] = useState<RailView>("hosts");
|
||||
const [profileDropdownOpen, setProfileDropdownOpen] = useState(false);
|
||||
const [sidebarWidth, setSidebarWidth] = useState(266);
|
||||
const [sidebarWidth, setSidebarWidth] = useState(() => {
|
||||
const saved = localStorage.getItem("termix_sidebarWidth");
|
||||
return saved ? parseInt(saved, 10) : 266;
|
||||
});
|
||||
const [sidebarDragging, setSidebarDragging] = useState(false);
|
||||
const [sidebarEditing, setSidebarEditing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("termix_sidebarWidth", String(sidebarWidth));
|
||||
}, [sidebarWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("termix_splitMode", splitMode);
|
||||
}, [splitMode]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("termix_paneTabIds", JSON.stringify(paneTabIds));
|
||||
}, [paneTabIds]);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const sidebarOpenBeforeMobile = useRef(sidebarOpen);
|
||||
@@ -170,6 +217,11 @@ export function AppShell({
|
||||
}, []);
|
||||
|
||||
const lastShiftTime = useRef(0);
|
||||
const [commandPaletteShortcutEnabled, setCommandPaletteShortcutEnabled] =
|
||||
useState<boolean>(() => {
|
||||
const v = localStorage.getItem("commandPaletteShortcutEnabled");
|
||||
return v !== null ? v === "true" : true;
|
||||
});
|
||||
const terminalRefs = useRef<Map<string, ReturnType<typeof createRef>>>(
|
||||
new Map(),
|
||||
);
|
||||
@@ -177,6 +229,25 @@ export function AppShell({
|
||||
(HTMLDivElement | null)[]
|
||||
>(Array(6).fill(null));
|
||||
|
||||
// Stable per-tab DOM nodes — created once per tab, never destroyed while the tab lives.
|
||||
// We always portal each tab's content into its own node, then move that node between
|
||||
// the normal-view container and the pane container via vanilla DOM so React's portal
|
||||
// target never changes (changing the target causes a remount).
|
||||
const tabNodesRef = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const normalViewRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const getTabNode = useCallback((tabId: string, isTerminal: boolean) => {
|
||||
if (!tabNodesRef.current.has(tabId)) {
|
||||
const el = document.createElement("div");
|
||||
el.style.position = "absolute";
|
||||
el.style.inset = "0";
|
||||
el.style.overflow = "hidden";
|
||||
if (!isTerminal) el.classList.add("bg-background");
|
||||
tabNodesRef.current.set(tabId, el);
|
||||
}
|
||||
return tabNodesRef.current.get(tabId)!;
|
||||
}, []);
|
||||
|
||||
const onPaneContentRef = useCallback(
|
||||
(paneIndex: number, el: HTMLDivElement | null) => {
|
||||
setPaneContentEls((prev) => {
|
||||
@@ -197,6 +268,7 @@ export function AppShell({
|
||||
snippets: "Snippets",
|
||||
history: "History",
|
||||
"split-screen": "Split Screen",
|
||||
connections: t("nav.connections"),
|
||||
"user-profile": "User Profile",
|
||||
"admin-settings": "Admin Settings",
|
||||
};
|
||||
@@ -204,15 +276,28 @@ export function AppShell({
|
||||
// Double-shift opens command palette
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "ShiftLeft") {
|
||||
if (e.code === "ShiftLeft" && !e.repeat) {
|
||||
const now = Date.now();
|
||||
if (now - lastShiftTime.current < 300)
|
||||
if (now - lastShiftTime.current < 300 && commandPaletteShortcutEnabled)
|
||||
setCommandPaletteOpen((prev) => !prev);
|
||||
lastShiftTime.current = now;
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [commandPaletteShortcutEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
const v = localStorage.getItem("commandPaletteShortcutEnabled");
|
||||
setCommandPaletteShortcutEnabled(v !== null ? v === "true" : true);
|
||||
};
|
||||
window.addEventListener("commandPaletteShortcutEnabledChanged", handler);
|
||||
return () =>
|
||||
window.removeEventListener(
|
||||
"commandPaletteShortcutEnabledChanged",
|
||||
handler,
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -275,6 +360,13 @@ export function AppShell({
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
getUserPreferences()
|
||||
.then((prefs) => setUserPrefs(prefs))
|
||||
.catch(() => {})
|
||||
.finally(() => setUserPrefsLoaded(true));
|
||||
}, []);
|
||||
|
||||
// Load real hosts from API
|
||||
const loadHosts = useCallback(async () => {
|
||||
try {
|
||||
@@ -286,6 +378,7 @@ export function AppShell({
|
||||
// Keep empty state on error
|
||||
} finally {
|
||||
setHostsLoading(false);
|
||||
setHostsLoaded(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -323,19 +416,149 @@ export function AppShell({
|
||||
return () => window.removeEventListener("termix:open-tab", handle);
|
||||
}, [allHosts]);
|
||||
|
||||
const PERSISTENT_TAB_TYPES: TabType[] = [
|
||||
"terminal",
|
||||
"rdp",
|
||||
"vnc",
|
||||
"telnet",
|
||||
"files",
|
||||
"docker",
|
||||
"stats",
|
||||
"tunnel",
|
||||
];
|
||||
|
||||
// On load: always read saved tabs from DB so background sessions are preserved across refreshes.
|
||||
// If reopenTabsOnLogin is on, also restore them as open tabs in the tab bar.
|
||||
const tabRestoreAttemptedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!hostsLoaded || !userPrefsLoaded) return;
|
||||
if (tabRestoreAttemptedRef.current) return;
|
||||
tabRestoreAttemptedRef.current = true;
|
||||
|
||||
async function loadSavedTabs() {
|
||||
try {
|
||||
const [savedTabs, activeSessions] = await Promise.all([
|
||||
getOpenTabs(),
|
||||
getActiveSessions(),
|
||||
]);
|
||||
|
||||
if (!Array.isArray(savedTabs) || savedTabs.length === 0) return;
|
||||
|
||||
const sessionByInstanceId = new Map(
|
||||
(Array.isArray(activeSessions) ? activeSessions : [])
|
||||
.filter((s) => s.tabInstanceId != null)
|
||||
.map((s) => [s.tabInstanceId, s]),
|
||||
);
|
||||
|
||||
if (userPrefs.reopenTabsOnLogin) {
|
||||
const hasPersistentTabs = tabs.some((t) =>
|
||||
PERSISTENT_TAB_TYPES.includes(t.type),
|
||||
);
|
||||
if (!hasPersistentTabs) {
|
||||
const restoredTabs: Tab[] = [];
|
||||
for (const saved of savedTabs as OpenTabRecord[]) {
|
||||
const host = saved.hostId
|
||||
? allHosts.find((h) => h.id === String(saved.hostId))
|
||||
: undefined;
|
||||
const hostlessTypes: TabType[] = ["dashboard", "tunnel"];
|
||||
if (!host && !hostlessTypes.includes(saved.tabType as TabType))
|
||||
continue;
|
||||
|
||||
// Singleton tabs use their type as the stable ID; host-bound tabs get a unique ID
|
||||
const tabId = host
|
||||
? `${host.name}-${saved.tabType}-${Date.now()}-${saved.tabOrder}`
|
||||
: saved.id;
|
||||
const liveSession = sessionByInstanceId.get(saved.id);
|
||||
const restoredSessionId =
|
||||
liveSession?.sessionId ?? saved.backendSessionId ?? null;
|
||||
|
||||
restoredTabs.push({
|
||||
id: tabId,
|
||||
instanceId: saved.id,
|
||||
type: saved.tabType as TabType,
|
||||
label: saved.label,
|
||||
host,
|
||||
openedAt: new Date(saved.createdAt).getTime(),
|
||||
restoredSessionId,
|
||||
terminalRef:
|
||||
saved.tabType === "terminal" ? createRef() : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (restoredTabs.length > 0) {
|
||||
setTabs((prev) => {
|
||||
const existingIds = new Set(prev.map((t) => t.id));
|
||||
const newTabs = restoredTabs.filter(
|
||||
(t) => !existingIds.has(t.id),
|
||||
);
|
||||
return newTabs.length > 0 ? [...prev, ...newTabs] : prev;
|
||||
});
|
||||
setActiveTabId(restoredTabs[0].id);
|
||||
}
|
||||
// Restored tabs are in the tab bar, not in background records
|
||||
}
|
||||
} else {
|
||||
// Not restoring to tab bar — keep as background records for ConnectionsPanel
|
||||
setBackgroundTabRecords(savedTabs as OpenTabRecord[]);
|
||||
}
|
||||
} catch {
|
||||
// silently fail
|
||||
} finally {
|
||||
setTabsReady(true);
|
||||
}
|
||||
}
|
||||
|
||||
loadSavedTabs();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hostsLoaded, userPrefsLoaded]);
|
||||
|
||||
// Debounced tab-order sync: when tab order changes, patch each persistent tab's tabOrder in DB.
|
||||
const orderSyncTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null,
|
||||
);
|
||||
const prevTabOrderRef = useRef<string>("");
|
||||
useEffect(() => {
|
||||
if (!tabsReady) return;
|
||||
const persistable = tabs.filter((t) =>
|
||||
PERSISTENT_TAB_TYPES.includes(t.type),
|
||||
);
|
||||
const orderKey = persistable.map((t) => t.instanceId).join(",");
|
||||
if (orderKey === prevTabOrderRef.current) return;
|
||||
prevTabOrderRef.current = orderKey;
|
||||
|
||||
if (orderSyncTimeoutRef.current) clearTimeout(orderSyncTimeoutRef.current);
|
||||
orderSyncTimeoutRef.current = setTimeout(() => {
|
||||
persistable.forEach((t, i) => {
|
||||
patchOpenTab(t.instanceId, { tabOrder: i }).catch(() => {});
|
||||
});
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
if (orderSyncTimeoutRef.current)
|
||||
clearTimeout(orderSyncTimeoutRef.current);
|
||||
};
|
||||
}, [tabs, tabsReady]);
|
||||
|
||||
// ─── Tab management ──────────────────────────────────────────────────────
|
||||
|
||||
const openTab = useCallback(function openTab(host: Host, type: TabType) {
|
||||
const openTab = useCallback(function openTab(
|
||||
host: Host,
|
||||
type: TabType,
|
||||
restore?: { instanceId: string; restoredSessionId: string | null },
|
||||
) {
|
||||
const tabId = `${host.name}-${type}-${Date.now()}`;
|
||||
const instanceId = restore?.instanceId ?? crypto.randomUUID();
|
||||
const openedAt = Date.now();
|
||||
const ref = type === "terminal" ? createRef() : undefined;
|
||||
if (ref) terminalRefs.current.set(tabId, ref);
|
||||
|
||||
let finalLabel = host.name;
|
||||
setTabs((prev) => {
|
||||
const same = prev.filter(
|
||||
(t) =>
|
||||
t.type === type && t.label.replace(/ \(\d+\)$/, "") === host.name,
|
||||
);
|
||||
const label =
|
||||
finalLabel =
|
||||
same.length === 0 ? host.name : `${host.name} (${same.length + 1})`;
|
||||
|
||||
// Retrofit the first duplicate's label to "(1)" if needed
|
||||
@@ -346,9 +569,31 @@ export function AppShell({
|
||||
)
|
||||
: prev;
|
||||
|
||||
return [...next, { id: tabId, type, label, host, terminalRef: ref }];
|
||||
return [
|
||||
...next,
|
||||
{
|
||||
id: tabId,
|
||||
instanceId,
|
||||
type,
|
||||
label: finalLabel,
|
||||
host,
|
||||
openedAt,
|
||||
terminalRef: ref,
|
||||
restoredSessionId: restore?.restoredSessionId ?? null,
|
||||
},
|
||||
];
|
||||
});
|
||||
setActiveTabId(tabId);
|
||||
|
||||
if (PERSISTENT_TAB_TYPES.includes(type)) {
|
||||
addOpenTab({
|
||||
id: instanceId,
|
||||
tabType: type,
|
||||
hostId: host ? parseInt(host.id) : null,
|
||||
label: finalLabel,
|
||||
tabOrder: 0,
|
||||
}).catch(() => {});
|
||||
}
|
||||
}, []);
|
||||
|
||||
function connectHost(host: Host, preferredType?: TabType) {
|
||||
@@ -398,17 +643,35 @@ export function AppShell({
|
||||
return;
|
||||
}
|
||||
const id = type;
|
||||
const singletonLabels: Partial<Record<TabType, string>> = {
|
||||
"host-manager": t("nav.hostManager"),
|
||||
docker: t("nav.docker"),
|
||||
tunnel: t("nav.tunnels"),
|
||||
network_graph: t("nav.networkGraph"),
|
||||
};
|
||||
setTabs((prev) => {
|
||||
if (prev.find((t) => t.id === id)) return prev;
|
||||
const singletonLabels: Partial<Record<TabType, string>> = {
|
||||
"host-manager": t("nav.hostManager"),
|
||||
docker: t("nav.docker"),
|
||||
tunnel: t("nav.tunnels"),
|
||||
network_graph: t("nav.networkGraph"),
|
||||
};
|
||||
return [...prev, { id, type, label: singletonLabels[type] ?? type }];
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
id,
|
||||
instanceId: id,
|
||||
type,
|
||||
label: singletonLabels[type] ?? type,
|
||||
openedAt: Date.now(),
|
||||
},
|
||||
];
|
||||
});
|
||||
setActiveTabId(id);
|
||||
if (PERSISTENT_TAB_TYPES.includes(type)) {
|
||||
addOpenTab({
|
||||
id,
|
||||
tabType: type,
|
||||
hostId: null,
|
||||
label: singletonLabels[type] ?? type,
|
||||
tabOrder: 0,
|
||||
}).catch(() => {});
|
||||
}
|
||||
},
|
||||
[t],
|
||||
);
|
||||
@@ -416,6 +679,14 @@ export function AppShell({
|
||||
const SESSION_TAB_TYPES: TabType[] = ["terminal", "rdp", "vnc", "telnet"];
|
||||
|
||||
function doCloseTab(id: string) {
|
||||
const tabToClose = tabs.find((t) => t.id === id);
|
||||
if (
|
||||
tabToClose?.instanceId &&
|
||||
PERSISTENT_TAB_TYPES.includes(tabToClose.type)
|
||||
) {
|
||||
deleteOpenTab(tabToClose.instanceId).catch(() => {});
|
||||
}
|
||||
|
||||
terminalRefs.current.delete(id);
|
||||
if (id === activeTabId) {
|
||||
const remaining = tabs.filter((t) => t.id !== id);
|
||||
@@ -423,11 +694,18 @@ export function AppShell({
|
||||
remaining.length > 0 ? remaining[remaining.length - 1].id : "dashboard",
|
||||
);
|
||||
}
|
||||
setPaneTabIds((prev) => prev.map((p) => (p === id ? null : p)));
|
||||
setTabs((prev) => {
|
||||
const next = prev.filter((t) => t.id !== id);
|
||||
if (next.length === 0)
|
||||
return [
|
||||
{ id: "dashboard", type: "dashboard", label: t("nav.dashboard") },
|
||||
{
|
||||
id: "dashboard",
|
||||
instanceId: "dashboard",
|
||||
type: "dashboard",
|
||||
label: t("nav.dashboard"),
|
||||
openedAt: Date.now(),
|
||||
},
|
||||
];
|
||||
return next;
|
||||
});
|
||||
@@ -466,6 +744,53 @@ export function AppShell({
|
||||
doCloseTab(id);
|
||||
}
|
||||
|
||||
function splitTabQuick(tabId: string, mode: SplitMode) {
|
||||
setSplitMode(mode);
|
||||
setPaneTabIds(() => {
|
||||
const count = PANE_COUNTS[mode];
|
||||
const next: (string | null)[] = Array(6).fill(null);
|
||||
next[0] = tabId;
|
||||
// Fill remaining panes with other non-dashboard tabs in order
|
||||
let slot = 1;
|
||||
for (const tab of tabs) {
|
||||
if (slot >= count) break;
|
||||
if (tab.id !== tabId && tab.type !== "dashboard") {
|
||||
next[slot] = tab.id;
|
||||
slot++;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function addTabToSplit(tabId: string) {
|
||||
setPaneTabIds((prev) => {
|
||||
// Remove from any current slot first
|
||||
const next = prev.map((p) => (p === tabId ? null : p));
|
||||
// Find first empty slot within the current pane count
|
||||
const count = PANE_COUNTS[splitMode];
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (!next[i]) {
|
||||
next[i] = tabId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function removeTabFromSplit(tabId: string) {
|
||||
setPaneTabIds((prev) => prev.map((p) => (p === tabId ? null : p)));
|
||||
}
|
||||
|
||||
function assignPane(paneIndex: number, tabId: string) {
|
||||
setPaneTabIds((prev) => {
|
||||
const next = prev.map((p) => (p === tabId ? null : p));
|
||||
next[paneIndex] = tabId;
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Rail / sidebar ──────────────────────────────────────────────────────
|
||||
|
||||
function handleRailClick(view: RailView) {
|
||||
@@ -528,8 +853,57 @@ export function AppShell({
|
||||
return () => cancelAnimationFrame(id);
|
||||
}, [splitMode, sidebarWidth, sidebarOpen]);
|
||||
|
||||
const activeTab = tabs.find((t) => t.id === activeTabId)!;
|
||||
const isSplit = splitMode !== "none";
|
||||
|
||||
// Move each tab's stable DOM node to the right container (pane or normal-view).
|
||||
// This is vanilla DOM so React's portal target never changes — changing the portal
|
||||
// target causes a remount which is exactly what we're trying to avoid.
|
||||
useEffect(() => {
|
||||
const normalView = normalViewRef.current;
|
||||
if (!normalView) return;
|
||||
|
||||
const tabIds = new Set(tabs.map((t) => t.id));
|
||||
|
||||
// Remove nodes for closed tabs
|
||||
for (const [id, node] of tabNodesRef.current) {
|
||||
if (!tabIds.has(id)) {
|
||||
node.remove();
|
||||
tabNodesRef.current.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const tab of tabs) {
|
||||
const isTerminal = tab.type === "terminal";
|
||||
const node = getTabNode(tab.id, isTerminal);
|
||||
const paneIdx = isSplit ? paneTabIds.indexOf(tab.id) : -1;
|
||||
const inPane = paneIdx !== -1;
|
||||
const paneEl = inPane ? paneContentEls[paneIdx] : null;
|
||||
const activeInline = !inPane && tab.id === activeTabId;
|
||||
|
||||
if (inPane && paneEl) {
|
||||
if (node.parentElement !== paneEl) paneEl.appendChild(node);
|
||||
node.style.visibility = "visible";
|
||||
node.style.pointerEvents = "auto";
|
||||
node.style.display = "";
|
||||
node.style.zIndex = "";
|
||||
} else {
|
||||
if (node.parentElement !== normalView) normalView.appendChild(node);
|
||||
if (isTerminal) {
|
||||
node.style.display = "";
|
||||
node.style.visibility = activeInline ? "visible" : "hidden";
|
||||
node.style.pointerEvents = activeInline ? "auto" : "none";
|
||||
node.style.zIndex = activeInline && !isSplit ? "1" : "0";
|
||||
} else {
|
||||
node.style.visibility = "";
|
||||
node.style.pointerEvents = "";
|
||||
node.style.zIndex = activeInline ? "2" : "";
|
||||
node.style.display = activeInline ? "" : "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const activeTab = tabs.find((t) => t.id === activeTabId)!;
|
||||
const terminalTabs = tabs.filter((t) => t.type === "terminal");
|
||||
|
||||
// Sidebar panel content — shared between desktop inline sidebar and mobile sheet
|
||||
@@ -601,13 +975,62 @@ export function AppShell({
|
||||
setSplitMode={setSplitMode}
|
||||
paneTabIds={paneTabIds}
|
||||
setPaneTabIds={setPaneTabIds}
|
||||
onAssignPane={assignPane}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{railView === "connections" && (
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
<ConnectionsPanel
|
||||
tabs={tabs}
|
||||
activeTabId={activeTabId}
|
||||
allHosts={allHosts}
|
||||
backgroundTabRecords={backgroundTabRecords}
|
||||
onSwitchToTab={(tabId) => {
|
||||
setActiveTabId(tabId);
|
||||
if (isMobile) setSidebarOpen(false);
|
||||
}}
|
||||
onCloseTab={closeTab}
|
||||
onReopenTab={(record, restoredSessionId) => {
|
||||
const host = record.hostId
|
||||
? allHosts.find((h) => h.id === String(record.hostId))
|
||||
: undefined;
|
||||
const hostlessTypes: TabType[] = ["tunnel"];
|
||||
if (!host && !hostlessTypes.includes(record.tabType as TabType))
|
||||
return;
|
||||
setBackgroundTabRecords((prev) =>
|
||||
prev.filter((r) => r.id !== record.id),
|
||||
);
|
||||
if (host) {
|
||||
const effectiveSessionId =
|
||||
restoredSessionId ?? record.backendSessionId ?? null;
|
||||
openTab(host, record.tabType as TabType, {
|
||||
instanceId: record.id,
|
||||
restoredSessionId: effectiveSessionId,
|
||||
});
|
||||
} else {
|
||||
openSingletonTab(record.tabType as TabType);
|
||||
}
|
||||
if (isMobile) setSidebarOpen(false);
|
||||
}}
|
||||
onForgetBackground={(recordId) => {
|
||||
setBackgroundTabRecords((prev) =>
|
||||
prev.filter((r) => r.id !== recordId),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{railView === "user-profile" && (
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
<UserProfilePanel username={username} onLogout={onLogout} />
|
||||
<UserProfilePanel
|
||||
username={username}
|
||||
onLogout={onLogout}
|
||||
userPrefs={userPrefs}
|
||||
onPrefsChange={setUserPrefs}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -659,6 +1082,10 @@ export function AppShell({
|
||||
railView={railView}
|
||||
sidebarOpen={sidebarOpen}
|
||||
splitMode={splitMode}
|
||||
connectionCount={
|
||||
tabs.filter((t) => PERSISTENT_TAB_TYPES.includes(t.type)).length +
|
||||
backgroundTabRecords.length
|
||||
}
|
||||
username={username}
|
||||
isAdmin={isAdmin}
|
||||
profileDropdownOpen={profileDropdownOpen}
|
||||
@@ -721,10 +1148,16 @@ export function AppShell({
|
||||
<TabBar
|
||||
tabs={tabs}
|
||||
activeTabId={activeTabId}
|
||||
splitMode={splitMode}
|
||||
paneTabIds={paneTabIds}
|
||||
focusedPaneIndex={focusedPaneIndex}
|
||||
onSetActiveTab={setActiveTabId}
|
||||
onCloseTab={closeTab}
|
||||
onRefreshTab={refreshTab}
|
||||
onReorderTabs={setTabs}
|
||||
onSplitTab={splitTabQuick}
|
||||
onAddToSplit={addTabToSplit}
|
||||
onRemoveFromSplit={removeTabFromSplit}
|
||||
/>
|
||||
<div className="relative flex flex-col flex-1 min-h-0 overflow-hidden">
|
||||
{/* Split view — always mounted when not mobile, hidden via CSS when inactive */}
|
||||
@@ -740,86 +1173,42 @@ export function AppShell({
|
||||
tabs={tabs}
|
||||
paneTabIds={paneTabIds}
|
||||
splitMode={splitMode}
|
||||
onOpenSingletonTab={openSingletonTab}
|
||||
onOpenTab={openTab}
|
||||
focusedPaneIndex={focusedPaneIndex}
|
||||
onTerminalResize={resizeAllTerminals}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
onPaneClick={setFocusedPaneIndex}
|
||||
onAssignPane={assignPane}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Normal tab view — always mounted, hidden via CSS when split is active */}
|
||||
{/* Normal-view container. Tab nodes are appended here (or to pane elements)
|
||||
by the DOM-placement effect above. React portals each tab's content
|
||||
into its stable per-tab node so the component is never remounted.
|
||||
Hidden when split is active — pane-assigned nodes escape via vanilla DOM
|
||||
appendChild to paneEl, so hiding this doesn't affect them. */}
|
||||
<div
|
||||
className="absolute inset-0 flex flex-col"
|
||||
style={{ display: isSplit && !isMobile ? "none" : "flex" }}
|
||||
ref={normalViewRef}
|
||||
className="absolute inset-0"
|
||||
style={{ display: isSplit && !isMobile ? "none" : undefined }}
|
||||
>
|
||||
{/* Terminal tabs: always in DOM, visibility-toggled so xterm keeps its dimensions.
|
||||
When split is active and this tab is assigned to a pane, portal it there. */}
|
||||
{tabs
|
||||
.filter((tab) => tab.type === "terminal")
|
||||
.map((tab) => {
|
||||
const paneIdx = isSplit ? paneTabIds.indexOf(tab.id) : -1;
|
||||
const inPane = paneIdx !== -1;
|
||||
const paneEl = inPane ? paneContentEls[paneIdx] : null;
|
||||
const visible = inPane || tab.id === activeTabId;
|
||||
|
||||
const terminalContent = renderTabContent(
|
||||
{tabs.map((tab) => {
|
||||
const tabNode = getTabNode(tab.id, tab.type === "terminal");
|
||||
const paneIdx = isSplit ? paneTabIds.indexOf(tab.id) : -1;
|
||||
const inPane = paneIdx !== -1;
|
||||
const activeInline = !inPane && tab.id === activeTabId;
|
||||
return createPortal(
|
||||
renderTabContent(
|
||||
tab,
|
||||
openSingletonTab,
|
||||
openTab,
|
||||
closeTab,
|
||||
visible,
|
||||
);
|
||||
|
||||
if (inPane && paneEl) {
|
||||
return createPortal(
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
{terminalContent}
|
||||
</div>,
|
||||
paneEl,
|
||||
tab.id,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={tab.id}
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
style={{
|
||||
visibility: visible ? "visible" : "hidden",
|
||||
pointerEvents: visible ? "auto" : "none",
|
||||
zIndex: tab.id === activeTabId && !isSplit ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
{terminalContent}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Non-terminal tabs */}
|
||||
{tabs
|
||||
.filter((tab) => tab.type !== "terminal")
|
||||
.map((tab) => {
|
||||
const visible = tab.id === activeTabId;
|
||||
return (
|
||||
<div
|
||||
key={tab.id}
|
||||
className="flex flex-col overflow-hidden bg-background"
|
||||
style={
|
||||
visible
|
||||
? { position: "absolute", inset: 0, zIndex: 2 }
|
||||
: { display: "none" }
|
||||
}
|
||||
>
|
||||
{renderTabContent(
|
||||
tab,
|
||||
openSingletonTab,
|
||||
openTab,
|
||||
closeTab,
|
||||
visible,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
inPane || activeInline,
|
||||
),
|
||||
tabNode,
|
||||
tab.id,
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,13 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
"relative w-full border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
default: "bg-background text-foreground border-border",
|
||||
destructive:
|
||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
"text-destructive bg-background border-destructive/40 [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -38,21 +38,23 @@ export function SettingRow({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-3 border-b border-border last:border-0">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm font-medium">{label}</span>
|
||||
<div className="flex items-center justify-between gap-3 py-3 border-b border-border last:border-0">
|
||||
<div className="flex flex-col gap-0.5 min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="text-sm font-medium leading-snug">{label}</span>
|
||||
{badge && (
|
||||
<span className="text-[10px] font-bold text-yellow-500 border border-yellow-500/40 px-1">
|
||||
<span className="text-[10px] font-bold text-yellow-500 border border-yellow-500/40 px-1 shrink-0">
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{description && (
|
||||
<span className="text-xs text-muted-foreground">{description}</span>
|
||||
<span className="text-xs text-muted-foreground leading-snug">
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="shrink-0 ml-4 md:ml-8">{children}</div>
|
||||
<div className="shrink-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,9 @@ function sshHostToHost(h: SSHHostWithStatus): Host {
|
||||
name: a.name,
|
||||
snippetId: String(a.snippetId),
|
||||
})),
|
||||
jumpHosts: (h.jumpHosts ?? []).map((j) => ({
|
||||
hostId: String(j.hostId),
|
||||
})),
|
||||
serverTunnels: [],
|
||||
defaultPath: h.defaultPath,
|
||||
terminalConfig: h.terminalConfig as Host["terminalConfig"],
|
||||
@@ -88,6 +91,7 @@ function sshHostToHost(h: SSHHostWithStatus): Host {
|
||||
socks5Port: h.socks5Port,
|
||||
socks5Username: h.socks5Username,
|
||||
socks5Password: h.socks5Password,
|
||||
socks5ProxyChain: h.socks5ProxyChain ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,10 @@ export function NetworkGraphCard({
|
||||
const { t } = useTranslation();
|
||||
const { addTab } = useTabsSafe();
|
||||
|
||||
// Gate Cytoscape mounting on the container actually having non-zero dimensions.
|
||||
// This avoids the "bb is undefined" crash when the card is hidden via display:none.
|
||||
const [containerReady, setContainerReady] = useState(false);
|
||||
|
||||
const [elements, setElements] = useState<NetworkElement[]>([]);
|
||||
const [hosts, setHosts] = useState<SSHHostWithStatus[]>([]);
|
||||
const [hostMap, setHostMap] = useState<HostMap>({});
|
||||
@@ -230,6 +234,21 @@ export function NetworkGraphCard({
|
||||
|
||||
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerReady) return;
|
||||
const el = cyContainerRef.current;
|
||||
if (!el) return;
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
const { width, height } = entries[0].contentRect;
|
||||
if (width > 0 && height > 0) {
|
||||
setContainerReady(true);
|
||||
ro.disconnect();
|
||||
}
|
||||
});
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, [containerReady]);
|
||||
|
||||
const cyRef = useRef<cytoscape.Core | null>(null);
|
||||
const statusIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -864,15 +883,17 @@ export function NetworkGraphCard({
|
||||
</div>
|
||||
)}
|
||||
{contextMenuEl}
|
||||
<CytoscapeComponent
|
||||
elements={elements}
|
||||
style={{ width: "100%", height: "100%", background: "transparent" }}
|
||||
layout={{ name: "preset" }}
|
||||
cy={handleNodeInit}
|
||||
wheelSensitivity={1.5}
|
||||
minZoom={0.2}
|
||||
maxZoom={3}
|
||||
/>
|
||||
{containerReady && (
|
||||
<CytoscapeComponent
|
||||
elements={elements}
|
||||
style={{ width: "100%", height: "100%", background: "transparent" }}
|
||||
layout={{ name: "preset" }}
|
||||
cy={handleNodeInit}
|
||||
wheelSensitivity={1.5}
|
||||
minZoom={0.2}
|
||||
maxZoom={3}
|
||||
/>
|
||||
)}
|
||||
{!loading && elements.length === 0 && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 pointer-events-none">
|
||||
<Network className="size-8 text-muted-foreground/30" />
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/card.tsx";
|
||||
import { Button } from "@/components/button.tsx";
|
||||
import { Badge } from "@/components/badge.tsx";
|
||||
import {
|
||||
@@ -28,21 +21,29 @@ interface AlertCardProps {
|
||||
const getAlertIcon = (type?: string) => {
|
||||
switch (type) {
|
||||
case "warning":
|
||||
return <AlertTriangle className="h-5 w-5 text-yellow-500" />;
|
||||
return <AlertTriangle className="h-5 w-5 text-accent-brand" />;
|
||||
case "error":
|
||||
return <AlertCircle className="h-5 w-5 text-red-500" />;
|
||||
return <AlertCircle className="h-5 w-5 text-destructive" />;
|
||||
case "success":
|
||||
return <CheckCircle className="h-5 w-5 text-green-500" />;
|
||||
return <CheckCircle className="h-5 w-5 text-green-400" />;
|
||||
case "info":
|
||||
default:
|
||||
return <Info className="h-5 w-5 text-blue-500" />;
|
||||
return <Info className="h-5 w-5 text-muted-foreground" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityBadgeVariant = (priority?: string) => {
|
||||
const getAccentBarClass = (priority?: string, type?: string): string => {
|
||||
if (priority === "critical" || type === "error") return "bg-destructive";
|
||||
if (priority === "high" || type === "warning") return "bg-accent-brand";
|
||||
if (priority === "medium" || type === "success") return "bg-green-400";
|
||||
return "bg-border";
|
||||
};
|
||||
|
||||
const getPriorityBadgeVariant = (
|
||||
priority?: string,
|
||||
): "destructive" | "secondary" | "outline" => {
|
||||
switch (priority) {
|
||||
case "critical":
|
||||
return "destructive";
|
||||
case "high":
|
||||
return "destructive";
|
||||
case "medium":
|
||||
@@ -53,14 +54,15 @@ const getPriorityBadgeVariant = (priority?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeBadgeVariant = (type?: string) => {
|
||||
const getTypeBadgeVariant = (
|
||||
type?: string,
|
||||
): "destructive" | "secondary" | "outline" => {
|
||||
switch (type) {
|
||||
case "warning":
|
||||
return "secondary";
|
||||
case "error":
|
||||
return "destructive";
|
||||
case "warning":
|
||||
return "secondary";
|
||||
case "success":
|
||||
return "default";
|
||||
case "info":
|
||||
default:
|
||||
return "outline";
|
||||
@@ -84,59 +86,61 @@ export function AlertCard({
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-2xl mx-auto">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{getAlertIcon(alert.type)}
|
||||
<CardTitle className="text-xl font-bold">{alert.title}</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="w-full border border-edge rounded-md !bg-elevated overflow-hidden">
|
||||
<div className={"h-1 w-full " + getAccentBarClass(alert.priority, alert.type)} />
|
||||
|
||||
<div className="flex items-start justify-between px-4 pt-4 pb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
{getAlertIcon(alert.type)}
|
||||
<span className="text-sm font-semibold">{alert.title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Button variant="ghost" size="icon-sm" onClick={onClose}>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{(alert.priority || alert.type) && (
|
||||
<div className="flex items-center gap-2 px-4 pb-3">
|
||||
{alert.priority && (
|
||||
<Badge variant={getPriorityBadgeVariant(alert.priority)}>
|
||||
{alert.priority.toUpperCase()}
|
||||
</Badge>
|
||||
)}
|
||||
{alert.type && (
|
||||
<Badge variant={getTypeBadgeVariant(alert.type)}>
|
||||
<Badge
|
||||
variant={getTypeBadgeVariant(alert.type)}
|
||||
className={alert.type === "success" ? "text-green-400" : undefined}
|
||||
>
|
||||
{alert.type}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-4">
|
||||
<p className="text-muted-foreground leading-relaxed whitespace-pre-wrap">
|
||||
)}
|
||||
|
||||
<div className="mx-3 mb-3 border border-edge rounded-md px-3 py-3 !bg-canvas">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed whitespace-pre-wrap">
|
||||
{alert.message}
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter className="flex items-center justify-between pt-0">
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleDismiss}>
|
||||
{t("common.dismiss")}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 px-4 pb-4">
|
||||
<Button variant="outline" size="sm" onClick={handleDismiss}>
|
||||
{t("common.dismiss")}
|
||||
</Button>
|
||||
{alert.actionUrl && alert.actionText && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
window.open(alert.actionUrl, "_blank", "noopener,noreferrer")
|
||||
}
|
||||
className="gap-1.5 text-accent-brand border-accent-brand/30 hover:border-accent-brand/60"
|
||||
>
|
||||
{alert.actionText}
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{alert.actionUrl && alert.actionText && (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() =>
|
||||
window.open(alert.actionUrl, "_blank", "noopener,noreferrer")
|
||||
}
|
||||
className="gap-2"
|
||||
>
|
||||
{alert.actionText}
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,53 +119,46 @@ export function AlertManager({
|
||||
return null;
|
||||
}
|
||||
|
||||
const priorityCounts = { critical: 0, high: 0, medium: 0, low: 0 };
|
||||
alerts.forEach((alert) => {
|
||||
const priority = alert.priority || "low";
|
||||
priorityCounts[priority as keyof typeof priorityCounts]++;
|
||||
});
|
||||
const hasMultipleAlerts = alerts.length > 1;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm z-[99999]">
|
||||
<div className="relative w-full max-w-2xl mx-4">
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-background/80 z-[99999]">
|
||||
<div className="w-full max-w-2xl mx-4 flex flex-col gap-2">
|
||||
{hasMultipleAlerts && (
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{currentAlertIndex + 1} {t("common.of")} {alerts.length}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handlePreviousAlert}
|
||||
disabled={currentAlertIndex === 0}
|
||||
>
|
||||
{t("common.previous")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleNextAlert}
|
||||
disabled={currentAlertIndex === alerts.length - 1}
|
||||
>
|
||||
{t("common.next")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AlertCard
|
||||
alert={currentAlert}
|
||||
onDismiss={handleDismissAlert}
|
||||
onClose={handleCloseCurrentAlert}
|
||||
/>
|
||||
|
||||
{hasMultipleAlerts && (
|
||||
<div className="absolute -bottom-16 left-1/2 transform -translate-x-1/2 flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handlePreviousAlert}
|
||||
disabled={currentAlertIndex === 0}
|
||||
className="h-8 px-3"
|
||||
>
|
||||
{t("common.previous")}
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{currentAlertIndex + 1} {t("common.of")} {alerts.length}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleNextAlert}
|
||||
disabled={currentAlertIndex === alerts.length - 1}
|
||||
className="h-8 px-3"
|
||||
>
|
||||
{t("common.next")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="absolute -bottom-20 left-1/2 transform -translate-x-1/2">
|
||||
<div className="bg-destructive text-destructive-foreground px-3 py-1 rounded text-sm">
|
||||
{error}
|
||||
</div>
|
||||
<div className="border border-destructive/50 rounded-md px-3 py-2 text-destructive text-xs">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -96,11 +96,6 @@ interface FileManagerGridProps {
|
||||
}
|
||||
|
||||
const getFileTypeColor = (file: FileItem): string => {
|
||||
const colorEnabled = localStorage.getItem("fileColorCoding") !== "false";
|
||||
if (!colorEnabled) {
|
||||
return "text-muted-foreground";
|
||||
}
|
||||
|
||||
if (file.type === "directory") {
|
||||
return "text-red-400";
|
||||
}
|
||||
@@ -985,7 +980,7 @@ export function FileManagerGrid({
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
className="text-[11px] font-bold uppercase tracking-tight text-center truncate w-full px-1"
|
||||
className="text-[11px] font-bold tracking-tight text-center truncate w-full px-1"
|
||||
title={file.name}
|
||||
>
|
||||
{file.name}
|
||||
@@ -1108,7 +1103,7 @@ export function FileManagerGrid({
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="font-bold truncate uppercase tracking-tight"
|
||||
className="font-bold truncate tracking-tight"
|
||||
title={file.name}
|
||||
>
|
||||
{file.name}
|
||||
|
||||
@@ -15,9 +15,14 @@ import type { SSHHost } from "@/types";
|
||||
interface GuacamoleAppProps {
|
||||
hostId?: string;
|
||||
tabId?: string;
|
||||
protocol?: "rdp" | "vnc" | "telnet";
|
||||
}
|
||||
|
||||
const GuacamoleApp: React.FC<GuacamoleAppProps> = ({ hostId, tabId }) => {
|
||||
const GuacamoleApp: React.FC<GuacamoleAppProps> = ({
|
||||
hostId,
|
||||
tabId,
|
||||
protocol,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -76,6 +81,7 @@ const GuacamoleApp: React.FC<GuacamoleAppProps> = ({ hostId, tabId }) => {
|
||||
hostId={parseInt(hostId, 10)}
|
||||
hostConfig={hostConfig}
|
||||
tabId={tabId}
|
||||
protocol={protocol}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
@@ -87,12 +93,14 @@ interface GuacamoleAppInnerProps {
|
||||
hostId: number;
|
||||
hostConfig: Pick<SSHHost, "connectionType">;
|
||||
tabId?: string;
|
||||
protocol?: "rdp" | "vnc" | "telnet";
|
||||
}
|
||||
|
||||
const GuacamoleAppInner: React.FC<GuacamoleAppInnerProps> = ({
|
||||
hostId,
|
||||
hostConfig,
|
||||
tabId,
|
||||
protocol,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
@@ -110,7 +118,7 @@ const GuacamoleAppInner: React.FC<GuacamoleAppInnerProps> = ({
|
||||
setError(t("guacamole.guacdUnavailable"));
|
||||
return;
|
||||
}
|
||||
return getGuacamoleTokenFromHost(hostId);
|
||||
return getGuacamoleTokenFromHost(hostId, protocol);
|
||||
})
|
||||
.then((result) => {
|
||||
if (result) setToken(result.token);
|
||||
@@ -172,14 +180,21 @@ const GuacamoleAppInner: React.FC<GuacamoleAppInnerProps> = ({
|
||||
<SimpleLoader
|
||||
visible={true}
|
||||
message={t("guacamole.connecting", {
|
||||
type: (hostConfig.connectionType || "remote").toUpperCase(),
|
||||
type: (
|
||||
protocol ||
|
||||
hostConfig.connectionType ||
|
||||
"remote"
|
||||
).toUpperCase(),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const protocol = hostConfig.connectionType as "rdp" | "vnc" | "telnet";
|
||||
const resolvedProtocol = (protocol ?? hostConfig.connectionType) as
|
||||
| "rdp"
|
||||
| "vnc"
|
||||
| "telnet";
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
@@ -213,13 +228,17 @@ const GuacamoleAppInner: React.FC<GuacamoleAppInnerProps> = ({
|
||||
<GuacamoleDisplay
|
||||
key={token}
|
||||
ref={displayRef}
|
||||
connectionConfig={{ token, protocol, type: protocol }}
|
||||
connectionConfig={{
|
||||
token,
|
||||
protocol: resolvedProtocol,
|
||||
type: resolvedProtocol,
|
||||
}}
|
||||
isVisible={true}
|
||||
onError={(err) => setConnectionError(err)}
|
||||
/>
|
||||
<GuacamoleToolbar
|
||||
displayRef={displayRef}
|
||||
protocol={protocol}
|
||||
protocol={resolvedProtocol}
|
||||
onReconnect={handleReconnect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -373,6 +373,7 @@ export const GuacamoleDisplay = forwardRef<
|
||||
const h = Math.round(rect.height);
|
||||
if (w > 0 && h > 0) client.sendSize(w, h);
|
||||
}
|
||||
rescaleDisplay(false);
|
||||
break;
|
||||
case 4:
|
||||
break;
|
||||
@@ -517,7 +518,10 @@ export const GuacamoleDisplay = forwardRef<
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const w = Math.round(rect.width);
|
||||
const h = Math.round(rect.height);
|
||||
if (w > 0 && h > 0) clientRef.current.sendSize(w, h);
|
||||
if (w > 0 && h > 0) {
|
||||
clientRef.current.sendSize(w, h);
|
||||
rescaleDisplay(true);
|
||||
}
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
@@ -527,7 +531,7 @@ export const GuacamoleDisplay = forwardRef<
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
}, [rescaleDisplay]);
|
||||
|
||||
const syncClipboard = useCallback(() => {
|
||||
const client = clientRef.current;
|
||||
|
||||
@@ -96,6 +96,7 @@ function resolveTermixThemeColors(activeTheme: string, appTheme: string) {
|
||||
interface HostConfig {
|
||||
id?: number;
|
||||
instanceId?: string;
|
||||
restoredSessionId?: string | null;
|
||||
ip: string;
|
||||
port: number;
|
||||
username: string;
|
||||
@@ -231,6 +232,8 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
|
||||
const sessionIdRef = useRef<string | null>(null);
|
||||
const isAttachingSessionRef = useRef<boolean>(false);
|
||||
// Consumed on first connectToHost call so retries don't re-attempt a stale session
|
||||
const pendingRestoredSessionIdRef = useRef<string | null>(hostConfig.restoredSessionId ?? null);
|
||||
const [tmuxSessionPicker, setTmuxSessionPicker] = useState<{
|
||||
sessions: Array<{
|
||||
name: string;
|
||||
@@ -254,6 +257,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
const isReconnectingRef = useRef(false);
|
||||
const isConnectingRef = useRef(false);
|
||||
const wasConnectedRef = useRef(false);
|
||||
const wasSessionExpiredRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
isUnmountingRef.current = false;
|
||||
@@ -685,8 +689,6 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
if (webSocketRef.current?.readyState === WebSocket.OPEN) {
|
||||
webSocketRef.current.send(JSON.stringify({ type: "disconnect" }));
|
||||
}
|
||||
const tabId = hostConfig.id ?? "default";
|
||||
localStorage.removeItem(`termix_session_${tabId}`);
|
||||
sessionIdRef.current = null;
|
||||
webSocketRef.current?.close();
|
||||
setIsConnected(false);
|
||||
@@ -983,23 +985,19 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
currentHostIdRef.current = hostConfig.id;
|
||||
currentHostConfigRef.current = hostConfig;
|
||||
|
||||
const persistenceEnabled =
|
||||
localStorage.getItem("enableTerminalSessionPersistence") !== "false";
|
||||
const tabId = hostConfig.instanceId
|
||||
? `${hostConfig.id}_${hostConfig.instanceId}`
|
||||
: `${hostConfig.id}_${Date.now()}`;
|
||||
const savedSessionId = persistenceEnabled
|
||||
? localStorage.getItem(`termix_session_${tabId}`)
|
||||
: null;
|
||||
if (savedSessionId) {
|
||||
sessionIdRef.current = savedSessionId;
|
||||
// Consume the pending restored session ID once; retries get null so they create fresh connections
|
||||
const restoredSessionId = pendingRestoredSessionIdRef.current;
|
||||
pendingRestoredSessionIdRef.current = null;
|
||||
|
||||
if (restoredSessionId) {
|
||||
sessionIdRef.current = restoredSessionId;
|
||||
isAttachingSessionRef.current = true;
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "attachSession",
|
||||
data: {
|
||||
sessionId: savedSessionId,
|
||||
sessionId: restoredSessionId,
|
||||
cols,
|
||||
rows,
|
||||
tabInstanceId: hostConfig.instanceId,
|
||||
@@ -1484,12 +1482,10 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
}
|
||||
} else if (msg.type === "sessionCreated") {
|
||||
sessionIdRef.current = msg.sessionId;
|
||||
const persistenceEnabled =
|
||||
localStorage.getItem("enableTerminalSessionPersistence") !==
|
||||
"false";
|
||||
if (persistenceEnabled && hostConfig.instanceId) {
|
||||
const tabId = `${hostConfig.id}_${hostConfig.instanceId}`;
|
||||
localStorage.setItem(`termix_session_${tabId}`, msg.sessionId);
|
||||
if (hostConfig.instanceId) {
|
||||
import("@/main-axios").then(({ patchOpenTab }) => {
|
||||
patchOpenTab(hostConfig.instanceId!, { backendSessionId: msg.sessionId }).catch(() => {});
|
||||
});
|
||||
}
|
||||
} else if (msg.type === "sessionAttached") {
|
||||
isAttachingSessionRef.current = false;
|
||||
@@ -1520,22 +1516,18 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
});
|
||||
} else if (msg.type === "sessionExpired") {
|
||||
isAttachingSessionRef.current = false;
|
||||
shouldNotReconnectRef.current = false;
|
||||
if (hostConfig.instanceId) {
|
||||
const tabId = `${hostConfig.id}_${hostConfig.instanceId}`;
|
||||
localStorage.removeItem(`termix_session_${tabId}`);
|
||||
}
|
||||
sessionIdRef.current = null;
|
||||
|
||||
wasSessionExpiredRef.current = true;
|
||||
if (hostConfig.instanceId) {
|
||||
import("@/main-axios").then(({ patchOpenTab }) => {
|
||||
patchOpenTab(hostConfig.instanceId!, { backendSessionId: null }).catch(() => {});
|
||||
});
|
||||
}
|
||||
if (webSocketRef.current) {
|
||||
webSocketRef.current.close();
|
||||
}
|
||||
} else if (msg.type === "sessionTakenOver") {
|
||||
if (sessionIdRef.current && hostConfig.instanceId) {
|
||||
const tabId = `${hostConfig.id}_${hostConfig.instanceId}`;
|
||||
localStorage.removeItem(`termix_session_${tabId}`);
|
||||
sessionIdRef.current = null;
|
||||
}
|
||||
sessionIdRef.current = null;
|
||||
|
||||
if (terminal) {
|
||||
terminal.clear();
|
||||
@@ -1631,6 +1623,14 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
totpTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
if (wasSessionExpiredRef.current) {
|
||||
wasSessionExpiredRef.current = false;
|
||||
const cols = terminal?.cols || 80;
|
||||
const rows = terminal?.rows || 24;
|
||||
connectToHost(cols, rows);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.code === 1006) {
|
||||
console.warn(
|
||||
"[WebSocket] Abnormal closure detected - attempting reconnection",
|
||||
@@ -2164,18 +2164,6 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
pongTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
const persistenceEnabled =
|
||||
localStorage.getItem("enableTerminalSessionPersistence") !==
|
||||
"false";
|
||||
if (
|
||||
!persistenceEnabled &&
|
||||
sessionIdRef.current &&
|
||||
currentInstanceId
|
||||
) {
|
||||
const tabId = `${currentHostId}_${currentInstanceId}`;
|
||||
localStorage.removeItem(`termix_session_${tabId}`);
|
||||
}
|
||||
|
||||
if (webSocketRef.current) {
|
||||
webSocketRef.current.close();
|
||||
}
|
||||
|
||||
+78
-11
@@ -153,6 +153,7 @@
|
||||
"snippets": "Snippets",
|
||||
"hostManager": "Host Manager",
|
||||
"credentials": "Credentials",
|
||||
"connections": "Connections",
|
||||
"roleAdministrator": "Administrator",
|
||||
"roleUser": "User"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Fast Scroll Sensitivity",
|
||||
"moshCommandLabel": "Mosh Command",
|
||||
"startupSnippetLabel": "Startup Snippet",
|
||||
"keepaliveIntervalLabel": "Keepalive Interval",
|
||||
"keepaliveIntervalLabel": "Keepalive Interval (seconds)",
|
||||
"maxKeepaliveMisses": "Max Keepalive Misses",
|
||||
"tunnelSettings": "Tunnel Settings",
|
||||
"enableTunneling": "Enable Tunneling",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL copied",
|
||||
"telnetUrlCopied": "Telnet URL copied",
|
||||
"remoteDesktopUrlCopied": "Remote Desktop URL copied",
|
||||
"wakeOnLanAction": "Wake on LAN",
|
||||
"wakeOnLanSuccess": "Magic packet sent to {{name}}",
|
||||
"wakeOnLanError": "Failed to send magic packet",
|
||||
"cloneHostAction": "Clone Host",
|
||||
"copyAddress": "Copy Address",
|
||||
"copyLink": "Copy Link",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "No hosts match your search",
|
||||
"hostNotFound": "Host not found",
|
||||
"searchHosts": "Search hosts...",
|
||||
"sortHosts": "Sort Hosts",
|
||||
"sortDefault": "Default Order",
|
||||
"sortNameAsc": "Name (A → Z)",
|
||||
"sortNameDesc": "Name (Z → A)",
|
||||
"sortIpAsc": "IP Address (Asc)",
|
||||
"sortIpDesc": "IP Address (Desc)",
|
||||
"sortOnlineFirst": "Online First",
|
||||
"sortOfflineFirst": "Offline First",
|
||||
"sortPinnedFirst": "Pinned First",
|
||||
"filterHosts": "Filter Hosts",
|
||||
"filterClearAll": "Clear Filters",
|
||||
"filterStatusGroup": "Status",
|
||||
"filterOnline": "Online",
|
||||
"filterOffline": "Offline",
|
||||
"filterPinned": "Pinned",
|
||||
"filterAuthGroup": "Auth Type",
|
||||
"filterAuthPassword": "Password",
|
||||
"filterAuthKey": "SSH Key",
|
||||
"filterAuthCredential": "Credential",
|
||||
"filterAuthNone": "None",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protocol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Features",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "File Manager",
|
||||
"filterFeatureTunnel": "Tunnel",
|
||||
"filterFeatureDocker": "Docker",
|
||||
"filterTagsGroup": "Tags",
|
||||
"addHost": "Add Host",
|
||||
"shareHost": "Share Host",
|
||||
"shareHostTitle": "Share: {{name}}",
|
||||
@@ -741,7 +777,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "No tab assigned"
|
||||
"noTabAssigned": "No tab assigned",
|
||||
"focusedPane": "Active pane"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "No connections",
|
||||
"noConnectionsDesc": "Open a terminal, file manager, or remote desktop to see connections here",
|
||||
"connectedFor": "Connected for {{duration}}",
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"closeTab": "Close tab",
|
||||
"closeConnection": "Close connection",
|
||||
"forgetTab": "Forget",
|
||||
"removeBackground": "Remove",
|
||||
"reconnect": "Reconnect",
|
||||
"reopenTab": "Reopen",
|
||||
"sectionOpen": "Open",
|
||||
"sectionBackground": "Background",
|
||||
"backgroundDesc": "Sessions persist for 30 minutes after disconnect and can be reconnected.",
|
||||
"persisted": "Persisted in background",
|
||||
"expiresIn": "Expires in {{duration}}",
|
||||
"search": "Search connections...",
|
||||
"noSearchResults": "No connections match your search"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Connecting to {{type}} session...",
|
||||
@@ -1239,7 +1296,7 @@
|
||||
"retry": "Retry"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH SERVER MANAGER",
|
||||
"tagline": "Self-hosted SSH and remote desktop management",
|
||||
"loginTitle": "Login to Termix",
|
||||
"registerTitle": "Create Account",
|
||||
"forgotPassword": "Forgot Password?",
|
||||
@@ -1798,11 +1855,17 @@
|
||||
"selectLayoutHint": "Choose how many panes to display",
|
||||
"panesTitle": "Panes",
|
||||
"openTabsTitle": "Open Tabs",
|
||||
"dragTabsHint": "Drag tabs into panes above",
|
||||
"dragTabsHint": "Drag tabs into panes above, or use Quick Assign",
|
||||
"dropHere": "Drop here",
|
||||
"emptyPane": "Empty",
|
||||
"dashboard": "Dashboard",
|
||||
"clearSplitScreen": "Clear Split Screen"
|
||||
"clearSplitScreen": "Clear Split Screen",
|
||||
"quickAssign": "Quick Assign",
|
||||
"alreadyAssigned": "Pane {{index}}",
|
||||
"splitTab": "Split Tab",
|
||||
"addToSplit": "Add to Split",
|
||||
"removeFromSplit": "Remove from Split",
|
||||
"assignToPane": "Assign to pane"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Create Snippet",
|
||||
@@ -1856,6 +1919,8 @@
|
||||
"folderDeleteFailed": "Failed to delete folder",
|
||||
"folderEditSuccess": "Folder updated successfully",
|
||||
"folderEditFailed": "Failed to update folder",
|
||||
"confirmRunMessage": "Run \"{{name}}\"?",
|
||||
"confirmRunButton": "Run",
|
||||
"runSuccess": "Ran \"{{name}}\" in {{count}} terminal(s)",
|
||||
"copySuccess": "Copied \"{{name}}\" to clipboard",
|
||||
"shareTitle": "Share Snippet",
|
||||
@@ -1870,7 +1935,8 @@
|
||||
"currentAccess": "Current Access",
|
||||
"shareLoadError": "Failed to load share data",
|
||||
"loading": "Loading...",
|
||||
"close": "Close"
|
||||
"close": "Close",
|
||||
"reorderFailed": "Failed to save snippet order"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Account",
|
||||
@@ -1897,9 +1963,6 @@
|
||||
"themeLabel": "Theme",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Accent Color",
|
||||
"settingsFileManager": "File Manager",
|
||||
"fileColorCoding": "File Color Coding",
|
||||
"fileColorCodingDesc": "Color-code files by type",
|
||||
"settingsTerminal": "Terminal",
|
||||
"commandAutocomplete": "Command Autocomplete",
|
||||
"commandAutocompleteDesc": "Show autocomplete while typing",
|
||||
@@ -1909,13 +1972,17 @@
|
||||
"syntaxHighlightingDesc": "Highlight terminal output",
|
||||
"commandPalette": "Command Palette",
|
||||
"commandPaletteDesc": "Enable keyboard shortcut",
|
||||
"sessionPersistence": "Session Persistence",
|
||||
"sessionPersistenceDesc": "Keep sessions between reconnects",
|
||||
"reopenTabsOnLogin": "Reopen Tabs on Login",
|
||||
"reopenTabsOnLoginDesc": "Restore your open tabs when you log in or refresh the page, even from another device",
|
||||
"confirmTabClose": "Confirm Tab Close",
|
||||
"confirmTabCloseDesc": "Ask before closing terminal tabs",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Show Host Tags",
|
||||
"showHostTagsDesc": "Display tags in host list",
|
||||
"hostTrayOnClick": "Click to Expand Host Actions",
|
||||
"hostTrayOnClickDesc": "Require a click to expand host actions instead of hover",
|
||||
"pinAppRail": "Pin App Rail",
|
||||
"pinAppRailDesc": "Keep the left sidebar app rail always expanded instead of expanding on hover",
|
||||
"settingsSnippets": "Snippets",
|
||||
"foldersCollapsed": "Folders Collapsed",
|
||||
"foldersCollapsedDesc": "Collapse folders by default",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Brokkies",
|
||||
"hostManager": "Gasheerbestuurder",
|
||||
"credentials": "Geloofsbriewe",
|
||||
"connections": "Verbindings",
|
||||
"roleAdministrator": "Administrateur",
|
||||
"roleUser": "Gebruiker"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Vinnige blaai-sensitiwiteit",
|
||||
"moshCommandLabel": "Mosh-opdrag",
|
||||
"startupSnippetLabel": "Opstart-brokkie",
|
||||
"keepaliveIntervalLabel": "Hou-lewendige Interval",
|
||||
"keepaliveIntervalLabel": "Lewendige interval (sekondes)",
|
||||
"maxKeepaliveMisses": "Max Keepalive mis",
|
||||
"tunnelSettings": "Tonnelinstellings",
|
||||
"enableTunneling": "Aktiveer Tonnelbou",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL gekopieer",
|
||||
"telnetUrlCopied": "Telnet-URL gekopieer",
|
||||
"remoteDesktopUrlCopied": "URL vir afstandwerkskerm gekopieer",
|
||||
"wakeOnLanAction": "Wakker word op LAN",
|
||||
"wakeOnLanSuccess": "Magiese pakkie gestuur na {{name}}",
|
||||
"wakeOnLanError": "Kon nie magiese pakkie stuur nie",
|
||||
"cloneHostAction": "Kloon gasheer",
|
||||
"copyAddress": "Kopieer Adres",
|
||||
"copyLink": "Kopieer skakel",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Geen gashere stem ooreen met jou soektog nie",
|
||||
"hostNotFound": "Gasheer nie gevind nie",
|
||||
"searchHosts": "Soek gashere...",
|
||||
"sortHosts": "Sorteer gashere",
|
||||
"sortDefault": "Standaardbestelling",
|
||||
"sortNameAsc": "Naam (A → Z)",
|
||||
"sortNameDesc": "Naam (Z → A)",
|
||||
"sortIpAsc": "IP-adres (opstygend)",
|
||||
"sortIpDesc": "IP-adres (beskrywing)",
|
||||
"sortOnlineFirst": "Aanlyn Eerste",
|
||||
"sortOfflineFirst": "Vanlyn Eerste",
|
||||
"sortPinnedFirst": "Eerste vasgepen",
|
||||
"filterHosts": "Filter gashere",
|
||||
"filterClearAll": "Vee filters uit",
|
||||
"filterStatusGroup": "Status",
|
||||
"filterOnline": "Aanlyn",
|
||||
"filterOffline": "Vanlyn",
|
||||
"filterPinned": "Vasgepen",
|
||||
"filterAuthGroup": "Magtigingsoort",
|
||||
"filterAuthPassword": "Wagwoord",
|
||||
"filterAuthKey": "SSH-sleutel",
|
||||
"filterAuthCredential": "Geloofsbrief",
|
||||
"filterAuthNone": "Geen",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protokol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Kenmerke",
|
||||
"filterFeatureTerminal": "Terminaal",
|
||||
"filterFeatureFileManager": "Lêerbestuurder",
|
||||
"filterFeatureTunnel": "Tonnel",
|
||||
"filterFeatureDocker": "Docker",
|
||||
"filterTagsGroup": "Etikette",
|
||||
"shareHost": "Deel gasheer",
|
||||
"shareHostTitle": "Deel: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Ruit {{index}} - leeg",
|
||||
"noTabAssigned": "Geen oortjie toegeken nie"
|
||||
"noTabAssigned": "Geen oortjie toegeken nie",
|
||||
"focusedPane": "Aktiewe paneel"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Geen verbindings nie",
|
||||
"noConnectionsDesc": "Maak 'n terminaal, lêerbestuurder of afstandlessenaar oop om verbindings hier te sien",
|
||||
"connectedFor": "Gekoppel vir {{duration}}",
|
||||
"connected": "Verbonde",
|
||||
"disconnected": "Ontkoppel",
|
||||
"closeTab": "Maak oortjie toe",
|
||||
"closeConnection": "Maak verbinding toe",
|
||||
"forgetTab": "Vergeet",
|
||||
"removeBackground": "Verwyder",
|
||||
"reconnect": "Herkoppel",
|
||||
"reopenTab": "Heropen",
|
||||
"sectionOpen": "Oop",
|
||||
"sectionBackground": "Agtergrond",
|
||||
"backgroundDesc": "Sessies duur vir 30 minute na ontkoppeling voort en kan weer gekoppel word.",
|
||||
"persisted": "Het in die agtergrond voortgeduur",
|
||||
"expiresIn": "Verval oor {{duration}}",
|
||||
"search": "Soek verbindings...",
|
||||
"noSearchResults": "Geen verbindings stem ooreen met jou soektog nie"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Verbind met {{type}} sessie...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Probeer weer"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH-BEDIENERBESTUURDER",
|
||||
"tagline": "Self-gehoste SSH en afstandrekenaarbestuur",
|
||||
"loginTitle": "Teken aan by Termix",
|
||||
"registerTitle": "Skep Rekening",
|
||||
"forgotPassword": "Wagwoord vergeet?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Kies hoeveel panele om te vertoon",
|
||||
"panesTitle": "Ruite",
|
||||
"openTabsTitle": "Maak oortjies oop",
|
||||
"dragTabsHint": "Sleep oortjies na die vensters hierbo",
|
||||
"dragTabsHint": "Sleep oortjies na die panele hierbo, of gebruik Vinnige Toewysing",
|
||||
"dropHere": "Laat val hier",
|
||||
"emptyPane": "Leeg",
|
||||
"dashboard": "Dashboard",
|
||||
"clearSplitScreen": "Maak die gesplete skerm skoon"
|
||||
"clearSplitScreen": "Maak die gesplete skerm skoon",
|
||||
"quickAssign": "Vinnige Toewysing",
|
||||
"alreadyAssigned": "Ruit {{index}}",
|
||||
"splitTab": "Verdeel oortjie",
|
||||
"addToSplit": "Voeg by Split",
|
||||
"removeFromSplit": "Verwyder van Split",
|
||||
"assignToPane": "Toewys aan paneel"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Skep brokkie",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Kon nie vouer uitvee nie",
|
||||
"folderEditSuccess": "Vouer suksesvol opgedateer",
|
||||
"folderEditFailed": "Kon nie vouer opdateer nie",
|
||||
"confirmRunMessage": "Begin \"{{name}}\"?",
|
||||
"confirmRunButton": "Hardloop",
|
||||
"runSuccess": "Het \"{{name}}\" in {{count}} terminaal(e) uitgevoer",
|
||||
"copySuccess": "\"{{name}}\" na knipbord gekopieer",
|
||||
"shareTitle": "Deel brokkie",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Huidige Toegang",
|
||||
"shareLoadError": "Kon nie deeldata laai nie",
|
||||
"loading": "Laai tans...",
|
||||
"close": "Maak toe"
|
||||
"close": "Maak toe",
|
||||
"reorderFailed": "Kon nie die volgorde van die brokkie stoor nie"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Rekening",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Tema",
|
||||
"fontSizeLabel": "Lettergrootte",
|
||||
"accentColorLabel": "Aksentkleur",
|
||||
"settingsFileManager": "Lêerbestuurder",
|
||||
"fileColorCoding": "Lêerkleurkodering",
|
||||
"fileColorCodingDesc": "Kleurkodeer lêers volgens tipe",
|
||||
"settingsTerminal": "Terminaal",
|
||||
"commandAutocomplete": "Opdrag Outomatiese Voltooiing",
|
||||
"commandAutocompleteDesc": "Wys outovoltooiing terwyl jy tik",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Merk terminaaluitvoer",
|
||||
"commandPalette": "Opdragpalet",
|
||||
"commandPaletteDesc": "Aktiveer sleutelbordkortpaaie",
|
||||
"sessionPersistence": "Sessie-persistensie",
|
||||
"sessionPersistenceDesc": "Hou sessies tussen herverbindings",
|
||||
"reopenTabsOnLogin": "Heropen oortjies by aanmelding",
|
||||
"reopenTabsOnLoginDesc": "Herstel jou oop oortjies wanneer jy aanmeld of die bladsy verfris, selfs vanaf 'n ander toestel",
|
||||
"confirmTabClose": "Bevestig oortjie-sluiting",
|
||||
"confirmTabCloseDesc": "Vra voordat terminaal-oortjies gesluit word",
|
||||
"settingsSidebar": "Sybalk",
|
||||
"showHostTags": "Wys gasheer-etikette",
|
||||
"showHostTagsDesc": "Wys etikette in gasheerlys",
|
||||
"hostTrayOnClick": "Klik om gasheeraksies uit te brei",
|
||||
"hostTrayOnClickDesc": "Vereis 'n klik om gasheeraksies uit te brei in plaas van om te beweeg",
|
||||
"pinAppRail": "Speld App-spoor vas",
|
||||
"pinAppRailDesc": "Hou die linkerkantbalk-apprail altyd uitgebrei in plaas daarvan om uit te brei wanneer jy beweeg",
|
||||
"settingsSnippets": "Brokkies",
|
||||
"foldersCollapsed": "Vouers Ingevou",
|
||||
"foldersCollapsedDesc": "Vou vouers standaard in",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "كتل الكود",
|
||||
"hostManager": "مدير المضيف",
|
||||
"credentials": "وثائق التفويض",
|
||||
"connections": "الاتصالات",
|
||||
"roleAdministrator": "المدير",
|
||||
"roleUser": "المستخدم"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "حساسية التمرير السريع",
|
||||
"moshCommandLabel": "قيادة موش",
|
||||
"startupSnippetLabel": "بدء تشغيل كتلة الكود",
|
||||
"keepaliveIntervalLabel": "الفاصل الزمني للاحتفاظ",
|
||||
"keepaliveIntervalLabel": "فترة الإبقاء على الاتصال (بالثواني)",
|
||||
"maxKeepaliveMisses": "الحد الاقصى للاخطاء",
|
||||
"tunnelSettings": "إعدادات النفق",
|
||||
"enableTunneling": "تمكين الاتصال النفقي",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "تم نسخ رابط VNC",
|
||||
"telnetUrlCopied": "تم نسخ رابط الهاتف",
|
||||
"remoteDesktopUrlCopied": "تم نسخ رابط سطح المكتب عن بعد",
|
||||
"wakeOnLanAction": "تشغيل عبر الشبكة المحلية",
|
||||
"wakeOnLanSuccess": "تم إرسال حزمة سحرية إلى {{name}}",
|
||||
"wakeOnLanError": "فشل إرسال الحزمة السحرية",
|
||||
"cloneHostAction": "استنساخ المضيف",
|
||||
"copyAddress": "نسخ العنوان",
|
||||
"copyLink": "نسخ الرابط",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "لا يوجد مضيف يطابق بحثك",
|
||||
"hostNotFound": "لم يتم العثور على المضيف",
|
||||
"searchHosts": "البحث عن المضيفين...",
|
||||
"sortHosts": "فرز المضيفين",
|
||||
"sortDefault": "الترتيب الافتراضي",
|
||||
"sortNameAsc": "الاسم (من الألف إلى الياء)",
|
||||
"sortNameDesc": "الاسم (Z → A)",
|
||||
"sortIpAsc": "عنوان IP (تصاعدي)",
|
||||
"sortIpDesc": "عنوان IP (الوصف)",
|
||||
"sortOnlineFirst": "عبر الإنترنت أولاً",
|
||||
"sortOfflineFirst": "العمل دون اتصال بالإنترنت أولاً",
|
||||
"sortPinnedFirst": "مثبت أولاً",
|
||||
"filterHosts": "تصفية المضيفين",
|
||||
"filterClearAll": "فلاتر شفافة",
|
||||
"filterStatusGroup": "حالة",
|
||||
"filterOnline": "متصل",
|
||||
"filterOffline": "غير متصل بالإنترنت",
|
||||
"filterPinned": "مثبت",
|
||||
"filterAuthGroup": "نوع المصادقة",
|
||||
"filterAuthPassword": "كلمة المرور",
|
||||
"filterAuthKey": "مفتاح SSH",
|
||||
"filterAuthCredential": "بيانات الاعتماد",
|
||||
"filterAuthNone": "لا أحد",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "بروتوكول",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "تيلنت",
|
||||
"filterFeaturesGroup": "سمات",
|
||||
"filterFeatureTerminal": "صالة",
|
||||
"filterFeatureFileManager": "مدير الملفات",
|
||||
"filterFeatureTunnel": "نفق",
|
||||
"filterFeatureDocker": "عامل ميناء",
|
||||
"filterTagsGroup": "الوسوم",
|
||||
"shareHost": "مشاركة المضيف",
|
||||
"shareHostTitle": "مشاركة: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "لم يتم تعيين علامة تبويب"
|
||||
"noTabAssigned": "لم يتم تعيين علامة تبويب",
|
||||
"focusedPane": "اللوحة النشطة"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "لا توجد اتصالات",
|
||||
"noConnectionsDesc": "افتح نافذة طرفية أو مدير ملفات أو سطح مكتب بعيد لعرض الاتصالات هنا",
|
||||
"connectedFor": "متصل لمدة {{duration}}",
|
||||
"connected": "متصل",
|
||||
"disconnected": "غير متصل",
|
||||
"closeTab": "إغلاق علامة التبويب",
|
||||
"closeConnection": "اتصال وثيق",
|
||||
"forgetTab": "ينسى",
|
||||
"removeBackground": "يزيل",
|
||||
"reconnect": "إعادة الاتصال",
|
||||
"reopenTab": "إعادة فتح",
|
||||
"sectionOpen": "يفتح",
|
||||
"sectionBackground": "خلفية",
|
||||
"backgroundDesc": "تستمر الجلسات لمدة 30 دقيقة بعد قطع الاتصال ويمكن إعادة الاتصال بها.",
|
||||
"persisted": "استمر في الخلفية",
|
||||
"expiresIn": "تنتهي صلاحيتها في {{duration}}",
|
||||
"search": "ابحث عن الاتصالات...",
|
||||
"noSearchResults": "لا توجد نتائج مطابقة لبحثك"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "جاري الاتصال بجلسة {{type}}...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "إعادة المحاولة"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "إدارة خدمة SSH",
|
||||
"tagline": "إدارة SSH وسطح المكتب عن بُعد ذاتية الاستضافة",
|
||||
"loginTitle": "تسجيل الدخول إلى Termix",
|
||||
"registerTitle": "إنشاء حساب",
|
||||
"forgotPassword": "نسيت كلمة المرور؟",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "اختر عدد الألواح التي سيتم عرضها",
|
||||
"panesTitle": "لوحات",
|
||||
"openTabsTitle": "افتح علامات التبويب",
|
||||
"dragTabsHint": "اسحب علامات التبويب إلى الألواح أعلاه",
|
||||
"dragTabsHint": "اسحب علامات التبويب إلى الأجزاء العلوية، أو استخدم خاصية التعيين السريع.",
|
||||
"dropHere": "إسقاط هنا",
|
||||
"emptyPane": "فارغ",
|
||||
"dashboard": "لوحة التحكم",
|
||||
"clearSplitScreen": "مسح تقسيم الشاشة"
|
||||
"clearSplitScreen": "مسح تقسيم الشاشة",
|
||||
"quickAssign": "التعيين السريع",
|
||||
"alreadyAssigned": "لوحة {{index}}",
|
||||
"splitTab": "علامة تبويب مقسمة",
|
||||
"addToSplit": "أضف إلى التقسيم",
|
||||
"removeFromSplit": "أزل من التقسيم",
|
||||
"assignToPane": "تعيين إلى اللوحة"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "إنشاء كتلة كتلة",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "فشل في حذف المجلد",
|
||||
"folderEditSuccess": "تم تحديث المجلد بنجاح",
|
||||
"folderEditFailed": "فشل تحديث المجلد",
|
||||
"confirmRunMessage": "تشغيل \"{{name}}\"?",
|
||||
"confirmRunButton": "يجري",
|
||||
"runSuccess": "R\"{{name}}\" في المحطة الطرفية {{count}}",
|
||||
"copySuccess": "تم نسخ \"{{name}}\" إلى الحافظة",
|
||||
"shareTitle": "مشاركة كتلة الكود",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "الوصول الحالي",
|
||||
"shareLoadError": "فشل تحميل بيانات المشاركة",
|
||||
"loading": "تحميل...",
|
||||
"close": "أغلق"
|
||||
"close": "أغلق",
|
||||
"reorderFailed": "فشل حفظ ترتيب المقتطفات"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "حساب",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "السمة",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "لون اللكنة",
|
||||
"settingsFileManager": "مدير الملفات",
|
||||
"fileColorCoding": "ترميز لون الملف",
|
||||
"fileColorCodingDesc": "ملفات رمز الألوان حسب النوع",
|
||||
"settingsTerminal": "المحطة",
|
||||
"commandAutocomplete": "إكمال الأوامر التلقائي",
|
||||
"commandAutocompleteDesc": "إظهار الإكمال التلقائي أثناء الكتابة",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "تسليط الضوء على إخراج المحطة",
|
||||
"commandPalette": "لوحة الأوامر",
|
||||
"commandPaletteDesc": "تمكين اختصار لوحة المفاتيح",
|
||||
"sessionPersistence": "ثبات الجلسة",
|
||||
"sessionPersistenceDesc": "الحفاظ على الجلسات بين إعادة الاتصال",
|
||||
"reopenTabsOnLogin": "أعد فتح علامات التبويب عند تسجيل الدخول",
|
||||
"reopenTabsOnLoginDesc": "استعد علامات التبويب المفتوحة عند تسجيل الدخول أو تحديث الصفحة، حتى من جهاز آخر.",
|
||||
"confirmTabClose": "تأكيد إغلاق علامة التبويب",
|
||||
"confirmTabCloseDesc": "السؤال قبل إغلاق علامات التبويب الطرفية",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "إظهار الوسوم المضيفة",
|
||||
"showHostTagsDesc": "عرض العلامات في قائمة المضيف",
|
||||
"hostTrayOnClick": "انقر لتوسيع إجراءات المضيف",
|
||||
"hostTrayOnClickDesc": "يتطلب الأمر نقرة لتوسيع إجراءات المضيف بدلاً من التمرير",
|
||||
"pinAppRail": "دبوس سكة التطبيق",
|
||||
"pinAppRailDesc": "حافظ على شريط التطبيق الجانبي الأيسر مفتوحًا دائمًا بدلاً من توسيعه عند تمرير المؤشر فوقه.",
|
||||
"settingsSnippets": "كتل الكود",
|
||||
"foldersCollapsed": "مجلدات سقطت",
|
||||
"foldersCollapsedDesc": "طي المجلدات بشكل افتراضي",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Откъси",
|
||||
"hostManager": "Мениджър на домакини",
|
||||
"credentials": "Пълномощия",
|
||||
"connections": "Връзки",
|
||||
"roleAdministrator": "Администратор",
|
||||
"roleUser": "Потребител"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Чувствителност на бързото превъртане",
|
||||
"moshCommandLabel": "Команда Мош",
|
||||
"startupSnippetLabel": "Стартов фрагмент",
|
||||
"keepaliveIntervalLabel": "Интервал на поддържане на активността",
|
||||
"keepaliveIntervalLabel": "Интервал за поддържане на активност (секунди)",
|
||||
"maxKeepaliveMisses": "Максимални пропуски на Keepalive",
|
||||
"tunnelSettings": "Настройки на тунела",
|
||||
"enableTunneling": "Активиране на тунелиране",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL адресът е копиран",
|
||||
"telnetUrlCopied": "URL адресът на Telnet е копиран",
|
||||
"remoteDesktopUrlCopied": "URL адресът на отдалечен работен плот е копиран",
|
||||
"wakeOnLanAction": "Събуждане по LAN",
|
||||
"wakeOnLanSuccess": "Магически пакет, изпратен до {{name}}",
|
||||
"wakeOnLanError": "Изпращането на магически пакет не бе успешно",
|
||||
"cloneHostAction": "Клониране на хост",
|
||||
"copyAddress": "Копиране на адрес",
|
||||
"copyLink": "Копиране на връзката",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Няма хостове, които да отговарят на вашето търсене",
|
||||
"hostNotFound": "Хостът не е намерен",
|
||||
"searchHosts": "Търсене на хостове...",
|
||||
"sortHosts": "Сортиране на хостове",
|
||||
"sortDefault": "По подразбиране ред",
|
||||
"sortNameAsc": "Име (А → Я)",
|
||||
"sortNameDesc": "Име (Я → А)",
|
||||
"sortIpAsc": "IP адрес (възходящ)",
|
||||
"sortIpDesc": "IP адрес (описание)",
|
||||
"sortOnlineFirst": "Първо онлайн",
|
||||
"sortOfflineFirst": "Първо офлайн",
|
||||
"sortPinnedFirst": "Първо закачено",
|
||||
"filterHosts": "Филтриране на хостове",
|
||||
"filterClearAll": "Изчистване на филтрите",
|
||||
"filterStatusGroup": "Статус",
|
||||
"filterOnline": "Онлайн",
|
||||
"filterOffline": "Офлайн",
|
||||
"filterPinned": "Закачено",
|
||||
"filterAuthGroup": "Тип оторизация",
|
||||
"filterAuthPassword": "Парола",
|
||||
"filterAuthKey": "SSH ключ",
|
||||
"filterAuthCredential": "Пълномощия",
|
||||
"filterAuthNone": "Няма",
|
||||
"filterAuthOpkssh": "ОПКСШ",
|
||||
"filterProtocolGroup": "Протокол",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "ПРСР",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Телнет",
|
||||
"filterFeaturesGroup": "Характеристики",
|
||||
"filterFeatureTerminal": "Терминал",
|
||||
"filterFeatureFileManager": "Файлов мениджър",
|
||||
"filterFeatureTunnel": "Тунел",
|
||||
"filterFeatureDocker": "Докер",
|
||||
"filterTagsGroup": "Етикети",
|
||||
"shareHost": "Споделяне на хост",
|
||||
"shareHostTitle": "Споделяне: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Панел {{index}} - празен",
|
||||
"noTabAssigned": "Няма зададен раздел"
|
||||
"noTabAssigned": "Няма зададен раздел",
|
||||
"focusedPane": "Активен панел"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Няма връзки",
|
||||
"noConnectionsDesc": "Отворете терминал, файлов мениджър или отдалечен работен плот, за да видите връзките тук",
|
||||
"connectedFor": "Свързано за {{duration}}",
|
||||
"connected": "Свързан",
|
||||
"disconnected": "Изключен",
|
||||
"closeTab": "Затвори раздела",
|
||||
"closeConnection": "Затвори връзката",
|
||||
"forgetTab": "Забрави",
|
||||
"removeBackground": "Премахване",
|
||||
"reconnect": "Свържете се отново",
|
||||
"reopenTab": "Отвори отново",
|
||||
"sectionOpen": "Отворено",
|
||||
"sectionBackground": "Предистория",
|
||||
"backgroundDesc": "Сесиите продължават 30 минути след прекъсване на връзката и могат да бъдат свързани отново.",
|
||||
"persisted": "Продължава във фонов режим",
|
||||
"expiresIn": "Изтича след {{duration}}",
|
||||
"search": "Търсене на връзки...",
|
||||
"noSearchResults": "Няма връзки, които да отговарят на търсенето ви"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Свързване към сесия {{type}}...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Опитай отново"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH СЪРВЪР МЕНИДЖЪР",
|
||||
"tagline": "Самостоятелно хоствано SSH и управление на отдалечен работен плот",
|
||||
"loginTitle": "Вход в Termix",
|
||||
"registerTitle": "Създаване на акаунт",
|
||||
"forgotPassword": "Забравена парола?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Изберете колко панела да се показват",
|
||||
"panesTitle": "Прозорци",
|
||||
"openTabsTitle": "Отворени раздели",
|
||||
"dragTabsHint": "Плъзнете раздели в панелите по-горе",
|
||||
"dragTabsHint": "Плъзнете раздели в панелите по-горе или използвайте „Бързо присвояване“",
|
||||
"dropHere": "Пуснете тук",
|
||||
"emptyPane": "Празно",
|
||||
"dashboard": "Табло за управление",
|
||||
"clearSplitScreen": "Изчистване на разделения екран"
|
||||
"clearSplitScreen": "Изчистване на разделения екран",
|
||||
"quickAssign": "Бързо присвояване",
|
||||
"alreadyAssigned": "Панел {{index}}",
|
||||
"splitTab": "Разделяне на раздела",
|
||||
"addToSplit": "Добави към Сплит",
|
||||
"removeFromSplit": "Премахване от Сплит",
|
||||
"assignToPane": "Присвояване към панела"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Създаване на фрагмент",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Изтриването на папката не бе успешно",
|
||||
"folderEditSuccess": "Папката е актуализирана успешно",
|
||||
"folderEditFailed": "Актуализирането на папката не бе успешно",
|
||||
"confirmRunMessage": "Изпълни „{{name}}“?",
|
||||
"confirmRunButton": "Бягане",
|
||||
"runSuccess": "Изпълни „{{name}}“ в {{count}} терминал(и)",
|
||||
"copySuccess": "Копирано „{{name}}“ в клипборда",
|
||||
"shareTitle": "Споделяне на фрагмент",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Текущ достъп",
|
||||
"shareLoadError": "Зареждането на данните за споделяне не бе успешно",
|
||||
"loading": "Зареждане...",
|
||||
"close": "Затвори"
|
||||
"close": "Затвори",
|
||||
"reorderFailed": "Запазването на реда на фрагмента не бе успешно"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Профил",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Тема",
|
||||
"fontSizeLabel": "Размер на шрифта",
|
||||
"accentColorLabel": "Акцентен цвят",
|
||||
"settingsFileManager": "Файлов мениджър",
|
||||
"fileColorCoding": "Цветово кодиране на файлове",
|
||||
"fileColorCodingDesc": "Цветово кодиране на файлове по тип",
|
||||
"settingsTerminal": "Терминал",
|
||||
"commandAutocomplete": "Автоматично довършване на команди",
|
||||
"commandAutocompleteDesc": "Показване на автоматично довършване при въвеждане",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Маркирайте изхода на терминала",
|
||||
"commandPalette": "Палитра с команди",
|
||||
"commandPaletteDesc": "Активиране на клавишна комбинация",
|
||||
"sessionPersistence": "Устойчивост на сесията",
|
||||
"sessionPersistenceDesc": "Запазване на сесиите между повторните връзки",
|
||||
"reopenTabsOnLogin": "Отваряне на раздели отново при влизане",
|
||||
"reopenTabsOnLoginDesc": "Възстановете отворените си раздели, когато влезете или обновите страницата, дори от друго устройство",
|
||||
"confirmTabClose": "Потвърждаване на затваряне на раздела",
|
||||
"confirmTabCloseDesc": "Попитай преди затваряне на раздели на терминала",
|
||||
"settingsSidebar": "Странична лента",
|
||||
"showHostTags": "Показване на етикети на хоста",
|
||||
"showHostTagsDesc": "Показване на тагове в списъка с хостове",
|
||||
"hostTrayOnClick": "Щракнете, за да разгънете действията на хоста",
|
||||
"hostTrayOnClickDesc": "Изискване за кликване за разгъване на действията на хоста, вместо задържане на курсора на мишката",
|
||||
"pinAppRail": "Pin App Rail",
|
||||
"pinAppRailDesc": "Поддържайте релсата на приложението в лявата странична лента винаги разгъната, вместо да се разширява при задържане на курсора на мишката",
|
||||
"settingsSnippets": "Откъси",
|
||||
"foldersCollapsed": "Папките са свити",
|
||||
"foldersCollapsedDesc": "Свиване на папките по подразбиране",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "খণ্ডাংশ",
|
||||
"hostManager": "হোস্ট ম্যানেজার",
|
||||
"credentials": "যোগ্যতা",
|
||||
"connections": "সংযোগ",
|
||||
"roleAdministrator": "প্রশাসক",
|
||||
"roleUser": "ব্যবহারকারী"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "দ্রুত স্ক্রোল সংবেদনশীলতা",
|
||||
"moshCommandLabel": "মোশ কমান্ড",
|
||||
"startupSnippetLabel": "স্টার্টআপ স্নিপেট",
|
||||
"keepaliveIntervalLabel": "কিপঅ্যালাইভ ইন্টারভাল",
|
||||
"keepaliveIntervalLabel": "কিপঅ্যালাইভ ব্যবধান (সেকেন্ড)",
|
||||
"maxKeepaliveMisses": "ম্যাক্স কিপঅ্যালাইভ মিস করে",
|
||||
"tunnelSettings": "টানেল সেটিংস",
|
||||
"enableTunneling": "টানেলিং সক্ষম করুন",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL কপি করা হয়েছে",
|
||||
"telnetUrlCopied": "টেলনেট ইউআরএল কপি করা হয়েছে",
|
||||
"remoteDesktopUrlCopied": "রিমোট ডেস্কটপ ইউআরএল কপি করা হয়েছে",
|
||||
"wakeOnLanAction": "ওয়েক অন ল্যান",
|
||||
"wakeOnLanSuccess": "{{name}}-এ জাদুর প্যাকেট পাঠানো হয়েছে",
|
||||
"wakeOnLanError": "ম্যাজিক প্যাকেট পাঠাতে ব্যর্থ হয়েছে",
|
||||
"cloneHostAction": "ক্লোন হোস্ট",
|
||||
"copyAddress": "ঠিকানা কপি করুন",
|
||||
"copyLink": "লিঙ্ক কপি করুন",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "আপনার অনুসন্ধানের সাথে কোনো হোস্ট মেলে না।",
|
||||
"hostNotFound": "হোস্ট খুঁজে পাওয়া যায়নি",
|
||||
"searchHosts": "হোস্ট অনুসন্ধান করুন...",
|
||||
"sortHosts": "হোস্ট বাছাই করুন",
|
||||
"sortDefault": "ডিফল্ট অর্ডার",
|
||||
"sortNameAsc": "নাম (A → Z)",
|
||||
"sortNameDesc": "নাম (Z → A)",
|
||||
"sortIpAsc": "আইপি ঠিকানা (আরোহী)",
|
||||
"sortIpDesc": "আইপি ঠিকানা (বিবরণ)",
|
||||
"sortOnlineFirst": "অনলাইন প্রথম",
|
||||
"sortOfflineFirst": "অফলাইন প্রথম",
|
||||
"sortPinnedFirst": "প্রথমে পিন করা হয়েছে",
|
||||
"filterHosts": "হোস্ট ফিল্টার করুন",
|
||||
"filterClearAll": "ফিল্টার পরিষ্কার করুন",
|
||||
"filterStatusGroup": "অবস্থা",
|
||||
"filterOnline": "অনলাইন",
|
||||
"filterOffline": "অফলাইন",
|
||||
"filterPinned": "পিন করা",
|
||||
"filterAuthGroup": "প্রমাণীকরণ প্রকার",
|
||||
"filterAuthPassword": "পাসওয়ার্ড",
|
||||
"filterAuthKey": "SSH কী",
|
||||
"filterAuthCredential": "শংসাপত্র",
|
||||
"filterAuthNone": "কোনোটিই না",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "প্রোটোকল",
|
||||
"filterProtocolSsh": "এসএসএইচ",
|
||||
"filterProtocolRdp": "আরডিপি",
|
||||
"filterProtocolVnc": "ভিএনসি",
|
||||
"filterProtocolTelnet": "টেলনেট",
|
||||
"filterFeaturesGroup": "বৈশিষ্ট্য",
|
||||
"filterFeatureTerminal": "টার্মিনাল",
|
||||
"filterFeatureFileManager": "ফাইল ম্যানেজার",
|
||||
"filterFeatureTunnel": "টানেল",
|
||||
"filterFeatureDocker": "ডকার",
|
||||
"filterTagsGroup": "ট্যাগ",
|
||||
"shareHost": "হোস্ট শেয়ার করুন",
|
||||
"shareHostTitle": "শেয়ার করুন: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "প্যান {{index}} - খালি",
|
||||
"noTabAssigned": "কোন ট্যাব বরাদ্দ করা হয়নি"
|
||||
"noTabAssigned": "কোন ট্যাব বরাদ্দ করা হয়নি",
|
||||
"focusedPane": "সক্রিয় প্যানে"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "কোন সংযোগ নেই",
|
||||
"noConnectionsDesc": "এখানে সংযোগগুলি দেখতে একটি টার্মিনাল, ফাইল ম্যানেজার বা রিমোট ডেস্কটপ খুলুন।",
|
||||
"connectedFor": "{{duration}} এর জন্য সংযুক্ত",
|
||||
"connected": "সংযুক্ত",
|
||||
"disconnected": "বিচ্ছিন্ন",
|
||||
"closeTab": "ট্যাব বন্ধ করুন",
|
||||
"closeConnection": "ঘনিষ্ঠ সংযোগ",
|
||||
"forgetTab": "ভুলে যান",
|
||||
"removeBackground": "অপসারণ করুন",
|
||||
"reconnect": "পুনরায় সংযোগ করুন",
|
||||
"reopenTab": "পুনরায় খুলুন",
|
||||
"sectionOpen": "খোলা",
|
||||
"sectionBackground": "পটভূমি",
|
||||
"backgroundDesc": "সংযোগ বিচ্ছিন্ন হওয়ার পরেও সেশন ৩০ মিনিট পর্যন্ত স্থায়ী থাকে এবং পুনরায় সংযোগ করা যায়।",
|
||||
"persisted": "পটভূমিতে অব্যাহত ছিল",
|
||||
"expiresIn": "{{duration}}-এ মেয়াদ শেষ হবে",
|
||||
"search": "সংযোগগুলি অনুসন্ধান করুন...",
|
||||
"noSearchResults": "আপনার অনুসন্ধানের সাথে কোনো সংযোগ মেলেনি।"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "{{type}} সেশনে সংযোগ স্থাপন করা হচ্ছে...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "পুনরায় চেষ্টা করুন"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "এসএসএইচ সার্ভার ম্যানেজার",
|
||||
"tagline": "সেলফ-হোস্টেড SSH এবং রিমোট ডেস্কটপ ম্যানেজমেন্ট",
|
||||
"loginTitle": "Termix-এ লগইন করুন",
|
||||
"registerTitle": "অ্যাকাউন্ট তৈরি করুন",
|
||||
"forgotPassword": "পাসওয়ার্ড ভুলে গেছেন?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "কতগুলি প্যান প্রদর্শন করতে হবে তা বেছে নিন",
|
||||
"panesTitle": "প্যানে",
|
||||
"openTabsTitle": "ট্যাব খুলুন",
|
||||
"dragTabsHint": "ট্যাবগুলিকে উপরের প্যানে টেনে আনুন",
|
||||
"dragTabsHint": "ট্যাবগুলিকে উপরের প্যানে টেনে আনুন, অথবা কুইক অ্যাসাইন ব্যবহার করুন।",
|
||||
"dropHere": "এখানে নেমে যান",
|
||||
"emptyPane": "খালি",
|
||||
"dashboard": "ড্যাশবোর্ড",
|
||||
"clearSplitScreen": "ক্লিয়ার স্প্লিট স্ক্রিন"
|
||||
"clearSplitScreen": "ক্লিয়ার স্প্লিট স্ক্রিন",
|
||||
"quickAssign": "দ্রুত বরাদ্দ করুন",
|
||||
"alreadyAssigned": "প্যান {{index}}",
|
||||
"splitTab": "স্প্লিট ট্যাব",
|
||||
"addToSplit": "স্প্লিটে যোগ করুন",
|
||||
"removeFromSplit": "স্প্লিট থেকে সরান",
|
||||
"assignToPane": "প্যানে বরাদ্দ করুন"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "স্নিপেট তৈরি করুন",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "ফোল্ডারটি মুছে ফেলা ব্যর্থ হয়েছে",
|
||||
"folderEditSuccess": "ফোল্ডারটি সফলভাবে আপডেট করা হয়েছে",
|
||||
"folderEditFailed": "ফোল্ডার আপডেট করতে ব্যর্থ হয়েছে",
|
||||
"confirmRunMessage": "\"{{name}} \" চালান?",
|
||||
"confirmRunButton": "দৌড়",
|
||||
"runSuccess": "{{count}} টার্মিনালে \"{{name}}\" চালানো হয়েছে",
|
||||
"copySuccess": "\"{{name}}\" ক্লিপবোর্ডে কপি করা হয়েছে",
|
||||
"shareTitle": "স্নিপেট শেয়ার করুন",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "বর্তমান অ্যাক্সেস",
|
||||
"shareLoadError": "শেয়ার ডেটা লোড করতে ব্যর্থ হয়েছে",
|
||||
"loading": "লোড হচ্ছে...",
|
||||
"close": "বন্ধ করুন"
|
||||
"close": "বন্ধ করুন",
|
||||
"reorderFailed": "স্নিপেটের ক্রম সংরক্ষণ করতে ব্যর্থ হয়েছে"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "অ্যাকাউন্ট",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "থিম",
|
||||
"fontSizeLabel": "ফন্ট সাইজ",
|
||||
"accentColorLabel": "অ্যাকসেন্ট রঙ",
|
||||
"settingsFileManager": "ফাইল ম্যানেজার",
|
||||
"fileColorCoding": "ফাইল কালার কোডিং",
|
||||
"fileColorCodingDesc": "ফাইলের ধরণ অনুযায়ী রঙ দিয়ে চিহ্নিত করুন",
|
||||
"settingsTerminal": "টার্মিনাল",
|
||||
"commandAutocomplete": "কমান্ড অটোকমপ্লিট",
|
||||
"commandAutocompleteDesc": "টাইপ করার সময় স্বয়ংক্রিয় সম্পূর্ণতা দেখান",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "টার্মিনাল আউটপুট হাইলাইট করুন",
|
||||
"commandPalette": "কমান্ড প্যালেট",
|
||||
"commandPaletteDesc": "কিবোর্ড শর্টকাট সক্রিয় করুন",
|
||||
"sessionPersistence": "সেশন স্থায়িত্ব",
|
||||
"sessionPersistenceDesc": "পুনরায় সংযোগের মধ্যে সেশনগুলি রাখুন",
|
||||
"reopenTabsOnLogin": "লগইন করার সময় ট্যাবগুলি পুনরায় খুলুন",
|
||||
"reopenTabsOnLoginDesc": "লগ ইন করলে বা পৃষ্ঠা রিফ্রেশ করলে, এমনকি অন্য ডিভাইস থেকেও আপনার খোলা ট্যাবগুলি পুনরুদ্ধার করুন।",
|
||||
"confirmTabClose": "ট্যাব বন্ধ নিশ্চিত করুন",
|
||||
"confirmTabCloseDesc": "টার্মিনাল ট্যাব বন্ধ করার আগে জিজ্ঞাসা করুন।",
|
||||
"settingsSidebar": "সাইডবার",
|
||||
"showHostTags": "হোস্ট ট্যাগগুলি দেখান",
|
||||
"showHostTagsDesc": "হোস্ট তালিকায় ট্যাগগুলি প্রদর্শন করুন",
|
||||
"hostTrayOnClick": "হোস্ট অ্যাকশনগুলি প্রসারিত করতে ক্লিক করুন",
|
||||
"hostTrayOnClickDesc": "হোস্ট অ্যাকশনগুলো প্রসারিত করতে হোভারের পরিবর্তে ক্লিকের প্রয়োজন হবে।",
|
||||
"pinAppRail": "পিন অ্যাপ রেল",
|
||||
"pinAppRailDesc": "হোভার করার সময় প্রসারিত না হয়ে, বাম সাইডবারের অ্যাপ রেলটিকে সর্বদা প্রসারিত রাখুন।",
|
||||
"settingsSnippets": "খণ্ডাংশ",
|
||||
"foldersCollapsed": "ফোল্ডারগুলো ভেঙে গেছে",
|
||||
"foldersCollapsedDesc": "ডিফল্টরূপে ফোল্ডারগুলি সংকুচিত করুন",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Fragments",
|
||||
"hostManager": "Gestor d'amfitrions",
|
||||
"credentials": "Credencials",
|
||||
"connections": "Connexions",
|
||||
"roleAdministrator": "Administrador/a",
|
||||
"roleUser": "Usuari"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Sensibilitat de desplaçament ràpid",
|
||||
"moshCommandLabel": "Comandament Mosh",
|
||||
"startupSnippetLabel": "Fragment d'inici",
|
||||
"keepaliveIntervalLabel": "Interval de manteniment actiu",
|
||||
"keepaliveIntervalLabel": "Interval de manteniment (segons)",
|
||||
"maxKeepaliveMisses": "Màxims errors de manteniment en directe",
|
||||
"tunnelSettings": "Configuració del túnel",
|
||||
"enableTunneling": "Activa la tunelització",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "URL de VNC copiada",
|
||||
"telnetUrlCopied": "S'ha copiat l'URL de Telnet",
|
||||
"remoteDesktopUrlCopied": "S'ha copiat l'URL de l'escriptori remot",
|
||||
"wakeOnLanAction": "Despertar-se en LAN",
|
||||
"wakeOnLanSuccess": "Paquet màgic enviat a {{name}}",
|
||||
"wakeOnLanError": "No s'ha pogut enviar el paquet màgic",
|
||||
"cloneHostAction": "Host clonat",
|
||||
"copyAddress": "Copiar adreça",
|
||||
"copyLink": "Copia l'enllaç",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "No hi ha amfitrions que coincideixin amb la teva cerca",
|
||||
"hostNotFound": "No s'ha trobat l'amfitrió",
|
||||
"searchHosts": "Cerca amfitrions...",
|
||||
"sortHosts": "Ordena els hosts",
|
||||
"sortDefault": "Ordre per defecte",
|
||||
"sortNameAsc": "Nom (A → Z)",
|
||||
"sortNameDesc": "Nom (Z → A)",
|
||||
"sortIpAsc": "Adreça IP (Asc)",
|
||||
"sortIpDesc": "Adreça IP (Desc.)",
|
||||
"sortOnlineFirst": "En línia primer",
|
||||
"sortOfflineFirst": "Fora de línia primer",
|
||||
"sortPinnedFirst": "Fixat primer",
|
||||
"filterHosts": "Filtra els amfitrions",
|
||||
"filterClearAll": "Esborra els filtres",
|
||||
"filterStatusGroup": "Estat",
|
||||
"filterOnline": "En línia",
|
||||
"filterOffline": "Fora de línia",
|
||||
"filterPinned": "Fixat",
|
||||
"filterAuthGroup": "Tipus d'autorització",
|
||||
"filterAuthPassword": "Contrasenya",
|
||||
"filterAuthKey": "Clau SSH",
|
||||
"filterAuthCredential": "Credencial",
|
||||
"filterAuthNone": "Cap",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protocol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Característiques",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Gestor de fitxers",
|
||||
"filterFeatureTunnel": "Túnel",
|
||||
"filterFeatureDocker": "Docker",
|
||||
"filterTagsGroup": "Etiquetes",
|
||||
"shareHost": "Comparteix l'amfitrió",
|
||||
"shareHostTitle": "Comparteix: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Panell {{index}} - buit",
|
||||
"noTabAssigned": "Cap pestanya assignada"
|
||||
"noTabAssigned": "Cap pestanya assignada",
|
||||
"focusedPane": "Panell actiu"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Sense connexions",
|
||||
"noConnectionsDesc": "Obriu un terminal, un gestor de fitxers o un escriptori remot per veure les connexions aquí",
|
||||
"connectedFor": "Connectat per {{duration}}",
|
||||
"connected": "Connectat",
|
||||
"disconnected": "Desconnectat",
|
||||
"closeTab": "Tanca la pestanya",
|
||||
"closeConnection": "Connexió propera",
|
||||
"forgetTab": "Oblidar",
|
||||
"removeBackground": "Elimina",
|
||||
"reconnect": "Reconnecta",
|
||||
"reopenTab": "Reobrir",
|
||||
"sectionOpen": "Obre",
|
||||
"sectionBackground": "Fons",
|
||||
"backgroundDesc": "Les sessions persisteixen durant 30 minuts després de la desconnexió i es poden tornar a connectar.",
|
||||
"persisted": "Persistia en segon pla",
|
||||
"expiresIn": "Caduca en {{duration}}",
|
||||
"search": "Cerca connexions...",
|
||||
"noSearchResults": "No hi ha connexions que coincideixin amb la teva cerca"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Connectant a la sessió {{type}}...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Torna-ho a intentar"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "ADMINISTRADOR DE SERVIDORS SSH",
|
||||
"tagline": "SSH autoallotjat i gestió d'escriptori remot",
|
||||
"loginTitle": "Inicia la sessió a Termix",
|
||||
"registerTitle": "Crea un compte",
|
||||
"forgotPassword": "Has oblidat la contrasenya?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Trieu quants panells voleu mostrar",
|
||||
"panesTitle": "Panells",
|
||||
"openTabsTitle": "Obre les pestanyes",
|
||||
"dragTabsHint": "Arrossega les pestanyes als panells superiors",
|
||||
"dragTabsHint": "Arrossegueu pestanyes als panells superiors o feu servir Assignació ràpida",
|
||||
"dropHere": "Deixa anar aquí",
|
||||
"emptyPane": "Buit",
|
||||
"dashboard": "Tauler de control",
|
||||
"clearSplitScreen": "Pantalla dividida clara"
|
||||
"clearSplitScreen": "Pantalla dividida clara",
|
||||
"quickAssign": "Assignació ràpida",
|
||||
"alreadyAssigned": "Panell {{index}}",
|
||||
"splitTab": "Pestanya dividida",
|
||||
"addToSplit": "Afegeix a Dividir",
|
||||
"removeFromSplit": "Elimina de Split",
|
||||
"assignToPane": "Assigna al panell"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Crea un fragment",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "No s'ha pogut suprimir la carpeta",
|
||||
"folderEditSuccess": "La carpeta s'ha actualitzat correctament",
|
||||
"folderEditFailed": "No s'ha pogut actualitzar la carpeta",
|
||||
"confirmRunMessage": "Executar \"{{name}}\"?",
|
||||
"confirmRunButton": "Corre",
|
||||
"runSuccess": "Ha executat \"{{name}}\" en terminals {{count}}",
|
||||
"copySuccess": "S'ha copiat \"{{name}}\" al porta-retalls",
|
||||
"shareTitle": "Compartir fragment",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Accés actual",
|
||||
"shareLoadError": "No s'han pogut carregar les dades compartides",
|
||||
"loading": "S'està carregant...",
|
||||
"close": "Tanca"
|
||||
"close": "Tanca",
|
||||
"reorderFailed": "No s'ha pogut desar l'ordre del fragment"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Compte",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Tema",
|
||||
"fontSizeLabel": "Mida de la lletra",
|
||||
"accentColorLabel": "Color d'accent",
|
||||
"settingsFileManager": "Gestor de fitxers",
|
||||
"fileColorCoding": "Codificació de colors de fitxers",
|
||||
"fileColorCodingDesc": "Codificar els fitxers per colors per tipus",
|
||||
"settingsTerminal": "Terminal",
|
||||
"commandAutocomplete": "Autocompletar ordres",
|
||||
"commandAutocompleteDesc": "Mostra l'autocompleció mentre s'escriu",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Ressaltar la sortida del terminal",
|
||||
"commandPalette": "Paleta d'ordres",
|
||||
"commandPaletteDesc": "Activa la drecera de teclat",
|
||||
"sessionPersistence": "Persistència de la sessió",
|
||||
"sessionPersistenceDesc": "Mantenir sessions entre reconnexions",
|
||||
"reopenTabsOnLogin": "Reobrir pestanyes en iniciar sessió",
|
||||
"reopenTabsOnLoginDesc": "Restaura les pestanyes obertes quan inicies la sessió o actualitzes la pàgina, fins i tot des d'un altre dispositiu",
|
||||
"confirmTabClose": "Confirma el tancament de la pestanya",
|
||||
"confirmTabCloseDesc": "Pregunta abans de tancar pestanyes del terminal",
|
||||
"settingsSidebar": "Barra lateral",
|
||||
"showHostTags": "Mostra les etiquetes de l'amfitrió",
|
||||
"showHostTagsDesc": "Mostra les etiquetes a la llista d'amfitrions",
|
||||
"hostTrayOnClick": "Feu clic per expandir les accions de l'amfitrió",
|
||||
"hostTrayOnClickDesc": "Cal fer un clic per expandir les accions de l'amfitrió en comptes de passar el cursor per sobre",
|
||||
"pinAppRail": "Pin App Rail",
|
||||
"pinAppRailDesc": "Mantingueu el rail de l'aplicació de la barra lateral esquerra sempre expandit en comptes d'expandir-se en passar el cursor per sobre",
|
||||
"settingsSnippets": "Fragments",
|
||||
"foldersCollapsed": "Carpetes replegades",
|
||||
"foldersCollapsedDesc": "Replega les carpetes per defecte",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Úryvky",
|
||||
"hostManager": "Správce hostitelů",
|
||||
"credentials": "Pověření",
|
||||
"connections": "Připojení",
|
||||
"roleAdministrator": "Administrátor",
|
||||
"roleUser": "Uživatel"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Citlivost rychlého rolování",
|
||||
"moshCommandLabel": "Příkaz Mosh",
|
||||
"startupSnippetLabel": "Spustit snippet",
|
||||
"keepaliveIntervalLabel": "Interval uchování",
|
||||
"keepaliveIntervalLabel": "Interval udržování aktivity (sekundy)",
|
||||
"maxKeepaliveMisses": "Maximální počet neúspěšných",
|
||||
"tunnelSettings": "Nastavení tunelu",
|
||||
"enableTunneling": "Povolit tunely",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL zkopírováno",
|
||||
"telnetUrlCopied": "URL adresa Telnet zkopírována",
|
||||
"remoteDesktopUrlCopied": "URL vzdálené plochy zkopírována",
|
||||
"wakeOnLanAction": "Probuzení přes LAN",
|
||||
"wakeOnLanSuccess": "Magický balíček odeslán na {{name}}",
|
||||
"wakeOnLanError": "Nepodařilo se odeslat magický paket",
|
||||
"cloneHostAction": "Klonovat hostitele",
|
||||
"copyAddress": "Kopírovat adresu",
|
||||
"copyLink": "Kopírovat odkaz",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Žádné hostitele neodpovídají vašemu hledání",
|
||||
"hostNotFound": "Hostitel nenalezen",
|
||||
"searchHosts": "Hledat v hostech...",
|
||||
"sortHosts": "Seřadit hostitele",
|
||||
"sortDefault": "Výchozí pořadí",
|
||||
"sortNameAsc": "Jméno (A → Z)",
|
||||
"sortNameDesc": "Jméno (Z → A)",
|
||||
"sortIpAsc": "IP adresa (vzestupně)",
|
||||
"sortIpDesc": "IP adresa (popis)",
|
||||
"sortOnlineFirst": "Online první",
|
||||
"sortOfflineFirst": "Offline First",
|
||||
"sortPinnedFirst": "Připnuto jako první",
|
||||
"filterHosts": "Filtrovat hostitele",
|
||||
"filterClearAll": "Vymazat filtry",
|
||||
"filterStatusGroup": "Postavení",
|
||||
"filterOnline": "Online",
|
||||
"filterOffline": "Offline",
|
||||
"filterPinned": "Připnuto",
|
||||
"filterAuthGroup": "Typ autorizace",
|
||||
"filterAuthPassword": "Heslo",
|
||||
"filterAuthKey": "SSH klíč",
|
||||
"filterAuthCredential": "Pověření",
|
||||
"filterAuthNone": "Žádný",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protokol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "PRV",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Funkce",
|
||||
"filterFeatureTerminal": "Terminál",
|
||||
"filterFeatureFileManager": "Správce souborů",
|
||||
"filterFeatureTunnel": "Tunel",
|
||||
"filterFeatureDocker": "Přístavní dělník",
|
||||
"filterTagsGroup": "Štítky",
|
||||
"shareHost": "Sdílet hostitele",
|
||||
"shareHostTitle": "Sdílet: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Žádná karta nebyla přiřazena"
|
||||
"noTabAssigned": "Žádná karta nebyla přiřazena",
|
||||
"focusedPane": "Aktivní panel"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Žádné spojení",
|
||||
"noConnectionsDesc": "Otevřete terminál, správce souborů nebo vzdálenou plochu a zobrazte zde připojení.",
|
||||
"connectedFor": "Připojeno pro {{duration}}",
|
||||
"connected": "Připojeno",
|
||||
"disconnected": "Odpojeno",
|
||||
"closeTab": "Zavřít kartu",
|
||||
"closeConnection": "Zavřete spojení",
|
||||
"forgetTab": "Zapomenout",
|
||||
"removeBackground": "Odstranit",
|
||||
"reconnect": "Znovu se připojte",
|
||||
"reopenTab": "Znovu otevřít",
|
||||
"sectionOpen": "OTEVŘENO",
|
||||
"sectionBackground": "Pozadí",
|
||||
"backgroundDesc": "Relace přetrvávají 30 minut po odpojení a lze je znovu připojit.",
|
||||
"persisted": "Přetrvává v pozadí",
|
||||
"expiresIn": "Platnost vyprší za {{duration}}",
|
||||
"search": "Hledat spojení...",
|
||||
"noSearchResults": "Vašemu vyhledávání neodpovídají žádná spojení"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Připojování k {{type}} relaci...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Opakovat"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SPRÁVCE SSH SERVER",
|
||||
"tagline": "Samostatně hostované SSH a správa vzdálené plochy",
|
||||
"loginTitle": "Přihlásit se do Termix",
|
||||
"registerTitle": "Vytvořit účet",
|
||||
"forgotPassword": "Zapomněli jste heslo?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Vyberte počet panelů k zobrazení",
|
||||
"panesTitle": "Panely",
|
||||
"openTabsTitle": "Otevřené panely",
|
||||
"dragTabsHint": "Přetáhněte karty do panelů výše",
|
||||
"dragTabsHint": "Přetáhněte záložky do panelů výše nebo použijte Rychlé přiřazení",
|
||||
"dropHere": "Přetáhněte sem",
|
||||
"emptyPane": "Prázdné",
|
||||
"dashboard": "Nástěnka",
|
||||
"clearSplitScreen": "Vymazat rozdělenou obrazovku"
|
||||
"clearSplitScreen": "Vymazat rozdělenou obrazovku",
|
||||
"quickAssign": "Rychlé přiřazení",
|
||||
"alreadyAssigned": "Podokno {{index}}",
|
||||
"splitTab": "Rozdělit kartu",
|
||||
"addToSplit": "Přidat do Splitu",
|
||||
"removeFromSplit": "Odebrat ze Splitu",
|
||||
"assignToPane": "Přiřadit k podoknu"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Vytvořit snippet",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Nepodařilo se odstranit složku",
|
||||
"folderEditSuccess": "Složka byla úspěšně aktualizována",
|
||||
"folderEditFailed": "Nepodařilo se aktualizovat složku",
|
||||
"confirmRunMessage": "Spustit „{{name}}“?",
|
||||
"confirmRunButton": "Běh",
|
||||
"runSuccess": "Ano \"{{name}}\" v {{count}} terminálech",
|
||||
"copySuccess": "Zkopírováno \"{{name}}\" do schránky",
|
||||
"shareTitle": "Sdílet snippet",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Aktuální přístup",
|
||||
"shareLoadError": "Nepodařilo se načíst sdílená data",
|
||||
"loading": "Načítám...",
|
||||
"close": "Zavřít"
|
||||
"close": "Zavřít",
|
||||
"reorderFailed": "Nepodařilo se uložit pořadí úryvků"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Účet",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Téma",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Barva zvýraznění",
|
||||
"settingsFileManager": "Správce souborů",
|
||||
"fileColorCoding": "Barevné kódování souborů",
|
||||
"fileColorCodingDesc": "Barevné kódové soubory podle typu",
|
||||
"settingsTerminal": "Terminál",
|
||||
"commandAutocomplete": "Automatické dokončování příkazu",
|
||||
"commandAutocompleteDesc": "Zobrazit automatické dokončování při psaní",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Zvýraznit výstup terminálu",
|
||||
"commandPalette": "Paleta příkazu",
|
||||
"commandPaletteDesc": "Povolit klávesovou zkratku",
|
||||
"sessionPersistence": "Trvalost relace",
|
||||
"sessionPersistenceDesc": "Zachovat relace mezi opětovným připojením",
|
||||
"reopenTabsOnLogin": "Znovu otevřít karty po přihlášení",
|
||||
"reopenTabsOnLoginDesc": "Obnovte otevřené karty po přihlášení nebo obnovení stránky, a to i z jiného zařízení",
|
||||
"confirmTabClose": "Potvrdit zavření záložky",
|
||||
"confirmTabCloseDesc": "Zeptat se před zavřením karet terminálu",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Zobrazit štítky hostitele",
|
||||
"showHostTagsDesc": "Zobrazit tagy v seznamu hostitelů",
|
||||
"hostTrayOnClick": "Kliknutím rozbalíte Akce hostitele",
|
||||
"hostTrayOnClickDesc": "Vyžadovat kliknutí pro rozbalení akcí hostitele místo najetí myší",
|
||||
"pinAppRail": "Pin App Rail",
|
||||
"pinAppRailDesc": "Udržovat levý postranní panel aplikace vždy rozbalený, místo aby se rozbaloval při najetí myší",
|
||||
"settingsSnippets": "Výstřižky bloků",
|
||||
"foldersCollapsed": "Sbalené složky",
|
||||
"foldersCollapsedDesc": "Sbalit složky ve výchozím nastavení",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Stumper",
|
||||
"hostManager": "Vært Manager",
|
||||
"credentials": "Legitimation",
|
||||
"connections": "Forbindelser",
|
||||
"roleAdministrator": "Administrator",
|
||||
"roleUser": "Bruger"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Hurtig Rul Følsomhed",
|
||||
"moshCommandLabel": "Mosh Kommando",
|
||||
"startupSnippetLabel": "Opstart Snippet",
|
||||
"keepaliveIntervalLabel": "Keepalive Interval",
|
||||
"keepaliveIntervalLabel": "Keepalive-interval (sekunder)",
|
||||
"maxKeepaliveMisses": "Maks. Keepalive Mangler",
|
||||
"tunnelSettings": "Tunnel Indstillinger",
|
||||
"enableTunneling": "Aktiver Tunneling",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL kopieret",
|
||||
"telnetUrlCopied": "Telnet URL kopieret",
|
||||
"remoteDesktopUrlCopied": "Fjernskrivebords URL kopieret",
|
||||
"wakeOnLanAction": "Vågn på LAN",
|
||||
"wakeOnLanSuccess": "Magisk pakke sendt til {{name}}",
|
||||
"wakeOnLanError": "Kunne ikke sende magisk pakke",
|
||||
"cloneHostAction": "Klon Vært",
|
||||
"copyAddress": "Kopiér Adresse",
|
||||
"copyLink": "Kopier Link",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Ingen værter matcher din søgning",
|
||||
"hostNotFound": "Vært ikke fundet",
|
||||
"searchHosts": "Søg efter værter...",
|
||||
"sortHosts": "Sortér værter",
|
||||
"sortDefault": "Standardordre",
|
||||
"sortNameAsc": "Navn (A → Å)",
|
||||
"sortNameDesc": "Navn (Z → A)",
|
||||
"sortIpAsc": "IP-adresse (stigende)",
|
||||
"sortIpDesc": "IP-adresse (beskrivelse)",
|
||||
"sortOnlineFirst": "Online først",
|
||||
"sortOfflineFirst": "Offline først",
|
||||
"sortPinnedFirst": "Fastgjort først",
|
||||
"filterHosts": "Filtrer værter",
|
||||
"filterClearAll": "Ryd filtre",
|
||||
"filterStatusGroup": "Status",
|
||||
"filterOnline": "Online",
|
||||
"filterOffline": "Offline",
|
||||
"filterPinned": "Fastgjort",
|
||||
"filterAuthGroup": "Godkendelsestype",
|
||||
"filterAuthPassword": "Adgangskode",
|
||||
"filterAuthKey": "SSH-nøgle",
|
||||
"filterAuthCredential": "Legitimationsoplysninger",
|
||||
"filterAuthNone": "Ingen",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protokol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "Landdistriktsudviklingsplan",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Funktioner",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Filhåndtering",
|
||||
"filterFeatureTunnel": "Tunnel",
|
||||
"filterFeatureDocker": "Docker",
|
||||
"filterTagsGroup": "Tags",
|
||||
"shareHost": "Del Vært",
|
||||
"shareHostTitle": "Del: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Ingen fane tildelt"
|
||||
"noTabAssigned": "Ingen fane tildelt",
|
||||
"focusedPane": "Aktiv rude"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Ingen forbindelser",
|
||||
"noConnectionsDesc": "Åbn en terminal, filhåndtering eller fjernskrivebord for at se forbindelser her",
|
||||
"connectedFor": "Forbundet for {{duration}}",
|
||||
"connected": "Forbundet",
|
||||
"disconnected": "Afbrudt",
|
||||
"closeTab": "Luk faneblad",
|
||||
"closeConnection": "Luk forbindelse",
|
||||
"forgetTab": "Glemme",
|
||||
"removeBackground": "Fjerne",
|
||||
"reconnect": "Genopret forbindelse",
|
||||
"reopenTab": "Genåbn",
|
||||
"sectionOpen": "Åben",
|
||||
"sectionBackground": "Baggrund",
|
||||
"backgroundDesc": "Sessioner fortsætter i 30 minutter efter afbrydelse og kan genoprettes.",
|
||||
"persisted": "Fortsatte i baggrunden",
|
||||
"expiresIn": "Udløber om {{duration}}",
|
||||
"search": "Søg efter forbindelser...",
|
||||
"noSearchResults": "Ingen forbindelser matcher din søgning"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Forbinder til {{type}} session...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Forsøg igen"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH SERVER MANAGER",
|
||||
"tagline": "Selvhostet SSH og fjernskrivebordsadministration",
|
||||
"loginTitle": "Log ind på Termix",
|
||||
"registerTitle": "Opret Konto",
|
||||
"forgotPassword": "Glemt Adgangskode?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Vælg hvor mange ruder der skal vises",
|
||||
"panesTitle": "Paner",
|
||||
"openTabsTitle": "Åbn Faneblade",
|
||||
"dragTabsHint": "Træk faneblade i ruder ovenfor",
|
||||
"dragTabsHint": "Træk faner ind i ruderne ovenfor, eller brug Hurtig tildeling",
|
||||
"dropHere": "Slip her",
|
||||
"emptyPane": "Tom",
|
||||
"dashboard": "Instrumentbræt",
|
||||
"clearSplitScreen": "Ryd Opdelt Skærm"
|
||||
"clearSplitScreen": "Ryd Opdelt Skærm",
|
||||
"quickAssign": "Hurtig tildeling",
|
||||
"alreadyAssigned": "Ruden {{index}}",
|
||||
"splitTab": "Opdel faneblad",
|
||||
"addToSplit": "Tilføj til Opdeling",
|
||||
"removeFromSplit": "Fjern fra Split",
|
||||
"assignToPane": "Tildel til rude"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Opret Snippet",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Kunne ikke slette mappe",
|
||||
"folderEditSuccess": "Mappen er opdateret",
|
||||
"folderEditFailed": "Mislykkedes at opdatere mappe",
|
||||
"confirmRunMessage": "Kør \"{{name}}\"?",
|
||||
"confirmRunButton": "Løbe",
|
||||
"runSuccess": "Ran \"{{name}}\" i {{count}} terminal(er)",
|
||||
"copySuccess": "Kopieret \"{{name}}\" til udklipsholder",
|
||||
"shareTitle": "Del Snippet",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Nuværende Adgang",
|
||||
"shareLoadError": "Kunne ikke indlæse delingsdata",
|
||||
"loading": "Indlæser...",
|
||||
"close": "Luk"
|
||||
"close": "Luk",
|
||||
"reorderFailed": "Kunne ikke gemme rækkefølgen af uddrag"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Konto",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Tema",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Accent Farve",
|
||||
"settingsFileManager": "Filhåndtering",
|
||||
"fileColorCoding": "Fil Farvekodning",
|
||||
"fileColorCodingDesc": "Farvekode filer efter type",
|
||||
"settingsTerminal": "Terminal",
|
||||
"commandAutocomplete": "Kommando Autofuldfør",
|
||||
"commandAutocompleteDesc": "Vis autofuldførelse under indtastning",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Fremhæv terminal output",
|
||||
"commandPalette": "Kommando Palet",
|
||||
"commandPaletteDesc": "Aktivér tastaturgenvej",
|
||||
"sessionPersistence": "Session Persistens",
|
||||
"sessionPersistenceDesc": "Behold sessioner mellem genforbinder",
|
||||
"reopenTabsOnLogin": "Genåbn faner ved login",
|
||||
"reopenTabsOnLoginDesc": "Gendan dine åbne faner, når du logger ind eller opdaterer siden, selv fra en anden enhed",
|
||||
"confirmTabClose": "Bekræft Luk Af Faneblad",
|
||||
"confirmTabCloseDesc": "Spørg før lukning af terminalfaner",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Vis Vært Tags",
|
||||
"showHostTagsDesc": "Vis tags i værtslisten",
|
||||
"hostTrayOnClick": "Klik for at udvide værtshandlinger",
|
||||
"hostTrayOnClickDesc": "Kræv et klik for at udvide værtshandlinger i stedet for at holde musen over",
|
||||
"pinAppRail": "Fastgør app-skinne",
|
||||
"pinAppRailDesc": "Hold appskinnen i venstre sidebar altid udvidet i stedet for at udvide den, når du holder musen over den",
|
||||
"settingsSnippets": "Stumper",
|
||||
"foldersCollapsed": "Mapper Sammenbrudt",
|
||||
"foldersCollapsedDesc": "Fold som standard mapper sammen",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Schnipsel",
|
||||
"hostManager": "Host-Manager",
|
||||
"credentials": "Anmeldedaten",
|
||||
"connections": "Verbindungen",
|
||||
"roleAdministrator": "Administrator",
|
||||
"roleUser": "Benutzer"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Schnelle Scroll Empfindlichkeit",
|
||||
"moshCommandLabel": "Mosh-Befehl",
|
||||
"startupSnippetLabel": "Startschlitz",
|
||||
"keepaliveIntervalLabel": "Keepalive-Intervall",
|
||||
"keepaliveIntervalLabel": "Keepalive-Intervall (Sekunden)",
|
||||
"maxKeepaliveMisses": "Max. Keepalive Vermisste",
|
||||
"tunnelSettings": "Tunnel Einstellungen",
|
||||
"enableTunneling": "Tunneling aktivieren",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC-URL kopiert",
|
||||
"telnetUrlCopied": "Telnet-URL kopiert",
|
||||
"remoteDesktopUrlCopied": "Remote-Desktop-URL kopiert",
|
||||
"wakeOnLanAction": "Wake on LAN",
|
||||
"wakeOnLanSuccess": "Magisches Paket an {{name}} gesendet",
|
||||
"wakeOnLanError": "Das Senden des Magic Packets ist fehlgeschlagen.",
|
||||
"cloneHostAction": "Host klonen",
|
||||
"copyAddress": "Adresse kopieren",
|
||||
"copyLink": "Link kopieren",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Keine Hosts entsprechen Ihrer Suche",
|
||||
"hostNotFound": "Host nicht gefunden",
|
||||
"searchHosts": "Hosts suchen...",
|
||||
"sortHosts": "Hosts sortieren",
|
||||
"sortDefault": "Standardreihenfolge",
|
||||
"sortNameAsc": "Name (A → Z)",
|
||||
"sortNameDesc": "Name (Z → A)",
|
||||
"sortIpAsc": "IP-Adresse (aufsteigend)",
|
||||
"sortIpDesc": "IP-Adresse (Beschreibung)",
|
||||
"sortOnlineFirst": "Online First",
|
||||
"sortOfflineFirst": "Offline First",
|
||||
"sortPinnedFirst": "Als erstes angepinnt",
|
||||
"filterHosts": "Hosts filtern",
|
||||
"filterClearAll": "Filter löschen",
|
||||
"filterStatusGroup": "Status",
|
||||
"filterOnline": "Online",
|
||||
"filterOffline": "Offline",
|
||||
"filterPinned": "Angepinnt",
|
||||
"filterAuthGroup": "Authentifizierungstyp",
|
||||
"filterAuthPassword": "Passwort",
|
||||
"filterAuthKey": "SSH-Schlüssel",
|
||||
"filterAuthCredential": "Berechtigung",
|
||||
"filterAuthNone": "Keiner",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protokoll",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Merkmale",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Dateimanager",
|
||||
"filterFeatureTunnel": "Tunnel",
|
||||
"filterFeatureDocker": "Docker",
|
||||
"filterTagsGroup": "Tags",
|
||||
"shareHost": "Host teilen",
|
||||
"shareHostTitle": "Weitergeben: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Kein Tab zugewiesen"
|
||||
"noTabAssigned": "Kein Tab zugewiesen",
|
||||
"focusedPane": "Aktiver Bereich"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Keine Verbindungen",
|
||||
"noConnectionsDesc": "Öffnen Sie ein Terminal, einen Dateimanager oder eine Remote-Desktop-Verbindung, um die Verbindungen hier anzuzeigen.",
|
||||
"connectedFor": "Verbunden für {{duration}}",
|
||||
"connected": "Verbunden",
|
||||
"disconnected": "Getrennt",
|
||||
"closeTab": "Tab schließen",
|
||||
"closeConnection": "Enge Verbindung",
|
||||
"forgetTab": "Vergessen",
|
||||
"removeBackground": "Entfernen",
|
||||
"reconnect": "Wiederverbinden",
|
||||
"reopenTab": "Wieder öffnen",
|
||||
"sectionOpen": "Offen",
|
||||
"sectionBackground": "Hintergrund",
|
||||
"backgroundDesc": "Die Sitzungen bleiben nach der Trennung noch 30 Minuten bestehen und können wiederhergestellt werden.",
|
||||
"persisted": "Im Hintergrund bestehen geblieben",
|
||||
"expiresIn": "Läuft ab in {{duration}}",
|
||||
"search": "Verbindungen suchen...",
|
||||
"noSearchResults": "Es wurden keine Ergebnisse gefunden, die Ihrer Suche entsprechen."
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Verbinde mit {{type}} Sitzung...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Wiederholen"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH SERVERMANAGER",
|
||||
"tagline": "Selbstgehostete SSH- und Remote-Desktop-Verwaltung",
|
||||
"loginTitle": "Bei Termix anmelden",
|
||||
"registerTitle": "Konto erstellen",
|
||||
"forgotPassword": "Passwort vergessen?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Anzahl der anzuzeigenden Bereiche auswählen",
|
||||
"panesTitle": "Scheiben",
|
||||
"openTabsTitle": "Tabs öffnen",
|
||||
"dragTabsHint": "Tabs in die oberen Bereiche ziehen",
|
||||
"dragTabsHint": "Ziehen Sie Registerkarten in die oberen Bereiche oder verwenden Sie die Schnellzuweisung.",
|
||||
"dropHere": "Hier ablegen",
|
||||
"emptyPane": "Leer",
|
||||
"dashboard": "Dashboard",
|
||||
"clearSplitScreen": "Split Bildschirm löschen"
|
||||
"clearSplitScreen": "Split Bildschirm löschen",
|
||||
"quickAssign": "Schnellzuweisung",
|
||||
"alreadyAssigned": "Pane {{index}}",
|
||||
"splitTab": "Split Tab",
|
||||
"addToSplit": "Zu Split hinzufügen",
|
||||
"removeFromSplit": "Aus Split entfernen",
|
||||
"assignToPane": "Dem Bereich zuweisen"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Snippet erstellen",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Fehler beim Löschen des Ordners",
|
||||
"folderEditSuccess": "Ordner erfolgreich aktualisiert",
|
||||
"folderEditFailed": "Fehler beim Aktualisieren des Ordners",
|
||||
"confirmRunMessage": "Run \"{{name}}\"?",
|
||||
"confirmRunButton": "Laufen",
|
||||
"runSuccess": "\"{{name}}\" in {{count}} Terminal(n) ran",
|
||||
"copySuccess": "Kopiert \"{{name}}\" in die Zwischenablage",
|
||||
"shareTitle": "Snippet teilen",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Aktueller Zugriff",
|
||||
"shareLoadError": "Fehler beim Laden der Freigabedaten",
|
||||
"loading": "Wird geladen...",
|
||||
"close": "Schließen"
|
||||
"close": "Schließen",
|
||||
"reorderFailed": "Fehler beim Speichern der Snippet-Reihenfolge"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Konto",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Thema",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Akzentfarbe",
|
||||
"settingsFileManager": "Datei-Manager",
|
||||
"fileColorCoding": "Dateigröße Codierung",
|
||||
"fileColorCodingDesc": "Farbcode-Dateien nach Typ",
|
||||
"settingsTerminal": "Terminal",
|
||||
"commandAutocomplete": "Befehl Autovervollständigung",
|
||||
"commandAutocompleteDesc": "Beim Tippen Autovervollständigung anzeigen",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Terminalausgabe hervorheben",
|
||||
"commandPalette": "Kommandopalette",
|
||||
"commandPaletteDesc": "Tastaturkürzel aktivieren",
|
||||
"sessionPersistence": "Session-Persistenz",
|
||||
"sessionPersistenceDesc": "Sitzungen zwischen Wiederverbindungen behalten",
|
||||
"reopenTabsOnLogin": "Tabs beim Anmelden wieder öffnen",
|
||||
"reopenTabsOnLoginDesc": "Ihre geöffneten Tabs werden beim Anmelden oder Aktualisieren der Seite wiederhergestellt, sogar von einem anderen Gerät aus.",
|
||||
"confirmTabClose": "Tab-Schließen bestätigen",
|
||||
"confirmTabCloseDesc": "Vor Schließen von Terminal-Tabs fragen",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Host-Tags anzeigen",
|
||||
"showHostTagsDesc": "Tags in der Hostliste anzeigen",
|
||||
"hostTrayOnClick": "Klicken Sie hier, um die Host-Aktionen zu erweitern.",
|
||||
"hostTrayOnClickDesc": "Zum Erweitern der Host-Aktionen ist ein Klick anstelle eines Hover-Effekts erforderlich.",
|
||||
"pinAppRail": "Pin App Rail",
|
||||
"pinAppRailDesc": "Die linke Seitenleiste der App-Bereiche sollte immer ausgeklappt sein, anstatt sich erst beim Überfahren mit der Maus zu erweitern.",
|
||||
"settingsSnippets": "Schnipsel",
|
||||
"foldersCollapsed": "Ordner zusammenklappt",
|
||||
"foldersCollapsedDesc": "Ordner standardmäßig einklappen",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Δείγματα",
|
||||
"hostManager": "Διαχειριστής Υπολογιστών",
|
||||
"credentials": "Διαπιστευτήρια",
|
||||
"connections": "Συνδέσεις",
|
||||
"roleAdministrator": "Διαχειριστής",
|
||||
"roleUser": "Χρήστης"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Ευαισθησία Γρήγορης Κύλισης",
|
||||
"moshCommandLabel": "Εντολή Mosh",
|
||||
"startupSnippetLabel": "Δείγμα Εκκίνησης",
|
||||
"keepaliveIntervalLabel": "Διάστημα Keepalive",
|
||||
"keepaliveIntervalLabel": "Διάστημα διατήρησης ζωντανής λειτουργίας (δευτερόλεπτα)",
|
||||
"maxKeepaliveMisses": "Μέγιστες Αστοχίες Keepalive",
|
||||
"tunnelSettings": "Ρυθμίσεις Tunnel",
|
||||
"enableTunneling": "Ενεργοποίηση Tunneling",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "Το URL VNC αντιγράφηκε",
|
||||
"telnetUrlCopied": "Το URL Telnet αντιγράφηκε",
|
||||
"remoteDesktopUrlCopied": "Η διεύθυνση URL απομακρυσμένης επιφάνειας εργασίας αντιγράφηκε",
|
||||
"wakeOnLanAction": "Αφύπνιση σε LAN",
|
||||
"wakeOnLanSuccess": "Μαγικό πακέτο στάλθηκε στον/στην {{name}}",
|
||||
"wakeOnLanError": "Αποτυχία αποστολής μαγικού πακέτου",
|
||||
"cloneHostAction": "Κλωνοποίηση Υπολογιστή",
|
||||
"copyAddress": "Αντιγραφή Διεύθυνσης",
|
||||
"copyLink": "Αντιγραφή Συνδέσμου",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Κανένας υπολογιστής δεν ταιριάζει με την αναζήτησή σας",
|
||||
"hostNotFound": "Δεν βρέθηκε υπολογιστής",
|
||||
"searchHosts": "Αναζήτηση υπολογιστών...",
|
||||
"sortHosts": "Ταξινόμηση κεντρικών υπολογιστών",
|
||||
"sortDefault": "Προεπιλεγμένη παραγγελία",
|
||||
"sortNameAsc": "Όνομα (Α → Ω)",
|
||||
"sortNameDesc": "Όνομα (Ω → Α)",
|
||||
"sortIpAsc": "Διεύθυνση IP (Αύξουσα)",
|
||||
"sortIpDesc": "Διεύθυνση IP (Περιγραφή)",
|
||||
"sortOnlineFirst": "Πρώτα στο διαδίκτυο",
|
||||
"sortOfflineFirst": "Πρώτα εκτός σύνδεσης",
|
||||
"sortPinnedFirst": "Καρφιτσώθηκε πρώτα",
|
||||
"filterHosts": "Φιλτράρισμα κεντρικών υπολογιστών",
|
||||
"filterClearAll": "Καθαρισμός φίλτρων",
|
||||
"filterStatusGroup": "Κατάσταση",
|
||||
"filterOnline": "Διαδικτυακά",
|
||||
"filterOffline": "Εκτός σύνδεσης",
|
||||
"filterPinned": "Καρφιτσωμένο",
|
||||
"filterAuthGroup": "Τύπος Εξουσιοδότησης",
|
||||
"filterAuthPassword": "Σύνθημα",
|
||||
"filterAuthKey": "Κλειδί SSH",
|
||||
"filterAuthCredential": "Πιστοποιητικό",
|
||||
"filterAuthNone": "Κανένας",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Πρωτόκολλο",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "ΠΑΑ",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Τέλνετ",
|
||||
"filterFeaturesGroup": "Χαρακτηριστικά",
|
||||
"filterFeatureTerminal": "Τερματικό",
|
||||
"filterFeatureFileManager": "Διαχειριστής αρχείων",
|
||||
"filterFeatureTunnel": "Σήραγγα",
|
||||
"filterFeatureDocker": "Λιμενεργάτης",
|
||||
"filterTagsGroup": "Ετικέτες",
|
||||
"shareHost": "Κοινοποίηση Διακομιστή",
|
||||
"shareHostTitle": "Κοινή χρήση: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Δεν έχει ανατεθεί καρτέλα"
|
||||
"noTabAssigned": "Δεν έχει ανατεθεί καρτέλα",
|
||||
"focusedPane": "Ενεργό παράθυρο"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Δεν υπάρχουν συνδέσεις",
|
||||
"noConnectionsDesc": "Ανοίξτε ένα τερματικό, έναν διαχειριστή αρχείων ή μια απομακρυσμένη επιφάνεια εργασίας για να δείτε τις συνδέσεις εδώ",
|
||||
"connectedFor": "Συνδεδεμένος για {{duration}}",
|
||||
"connected": "Συνδεδεμένος",
|
||||
"disconnected": "Ασύνδετος",
|
||||
"closeTab": "Κλείσιμο καρτέλας",
|
||||
"closeConnection": "Κλείσιμο σύνδεσης",
|
||||
"forgetTab": "Ξεχνώ",
|
||||
"removeBackground": "Αφαιρώ",
|
||||
"reconnect": "Επανασύνδεση",
|
||||
"reopenTab": "Ξανανοίγω",
|
||||
"sectionOpen": "Ανοιχτό",
|
||||
"sectionBackground": "Φόντο",
|
||||
"backgroundDesc": "Οι συνεδρίες διαρκούν για 30 λεπτά μετά την αποσύνδεση και μπορούν να επανασυνδεθούν.",
|
||||
"persisted": "Επέμεινε στο παρασκήνιο",
|
||||
"expiresIn": "Λήγει σε {{duration}}",
|
||||
"search": "Αναζήτηση συνδέσεων...",
|
||||
"noSearchResults": "Δεν υπάρχουν συνδέσεις που να αντιστοιχούν στην αναζήτησή σας"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Σύνδεση σε συνεδρία {{type}}...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Επανάληψη"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "ΔΙΑΧΕΙΡΙΣΗ SSH ΔΙΑΧΕΙΡΙΣΗΣ ΔΙΑΚΟΠΗΣ",
|
||||
"tagline": "Αυτοφιλοξενούμενη διαχείριση SSH και απομακρυσμένης επιφάνειας εργασίας",
|
||||
"loginTitle": "Συνδεθείτε στο Termix",
|
||||
"registerTitle": "Δημιουργία Λογαριασμού",
|
||||
"forgotPassword": "Ξεχάσατε Τον Κωδικό Πρόσβασης?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Επιλέξτε πόσα παράθυρα θα εμφανίζονται",
|
||||
"panesTitle": "Πίνακες",
|
||||
"openTabsTitle": "Άνοιγμα Καρτελών",
|
||||
"dragTabsHint": "Σύρετε τις καρτέλες στα παράθυρα πάνω",
|
||||
"dragTabsHint": "Σύρετε καρτέλες στα παραπάνω παράθυρα ή χρησιμοποιήστε τη Γρήγορη αντιστοίχιση",
|
||||
"dropHere": "Ρίξτε εδώ",
|
||||
"emptyPane": "Κενό",
|
||||
"dashboard": "Ταμπλό",
|
||||
"clearSplitScreen": "Καθαρισμός Διαίρεσης Οθόνης"
|
||||
"clearSplitScreen": "Καθαρισμός Διαίρεσης Οθόνης",
|
||||
"quickAssign": "Γρήγορη ανάθεση",
|
||||
"alreadyAssigned": "Παράθυρο {{index}}",
|
||||
"splitTab": "Διαίρεση καρτέλας",
|
||||
"addToSplit": "Προσθήκη στο Split",
|
||||
"removeFromSplit": "Αφαίρεση από το Σπλιτ",
|
||||
"assignToPane": "Ανάθεση σε παράθυρο"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Δημιουργία Δείγματος",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Αποτυχία διαγραφής φακέλου",
|
||||
"folderEditSuccess": "Ο φάκελος ενημερώθηκε επιτυχώς",
|
||||
"folderEditFailed": "Αποτυχία ενημέρωσης φακέλου",
|
||||
"confirmRunMessage": "Εκτέλεση \"{{name}}\";",
|
||||
"confirmRunButton": "Τρέξιμο",
|
||||
"runSuccess": "Ran \"{{name}}\" σε τερματικό(ά) {{count}}",
|
||||
"copySuccess": "Αντιγράφηκε το \"{{name}}\" στο πρόχειρο",
|
||||
"shareTitle": "Μοιραστείτε Δείγμα",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Τρέχουσα Πρόσβαση",
|
||||
"shareLoadError": "Αποτυχία φόρτωσης δεδομένων κοινής χρήσης",
|
||||
"loading": "Φόρτωση...",
|
||||
"close": "Κλείσιμο"
|
||||
"close": "Κλείσιμο",
|
||||
"reorderFailed": "Η αποθήκευση της σειράς αποσπάσματος απέτυχε"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Λογαριασμός",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Θέμα",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Χρώμα Έμφασης",
|
||||
"settingsFileManager": "Διαχειριστής Αρχείων",
|
||||
"fileColorCoding": "Κωδικοποίηση Χρώματος Αρχείου",
|
||||
"fileColorCodingDesc": "Αρχεία χρωμάτων-κώδικα κατά τύπο",
|
||||
"settingsTerminal": "Τερματικό",
|
||||
"commandAutocomplete": "Αυτόματη Ολοκλήρωση Εντολής",
|
||||
"commandAutocompleteDesc": "Εμφάνιση αυτόματης συμπλήρωσης κατά την πληκτρολόγηση",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Επισήμανση εξόδου τερματικού",
|
||||
"commandPalette": "Παλέτα Εντολών",
|
||||
"commandPaletteDesc": "Ενεργοποίηση συντόμευσης πληκτρολογίου",
|
||||
"sessionPersistence": "Διάρκεια Συνεδρίας",
|
||||
"sessionPersistenceDesc": "Διατήρηση συνεδριών μεταξύ επανασύνδεσης",
|
||||
"reopenTabsOnLogin": "Επανάνοιγμα καρτελών κατά τη σύνδεση",
|
||||
"reopenTabsOnLoginDesc": "Επαναφέρετε τις ανοιχτές καρτέλες σας όταν συνδέεστε ή ανανεώνετε τη σελίδα, ακόμα και από άλλη συσκευή",
|
||||
"confirmTabClose": "Επιβεβαίωση Κλεισίματος Καρτέλας",
|
||||
"confirmTabCloseDesc": "Ερώτηση πριν το κλείσιμο καρτελών τερματικού",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Εμφάνιση Ετικετών Υπολογιστή",
|
||||
"showHostTagsDesc": "Εμφάνιση ετικετών στη λίστα κεντρικών υπολογιστών",
|
||||
"hostTrayOnClick": "Κάντε κλικ για να αναπτύξετε τις Ενέργειες κεντρικού υπολογιστή",
|
||||
"hostTrayOnClickDesc": "Απαιτείται κλικ για την ανάπτυξη των ενεργειών κεντρικού υπολογιστή αντί για τοποθέτηση του δείκτη του ποντικιού",
|
||||
"pinAppRail": "Καρφίτσωμα εφαρμογής Rail",
|
||||
"pinAppRailDesc": "Διατηρήστε το αριστερό πλευρικό πλαίσιο εφαρμογής πάντα ανοιχτό αντί να επεκτείνεται κατά την τοποθέτηση του δείκτη του ποντικιού",
|
||||
"settingsSnippets": "Δείγματα",
|
||||
"foldersCollapsed": "Φάκελοι Που Απορρίφθηκαν",
|
||||
"foldersCollapsedDesc": "Σύμπτυξη φακέλων από προεπιλογή",
|
||||
|
||||
@@ -100,11 +100,11 @@
|
||||
"edit": "Editar",
|
||||
"add": "Añadir",
|
||||
"confirm": "Confirmar",
|
||||
"no": "Nu",
|
||||
"no": "No",
|
||||
"or": "O",
|
||||
"next": "Siguiente",
|
||||
"previous": "Anterior",
|
||||
"refresh": "Refrescar",
|
||||
"refresh": "Recargar",
|
||||
"language": "Idioma",
|
||||
"checking": "Comprobando...",
|
||||
"checkingDatabase": "Comprobando conexión a la base de datos...",
|
||||
@@ -128,7 +128,7 @@
|
||||
"tunnels": "Túneles",
|
||||
"fileManager": "Gestor de archivos",
|
||||
"serverStats": "Estadísticas del Servidor",
|
||||
"admin": "Admin",
|
||||
"admin": "Administrador",
|
||||
"userProfile": "Perfil de usuario",
|
||||
"splitScreen": "Dividir pantalla",
|
||||
"confirmClose": "¿Cerrar esta sesión activa?",
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Fragmentos",
|
||||
"hostManager": "Gestor de Anfitriones",
|
||||
"credentials": "Credenciales",
|
||||
"connections": "Conexiones",
|
||||
"roleAdministrator": "Administrador",
|
||||
"roleUser": "Usuario"
|
||||
},
|
||||
@@ -220,7 +221,7 @@
|
||||
"themePreview": "Vista previa del tema",
|
||||
"theme": "Tema",
|
||||
"fontFamily": "Familia de fuente",
|
||||
"fontSize": "Font Size",
|
||||
"fontSize": "Tamaño de fuente",
|
||||
"letterSpacing": "Espacio de letras",
|
||||
"lineHeight": "Altura de línea",
|
||||
"cursorStyle": "Estilo del cursor",
|
||||
@@ -313,7 +314,7 @@
|
||||
"terminalAppearance": "Apariencia Terminal",
|
||||
"colorTheme": "Tema de color",
|
||||
"fontFamilyLabel": "Familia de fuente",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"fontSizeLabel": "Tamaño de fuente",
|
||||
"cursorStyleLabel": "Estilo del cursor",
|
||||
"letterSpacingPx": "Espacio de letras (px)",
|
||||
"lineHeightLabel": "Altura de línea",
|
||||
@@ -342,10 +343,10 @@
|
||||
"fastScrollSensitivityLabel": "Sensibilidad de desplazamiento rápido",
|
||||
"moshCommandLabel": "Mosh Comando",
|
||||
"startupSnippetLabel": "Fragmento de inicio",
|
||||
"keepaliveIntervalLabel": "Intervalo de mantenimiento",
|
||||
"keepaliveIntervalLabel": "Intervalo de mantenimiento de conexión (segundos)",
|
||||
"maxKeepaliveMisses": "Misiones máximas de Keepalive",
|
||||
"tunnelSettings": "Configuración del túnel",
|
||||
"enableTunneling": "Activar Tunneling",
|
||||
"enableTunneling": "Activar túnel",
|
||||
"enableTunnelingDesc": "Habilitar la funcionalidad de túnel SSH para este host",
|
||||
"serverTunnelsSection": "Túneles de servidor",
|
||||
"addTunnelBtn": "Añadir túnel",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "URL VNC copiada",
|
||||
"telnetUrlCopied": "URL de Telnet copiada",
|
||||
"remoteDesktopUrlCopied": "URL de escritorio remoto copiada",
|
||||
"wakeOnLanAction": "Activar la LAN",
|
||||
"wakeOnLanSuccess": "Paquete mágico enviado a {{name}}",
|
||||
"wakeOnLanError": "Error al enviar el paquete mágico",
|
||||
"cloneHostAction": "Clonar Host",
|
||||
"copyAddress": "Copiar dirección",
|
||||
"copyLink": "Copiar enlace",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Ningún host coincide con tu búsqueda",
|
||||
"hostNotFound": "Host no encontrado",
|
||||
"searchHosts": "Buscar hosts...",
|
||||
"sortHosts": "Ordenar hosts",
|
||||
"sortDefault": "Orden predeterminada",
|
||||
"sortNameAsc": "Nombre (A → Z)",
|
||||
"sortNameDesc": "Nombre (Z → A)",
|
||||
"sortIpAsc": "Dirección IP (Asc)",
|
||||
"sortIpDesc": "Dirección IP (Descripción)",
|
||||
"sortOnlineFirst": "Primero en línea",
|
||||
"sortOfflineFirst": "Primero sin conexión",
|
||||
"sortPinnedFirst": "Fijado primero",
|
||||
"filterHosts": "Filtrar hosts",
|
||||
"filterClearAll": "Borrar filtros",
|
||||
"filterStatusGroup": "Estado",
|
||||
"filterOnline": "En línea",
|
||||
"filterOffline": "Desconectado",
|
||||
"filterPinned": "Fijado",
|
||||
"filterAuthGroup": "Tipo de autenticación",
|
||||
"filterAuthPassword": "Contraseña",
|
||||
"filterAuthKey": "Clave SSH",
|
||||
"filterAuthCredential": "Credencial",
|
||||
"filterAuthNone": "Ninguno",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protocolo",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Características",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Gestor de archivos",
|
||||
"filterFeatureTunnel": "Túnel",
|
||||
"filterFeatureDocker": "Estibador",
|
||||
"filterTagsGroup": "Etiquetas",
|
||||
"shareHost": "Compartir Host",
|
||||
"shareHostTitle": "Compartir: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Ninguna pestaña asignada"
|
||||
"noTabAssigned": "Ninguna pestaña asignada",
|
||||
"focusedPane": "Panel activo"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Sin conexiones",
|
||||
"noConnectionsDesc": "Abra una terminal, un administrador de archivos o un escritorio remoto para ver las conexiones aquí.",
|
||||
"connectedFor": "Conectado para {{duration}}",
|
||||
"connected": "Conectado",
|
||||
"disconnected": "Desconectado",
|
||||
"closeTab": "Cerrar pestaña",
|
||||
"closeConnection": "Conexión estrecha",
|
||||
"forgetTab": "Olvidar",
|
||||
"removeBackground": "Eliminar",
|
||||
"reconnect": "Reconectar",
|
||||
"reopenTab": "Reabrir",
|
||||
"sectionOpen": "Abierto",
|
||||
"sectionBackground": "Fondo",
|
||||
"backgroundDesc": "Las sesiones se mantienen activas durante 30 minutos después de la desconexión y se pueden volver a conectar.",
|
||||
"persisted": "Persistió en segundo plano",
|
||||
"expiresIn": "Caduca en {{duration}}",
|
||||
"search": "Buscar conexiones...",
|
||||
"noSearchResults": "No se encontraron conexiones que coincidan con su búsqueda."
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Conectando a la sesión {{type}}...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Reintentar"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "MANAGER DE SSH SERVIDOR",
|
||||
"tagline": "SSH autoalojado y administración remota de escritorio",
|
||||
"loginTitle": "Iniciar sesión en Termix",
|
||||
"registerTitle": "Crear cuenta",
|
||||
"forgotPassword": "¿Olvidó la contraseña?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Elegir cuántos paneles mostrar",
|
||||
"panesTitle": "Paneles",
|
||||
"openTabsTitle": "Abrir Pestañas",
|
||||
"dragTabsHint": "Arrastra las pestañas en los paneles de arriba",
|
||||
"dragTabsHint": "Arrastra las pestañas a los paneles superiores o usa la función Asignación rápida.",
|
||||
"dropHere": "Soltar aquí",
|
||||
"emptyPane": "Vaciar",
|
||||
"dashboard": "Tablero",
|
||||
"clearSplitScreen": "Limpiar pantalla dividida"
|
||||
"clearSplitScreen": "Limpiar pantalla dividida",
|
||||
"quickAssign": "Asignación rápida",
|
||||
"alreadyAssigned": "Panel {{index}}",
|
||||
"splitTab": "Pestaña dividida",
|
||||
"addToSplit": "Agregar a Dividir",
|
||||
"removeFromSplit": "Eliminar de Split",
|
||||
"assignToPane": "Asignar al panel"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Crear Fragmento",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Error al eliminar la carpeta",
|
||||
"folderEditSuccess": "Carpeta actualizada correctamente",
|
||||
"folderEditFailed": "Error al actualizar la carpeta",
|
||||
"confirmRunMessage": "Ejecutar \"{{name}}\"?",
|
||||
"confirmRunButton": "Correr",
|
||||
"runSuccess": "Ran \"{{name}}\" en terminales {{count}}",
|
||||
"copySuccess": "Copiado \"{{name}}\" al portapapeles",
|
||||
"shareTitle": "Compartir Snippet",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Acceso actual",
|
||||
"shareLoadError": "Error al cargar los datos compartidos",
|
||||
"loading": "Cargando...",
|
||||
"close": "Cerrar"
|
||||
"close": "Cerrar",
|
||||
"reorderFailed": "No se pudo guardar el orden del fragmento."
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Cuenta",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Tema",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Color de acento",
|
||||
"settingsFileManager": "Gestor de archivos",
|
||||
"fileColorCoding": "Codificación de color de archivo",
|
||||
"fileColorCodingDesc": "Archivos de código de color por tipo",
|
||||
"settingsTerminal": "Terminal",
|
||||
"commandAutocomplete": "Comando autocompletado",
|
||||
"commandAutocompleteDesc": "Mostrar autocompletado al escribir",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Resaltar salida de terminal",
|
||||
"commandPalette": "Paleta de comandos",
|
||||
"commandPaletteDesc": "Activar atajo de teclado",
|
||||
"sessionPersistence": "Persistencia de la sesión",
|
||||
"sessionPersistenceDesc": "Mantener sesiones entre reconectas",
|
||||
"reopenTabsOnLogin": "Reabrir pestañas al iniciar sesión",
|
||||
"reopenTabsOnLoginDesc": "Restaura tus pestañas abiertas cuando inicies sesión o actualices la página, incluso desde otro dispositivo.",
|
||||
"confirmTabClose": "Confirmar cierre de pestaña",
|
||||
"confirmTabCloseDesc": "Preguntar antes de cerrar pestañas de terminal",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Mostrar etiquetas de host",
|
||||
"showHostTagsDesc": "Mostrar etiquetas en la lista de hosts",
|
||||
"hostTrayOnClick": "Haz clic para expandir las acciones del anfitrión.",
|
||||
"hostTrayOnClickDesc": "Requerir un clic para expandir las acciones del anfitrión en lugar de pasar el cursor sobre ellas.",
|
||||
"pinAppRail": "Pin App Rail",
|
||||
"pinAppRailDesc": "Mantener siempre expandida la barra lateral izquierda en lugar de expandirla al pasar el cursor sobre ella.",
|
||||
"settingsSnippets": "Fragmentos",
|
||||
"foldersCollapsed": "Carpetas colapsadas",
|
||||
"foldersCollapsedDesc": "Colapsar carpetas por defecto",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Projekti",
|
||||
"hostManager": "Isäntälaitteen Hallinta",
|
||||
"credentials": "Käyttäjätunnukset",
|
||||
"connections": "Yhteydet",
|
||||
"roleAdministrator": "Ylläpitäjä",
|
||||
"roleUser": "Käyttäjä"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Nopea Vieritä Herkkyys",
|
||||
"moshCommandLabel": "Mosh Komento",
|
||||
"startupSnippetLabel": "Käynnistä Projekti",
|
||||
"keepaliveIntervalLabel": "Keepaliivinen Aikaväli",
|
||||
"keepaliveIntervalLabel": "Säilytysaika (sekuntia)",
|
||||
"maxKeepaliveMisses": "Enimmäismäärä Keepalive Misses",
|
||||
"tunnelSettings": "Tunnelin Asetukset",
|
||||
"enableTunneling": "Ota Tunnistus Käyttöön",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL kopioitu",
|
||||
"telnetUrlCopied": "Telnet URL kopioitu",
|
||||
"remoteDesktopUrlCopied": "Etätyöpöydän URL kopioitu",
|
||||
"wakeOnLanAction": "Herätys lähiverkossa",
|
||||
"wakeOnLanSuccess": "Taikapaketti lähetetty osoitteeseen {{name}}",
|
||||
"wakeOnLanError": "Taikapaketin lähettäminen epäonnistui",
|
||||
"cloneHostAction": "Kloonaa Isäntä",
|
||||
"copyAddress": "Kopioi Osoite",
|
||||
"copyLink": "Kopioi Linkki",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Yksikään isäntä ei vastaa hakuasi",
|
||||
"hostNotFound": "Palvelinta ei löydy",
|
||||
"searchHosts": "Etsi palvelimia...",
|
||||
"sortHosts": "Lajittele isännät",
|
||||
"sortDefault": "Oletusjärjestys",
|
||||
"sortNameAsc": "Nimi (A → Ö)",
|
||||
"sortNameDesc": "Nimi (Ö → A)",
|
||||
"sortIpAsc": "IP-osoite (nouseva)",
|
||||
"sortIpDesc": "IP-osoite (kuvaus)",
|
||||
"sortOnlineFirst": "Verkossa ensin",
|
||||
"sortOfflineFirst": "Offline First",
|
||||
"sortPinnedFirst": "Kiinnitetty ensin",
|
||||
"filterHosts": "Suodata isännät",
|
||||
"filterClearAll": "Tyhjennä suodattimet",
|
||||
"filterStatusGroup": "Status",
|
||||
"filterOnline": "Verkossa",
|
||||
"filterOffline": "Offline-tilassa",
|
||||
"filterPinned": "Kiinnitetty",
|
||||
"filterAuthGroup": "Valtuutustyyppi",
|
||||
"filterAuthPassword": "Salasana",
|
||||
"filterAuthKey": "SSH-avain",
|
||||
"filterAuthCredential": "Valtakirja",
|
||||
"filterAuthNone": "Ei mitään",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protokolla",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "Maaseudun kehittämisohjelma",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Ominaisuudet",
|
||||
"filterFeatureTerminal": "Terminaali",
|
||||
"filterFeatureFileManager": "Tiedostonhallinta",
|
||||
"filterFeatureTunnel": "Tunneli",
|
||||
"filterFeatureDocker": "Satamatyöläinen",
|
||||
"filterTagsGroup": "Tunnisteet",
|
||||
"shareHost": "Jaa Isäntä",
|
||||
"shareHostTitle": "Jaa: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Yhtään välilehteä ei määritetty"
|
||||
"noTabAssigned": "Yhtään välilehteä ei määritetty",
|
||||
"focusedPane": "Aktiivinen ruutu"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Ei yhteyksiä",
|
||||
"noConnectionsDesc": "Avaa pääte, tiedostonhallinta tai etätyöpöytä nähdäksesi yhteydet täällä",
|
||||
"connectedFor": "Yhdistetty kohteeseen {{duration}}",
|
||||
"connected": "Yhdistetty",
|
||||
"disconnected": "Yhteys katkennut",
|
||||
"closeTab": "Sulje välilehti",
|
||||
"closeConnection": "Sulje yhteys",
|
||||
"forgetTab": "Unohtaa",
|
||||
"removeBackground": "Poistaa",
|
||||
"reconnect": "Yhdistä uudelleen",
|
||||
"reopenTab": "Avaa uudelleen",
|
||||
"sectionOpen": "Avata",
|
||||
"sectionBackground": "Tausta",
|
||||
"backgroundDesc": "Istunto jatkuu 30 minuuttia yhteyden katkaisemisen jälkeen, ja se voidaan yhdistää uudelleen.",
|
||||
"persisted": "Jatkui taustalla",
|
||||
"expiresIn": "Vanhenee {{duration}} kuluttua",
|
||||
"search": "Hae yhteyksiä...",
|
||||
"noSearchResults": "Ei hakuasi vastaavia yhteyksiä"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Yhdistetään {{type}} istuntoon...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Yritä Uudelleen"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH PALVELIN HALLINTO",
|
||||
"tagline": "Itse isännöity SSH ja etätyöpöytähallinta",
|
||||
"loginTitle": "Kirjaudu Termixiin",
|
||||
"registerTitle": "Luo Tili",
|
||||
"forgotPassword": "Unohditko Salasanan?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Valitse kuinka monta paneelia näytetään",
|
||||
"panesTitle": "Paneelit",
|
||||
"openTabsTitle": "Avaa Välilehdet",
|
||||
"dragTabsHint": "Vedä välilehdet paneeleihin",
|
||||
"dragTabsHint": "Vedä välilehdet yllä oleviin ruutuihin tai käytä pikamääritystä",
|
||||
"dropHere": "Pudota tähän",
|
||||
"emptyPane": "Tyhjä",
|
||||
"dashboard": "Hallintapaneeli",
|
||||
"clearSplitScreen": "Tyhjennä Jaettu Näyttö"
|
||||
"clearSplitScreen": "Tyhjennä Jaettu Näyttö",
|
||||
"quickAssign": "Pikamääritys",
|
||||
"alreadyAssigned": "Ruutu {{index}}",
|
||||
"splitTab": "Jaettu välilehti",
|
||||
"addToSplit": "Lisää Splitiin",
|
||||
"removeFromSplit": "Poista jaosta",
|
||||
"assignToPane": "Määritä ruutuun"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Luo Projekti",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Kansion poistaminen epäonnistui",
|
||||
"folderEditSuccess": "Kansio päivitetty onnistuneesti",
|
||||
"folderEditFailed": "Kansion päivitys epäonnistui",
|
||||
"confirmRunMessage": "Suorittaa \"{{name}}\"?",
|
||||
"confirmRunButton": "Juokse",
|
||||
"runSuccess": "Suoritti komennon \"{{name}}\" {{count}} päätteessä/päätteissä",
|
||||
"copySuccess": "Kopioitu \"{{name}}\" leikepöydälle",
|
||||
"shareTitle": "Jaa Projekti",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Nykyinen Käyttöoikeus",
|
||||
"shareLoadError": "Jakamisen tietojen lataaminen epäonnistui",
|
||||
"loading": "Ladataan...",
|
||||
"close": "Sulje"
|
||||
"close": "Sulje",
|
||||
"reorderFailed": "Katkelmien järjestyksen tallentaminen epäonnistui"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Tili",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Teema",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Korostettu Väri",
|
||||
"settingsFileManager": "Tiedostojen Hallinta",
|
||||
"fileColorCoding": "Tiedoston Värikoodaus",
|
||||
"fileColorCodingDesc": "Värikooditiedostot tyypin mukaan",
|
||||
"settingsTerminal": "Pääte",
|
||||
"commandAutocomplete": "Komennon Automaattitäydennys",
|
||||
"commandAutocompleteDesc": "Näytä automaattitäydennys kirjoitettaessa",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Korosta päätteen ulostulo",
|
||||
"commandPalette": "Komentopaletti",
|
||||
"commandPaletteDesc": "Ota käyttöön pikanäppäin",
|
||||
"sessionPersistence": "Istunnon Pysyvyys",
|
||||
"sessionPersistenceDesc": "Pidä istunnot uudelleenyhteyksien välillä",
|
||||
"reopenTabsOnLogin": "Avaa välilehdet uudelleen kirjautumisen yhteydessä",
|
||||
"reopenTabsOnLoginDesc": "Palauta avoimet välilehdet, kun kirjaudut sisään tai päivität sivun, jopa toiselta laitteelta",
|
||||
"confirmTabClose": "Vahvista Välilehti Sulje",
|
||||
"confirmTabCloseDesc": "Kysy ennen päätevälilehtien sulkemista",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Näytä Isäntätunnisteet",
|
||||
"showHostTagsDesc": "Näytä tagit isäntäluettelossa",
|
||||
"hostTrayOnClick": "Napsauta laajentaaksesi isäntätoiminnot",
|
||||
"hostTrayOnClickDesc": "Vaaditaan napsautus isäntätoimintojen laajentamiseksi hiiren osoittimen sijaan",
|
||||
"pinAppRail": "Kiinnitä sovellusraide",
|
||||
"pinAppRailDesc": "Pidä vasemman sivupalkin sovelluspalkki aina laajennettuna sen sijaan, että se laajenisi hiiren osoittimen vaikutuksesta",
|
||||
"settingsSnippets": "Projekti",
|
||||
"foldersCollapsed": "Kansiot Tiivistetty",
|
||||
"foldersCollapsedDesc": "Pienennä kansiot oletuksena",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Extraits",
|
||||
"hostManager": "Responsable d'hôte",
|
||||
"credentials": "Identifiants",
|
||||
"connections": "Relations",
|
||||
"roleAdministrator": "Administrateur",
|
||||
"roleUser": "Utilisateur"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Sensibilité au défilement rapide",
|
||||
"moshCommandLabel": "Commande Mosh",
|
||||
"startupSnippetLabel": "Snippet de démarrage",
|
||||
"keepaliveIntervalLabel": "Intervalle de Keepalive",
|
||||
"keepaliveIntervalLabel": "Intervalle de maintien en vie (secondes)",
|
||||
"maxKeepaliveMisses": "Nombre max de manques de Keepalive",
|
||||
"tunnelSettings": "Paramètres du tunnel",
|
||||
"enableTunneling": "Activer le Tunneling",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "URL VNC copiée",
|
||||
"telnetUrlCopied": "URL Telnet copiée",
|
||||
"remoteDesktopUrlCopied": "URL du bureau distant copiée",
|
||||
"wakeOnLanAction": "Wake on LAN",
|
||||
"wakeOnLanSuccess": "Paquet magique envoyé à {{name}}",
|
||||
"wakeOnLanError": "Échec de l'envoi du paquet magique",
|
||||
"cloneHostAction": "Cloner l'hôte",
|
||||
"copyAddress": "Copier l'adresse",
|
||||
"copyLink": "Copier le lien",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Aucun hôte ne correspond à votre recherche",
|
||||
"hostNotFound": "Hôte introuvable",
|
||||
"searchHosts": "Rechercher des hôtes...",
|
||||
"sortHosts": "Trier les hôtes",
|
||||
"sortDefault": "Ordre par défaut",
|
||||
"sortNameAsc": "Nom (A → Z)",
|
||||
"sortNameDesc": "Nom (Z → A)",
|
||||
"sortIpAsc": "Adresse IP (Asc)",
|
||||
"sortIpDesc": "Adresse IP (Description)",
|
||||
"sortOnlineFirst": "En ligne d'abord",
|
||||
"sortOfflineFirst": "D'abord hors ligne",
|
||||
"sortPinnedFirst": "Épinglé en premier",
|
||||
"filterHosts": "Filtrer les hôtes",
|
||||
"filterClearAll": "Filtres transparents",
|
||||
"filterStatusGroup": "Statut",
|
||||
"filterOnline": "En ligne",
|
||||
"filterOffline": "Hors ligne",
|
||||
"filterPinned": "Épinglé",
|
||||
"filterAuthGroup": "Type d'autorisation",
|
||||
"filterAuthPassword": "Mot de passe",
|
||||
"filterAuthKey": "Clé SSH",
|
||||
"filterAuthCredential": "Attestation",
|
||||
"filterAuthNone": "Aucun",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protocole",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Caractéristiques",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Gestionnaire de fichiers",
|
||||
"filterFeatureTunnel": "Tunnel",
|
||||
"filterFeatureDocker": "Docker",
|
||||
"filterTagsGroup": "Étiquettes",
|
||||
"shareHost": "Partager l'hôte",
|
||||
"shareHostTitle": "Partager: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Aucun onglet assigné"
|
||||
"noTabAssigned": "Aucun onglet assigné",
|
||||
"focusedPane": "Volet actif"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Aucune connexion",
|
||||
"noConnectionsDesc": "Ouvrez un terminal, un gestionnaire de fichiers ou un bureau à distance pour voir les connexions ici.",
|
||||
"connectedFor": "Connecté pour {{duration}}",
|
||||
"connected": "Connecté",
|
||||
"disconnected": "Déconnecté",
|
||||
"closeTab": "Fermer l'onglet",
|
||||
"closeConnection": "Connexion étroite",
|
||||
"forgetTab": "Oublier",
|
||||
"removeBackground": "Retirer",
|
||||
"reconnect": "Reconnect",
|
||||
"reopenTab": "Rouvrir",
|
||||
"sectionOpen": "Ouvrir",
|
||||
"sectionBackground": "Arrière-plan",
|
||||
"backgroundDesc": "Les sessions restent actives pendant 30 minutes après la déconnexion et peuvent être reconnectées.",
|
||||
"persisted": "Persistant en arrière-plan",
|
||||
"expiresIn": "Expire dans {{duration}}",
|
||||
"search": "Rechercher des connexions...",
|
||||
"noSearchResults": "Aucun résultat ne correspond à votre recherche"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Connexion à la session {{type}}...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Réessayer"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "GESTION DU SERVEUR SSH",
|
||||
"tagline": "Gestion SSH et de bureau à distance auto-hébergée",
|
||||
"loginTitle": "Se connecter à Termix",
|
||||
"registerTitle": "Créer un compte",
|
||||
"forgotPassword": "Mot de passe oublié ?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Choisir le nombre de panneaux à afficher",
|
||||
"panesTitle": "Panneaux",
|
||||
"openTabsTitle": "Onglets ouverts",
|
||||
"dragTabsHint": "Glisser les onglets dans les panneaux au-dessus",
|
||||
"dragTabsHint": "Faites glisser les onglets dans les volets ci-dessus ou utilisez l'attribution rapide.",
|
||||
"dropHere": "Déposer ici",
|
||||
"emptyPane": "Vide",
|
||||
"dashboard": "Tableau de bord",
|
||||
"clearSplitScreen": "Nettoyer l'écran partagé"
|
||||
"clearSplitScreen": "Nettoyer l'écran partagé",
|
||||
"quickAssign": "Affectation rapide",
|
||||
"alreadyAssigned": "Volet {{index}}",
|
||||
"splitTab": "Onglet fractionné",
|
||||
"addToSplit": "Ajouter au fractionnement",
|
||||
"removeFromSplit": "Retirer de la division",
|
||||
"assignToPane": "Affecter au volet"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Créer un snippet",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Échec de la suppression du dossier",
|
||||
"folderEditSuccess": "Dossier mis à jour avec succès",
|
||||
"folderEditFailed": "Échec de la mise à jour du dossier",
|
||||
"confirmRunMessage": "Exécutez \"{{name}}\"?",
|
||||
"confirmRunButton": "Courir",
|
||||
"runSuccess": "Exécuter \"{{name}}\" dans un terminal {{count}}",
|
||||
"copySuccess": "Copié «{{name}}» dans le presse-papiers",
|
||||
"shareTitle": "Partager le Snippet",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Accès actuel",
|
||||
"shareLoadError": "Impossible de charger les données de partage",
|
||||
"loading": "Chargement en cours...",
|
||||
"close": "Fermer"
|
||||
"close": "Fermer",
|
||||
"reorderFailed": "Impossible d'enregistrer l'ordre des extraits"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Compte client",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Thème",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Couleur d'accentuation",
|
||||
"settingsFileManager": "Gestionnaire de fichiers",
|
||||
"fileColorCoding": "Codage des couleurs du fichier",
|
||||
"fileColorCodingDesc": "Code couleur des fichiers par type",
|
||||
"settingsTerminal": "Terminal",
|
||||
"commandAutocomplete": "Auto-complétion de commande",
|
||||
"commandAutocompleteDesc": "Afficher la saisie automatique lors de la saisie",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Surligner la sortie du terminal",
|
||||
"commandPalette": "Palette de commandes",
|
||||
"commandPaletteDesc": "Activer le raccourci clavier",
|
||||
"sessionPersistence": "Persistance de la session",
|
||||
"sessionPersistenceDesc": "Conserver les sessions entre les reconnexions",
|
||||
"reopenTabsOnLogin": "Rouvrir les onglets à la connexion",
|
||||
"reopenTabsOnLoginDesc": "Vos onglets ouverts seront restaurés lorsque vous vous connectez ou actualisez la page, même depuis un autre appareil.",
|
||||
"confirmTabClose": "Confirmer la fermeture de l'onglet",
|
||||
"confirmTabCloseDesc": "Demander avant de fermer les onglets du terminal",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Afficher les tags de l'hôte",
|
||||
"showHostTagsDesc": "Afficher les tags dans la liste des hôtes",
|
||||
"hostTrayOnClick": "Cliquez pour afficher les actions de l'hôte",
|
||||
"hostTrayOnClickDesc": "Exiger un clic pour afficher les actions de l'hôte au lieu d'un survol",
|
||||
"pinAppRail": "Rail d'application Pin",
|
||||
"pinAppRailDesc": "Conserver le panneau latéral gauche toujours déployé au lieu de le déployer au survol.",
|
||||
"settingsSnippets": "Extraits",
|
||||
"foldersCollapsed": "Dossiers réduits",
|
||||
"foldersCollapsedDesc": "Réduire les dossiers par défaut",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "קטעי טקסט",
|
||||
"hostManager": "מנהל מארח",
|
||||
"credentials": "אישורים",
|
||||
"connections": "חיבורים",
|
||||
"roleAdministrator": "מְנַהֵל",
|
||||
"roleUser": "מִשׁתַמֵשׁ"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "רגישות גלילה מהירה",
|
||||
"moshCommandLabel": "פיקוד מוש",
|
||||
"startupSnippetLabel": "קטע הפעלה",
|
||||
"keepaliveIntervalLabel": "מרווח זמן שמירה",
|
||||
"keepaliveIntervalLabel": "מרווח זמן שמירה (שניות)",
|
||||
"maxKeepaliveMisses": "מקס קיפלייב מחמיץ",
|
||||
"tunnelSettings": "הגדרות מנהרה",
|
||||
"enableTunneling": "הפעלת מנהור",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "כתובת ה-URL של VNC הועתקה",
|
||||
"telnetUrlCopied": "כתובת ה-URL של Telnet הועתקה",
|
||||
"remoteDesktopUrlCopied": "כתובת ה-URL של שולחן עבודה מרוחק הועתקה",
|
||||
"wakeOnLanAction": "התעוררות ברשת מקומית (LAN)",
|
||||
"wakeOnLanSuccess": "חבילת קסם נשלחה אל {{name}}",
|
||||
"wakeOnLanError": "שליחת חבילת הקסם נכשלה",
|
||||
"cloneHostAction": "מארח משוכפל",
|
||||
"copyAddress": "העתקת כתובת",
|
||||
"copyLink": "העתק קישור",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "אין מארחים התואמים את החיפוש שלך",
|
||||
"hostNotFound": "המארח לא נמצא",
|
||||
"searchHosts": "חיפוש מארחים...",
|
||||
"sortHosts": "מיין מארחים",
|
||||
"sortDefault": "סדר ברירת מחדל",
|
||||
"sortNameAsc": "שם (א' → ת')",
|
||||
"sortNameDesc": "שם (ת → א)",
|
||||
"sortIpAsc": "כתובת IP (עולה)",
|
||||
"sortIpDesc": "כתובת IP (תיאור)",
|
||||
"sortOnlineFirst": "מקוון תחילה",
|
||||
"sortOfflineFirst": "לא מקוון תחילה",
|
||||
"sortPinnedFirst": "הוצמד ראשון",
|
||||
"filterHosts": "סנן מארחים",
|
||||
"filterClearAll": "נקה מסננים",
|
||||
"filterStatusGroup": "סטָטוּס",
|
||||
"filterOnline": "באינטרנט",
|
||||
"filterOffline": "לא מקוון",
|
||||
"filterPinned": "מוצמד",
|
||||
"filterAuthGroup": "סוג אימות",
|
||||
"filterAuthPassword": "סִיסמָה",
|
||||
"filterAuthKey": "מפתח SSH",
|
||||
"filterAuthCredential": "תְעוּדָה",
|
||||
"filterAuthNone": "אַף לֹא אֶחָד",
|
||||
"filterAuthOpkssh": "אופקש",
|
||||
"filterProtocolGroup": "פּרוֹטוֹקוֹל",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "טלנט",
|
||||
"filterFeaturesGroup": "תכונות",
|
||||
"filterFeatureTerminal": "מָסוֹף",
|
||||
"filterFeatureFileManager": "מנהל הקבצים",
|
||||
"filterFeatureTunnel": "מִנהָרָה",
|
||||
"filterFeatureDocker": "דוקר",
|
||||
"filterTagsGroup": "תגיות",
|
||||
"shareHost": "שתף מארח",
|
||||
"shareHostTitle": "שתף: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "חלונית {{index}} - ריקה",
|
||||
"noTabAssigned": "לא הוקצתה לשונית"
|
||||
"noTabAssigned": "לא הוקצתה לשונית",
|
||||
"focusedPane": "חלונית פעילה"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "אין קשרים",
|
||||
"noConnectionsDesc": "פתח מסוף, מנהל קבצים או שולחן עבודה מרוחק כדי לראות חיבורים כאן",
|
||||
"connectedFor": "מחובר עבור {{duration}}",
|
||||
"connected": "מְחוּבָּר",
|
||||
"disconnected": "מְנוּתָק",
|
||||
"closeTab": "סגור את הכרטיסייה",
|
||||
"closeConnection": "קשר קרוב",
|
||||
"forgetTab": "לִשְׁכּוֹחַ",
|
||||
"removeBackground": "לְהַסִיר",
|
||||
"reconnect": "התחבר מחדש",
|
||||
"reopenTab": "פתיחה מחדש",
|
||||
"sectionOpen": "לִפְתוֹחַ",
|
||||
"sectionBackground": "רֶקַע",
|
||||
"backgroundDesc": "החיבור נשמר למשך 30 דקות לאחר הניתוק וניתן להתחבר מחדש.",
|
||||
"persisted": "נשמר ברקע",
|
||||
"expiresIn": "פג תוקף בעוד {{duration}}",
|
||||
"search": "חיפוש חיבורים...",
|
||||
"noSearchResults": "אין קשרים התואמים את החיפוש שלך"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "מתחבר לסשן {{type}}...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "נסה שוב"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "מנהל שרתי SSH",
|
||||
"tagline": "ניהול SSH ושולחן עבודה מרוחק באחסון עצמי",
|
||||
"loginTitle": "התחברות לטרמיקס",
|
||||
"registerTitle": "צור חשבון",
|
||||
"forgotPassword": "שכחת סיסמה?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "בחר כמה חלוניות להציג",
|
||||
"panesTitle": "חלוניות",
|
||||
"openTabsTitle": "פתח כרטיסיות",
|
||||
"dragTabsHint": "גרור כרטיסיות לחלוניות למעלה",
|
||||
"dragTabsHint": "גררו כרטיסיות לחלוניות למעלה, או השתמשו באפשרות 'הקצאה מהירה'",
|
||||
"dropHere": "זרוק כאן",
|
||||
"emptyPane": "רֵיק",
|
||||
"dashboard": "לוּחַ מַחווָנִים",
|
||||
"clearSplitScreen": "נקה מסך מפוצל"
|
||||
"clearSplitScreen": "נקה מסך מפוצל",
|
||||
"quickAssign": "הקצאה מהירה",
|
||||
"alreadyAssigned": "חלונית {{index}}",
|
||||
"splitTab": "פיצול טאב",
|
||||
"addToSplit": "הוסף לפיצול",
|
||||
"removeFromSplit": "הסר מפיצול",
|
||||
"assignToPane": "הקצאה לחלונית"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "צור קטע",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "מחיקת התיקייה נכשלה",
|
||||
"folderEditSuccess": "התיקייה עודכנה בהצלחה",
|
||||
"folderEditFailed": "עדכון התיקייה נכשל",
|
||||
"confirmRunMessage": "להפעיל את \"{{name}}\"?",
|
||||
"confirmRunButton": "לָרוּץ",
|
||||
"runSuccess": "הריץ את \"{{name}}\" בטרמינל/ים {{count}}",
|
||||
"copySuccess": "הועתק \"{{name}}\" ללוח",
|
||||
"shareTitle": "שתף קטע",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "גישה נוכחית",
|
||||
"shareLoadError": "טעינת נתוני השיתוף נכשלה",
|
||||
"loading": "טְעִינָה...",
|
||||
"close": "לִסְגוֹר"
|
||||
"close": "לִסְגוֹר",
|
||||
"reorderFailed": "שמירת סדר הקטעים נכשלה"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "חֶשְׁבּוֹן",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "נוֹשֵׂא",
|
||||
"fontSizeLabel": "גודל גופן",
|
||||
"accentColorLabel": "צבע מבטא",
|
||||
"settingsFileManager": "מנהל הקבצים",
|
||||
"fileColorCoding": "קידוד צבע קבצים",
|
||||
"fileColorCodingDesc": "קידוד צבע של קבצים לפי סוג",
|
||||
"settingsTerminal": "מָסוֹף",
|
||||
"commandAutocomplete": "השלמה אוטומטית של הפקודה",
|
||||
"commandAutocompleteDesc": "הצג השלמה אוטומטית בעת הקלדה",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "הדגש את פלט הטרמינל",
|
||||
"commandPalette": "לוח פקודות",
|
||||
"commandPaletteDesc": "הפעל קיצור מקלדת",
|
||||
"sessionPersistence": "התמדה של סשן",
|
||||
"sessionPersistenceDesc": "שמירת מפגשים בין חיבורים מחדש",
|
||||
"reopenTabsOnLogin": "פתח מחדש כרטיסיות בעת הכניסה",
|
||||
"reopenTabsOnLoginDesc": "שחזר את הכרטיסיות הפתוחות שלך בעת כניסה או רענון הדף, אפילו ממכשיר אחר",
|
||||
"confirmTabClose": "אישור סגירת כרטיסייה",
|
||||
"confirmTabCloseDesc": "שאל לפני סגירת כרטיסיות הטרמינל",
|
||||
"settingsSidebar": "סרגל צד",
|
||||
"showHostTags": "הצג תגיות מארח",
|
||||
"showHostTagsDesc": "הצגת תגיות ברשימת המארחים",
|
||||
"hostTrayOnClick": "לחץ להרחבת פעולות מארח",
|
||||
"hostTrayOnClickDesc": "נדרש לחיצה כדי להרחיב את פעולות המארח במקום לרחף",
|
||||
"pinAppRail": "אפליקציית Pin Rail",
|
||||
"pinAppRailDesc": "השאר את פס האפליקציה של הצד השמאלי פתוח תמיד במקום להרחיב בעת ריחוף",
|
||||
"settingsSnippets": "קטעי טקסט",
|
||||
"foldersCollapsed": "תיקיות מכווצות",
|
||||
"foldersCollapsedDesc": "כיווץ תיקיות כברירת מחדל",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "स्निपेट्स",
|
||||
"hostManager": "मेजबान प्रबंधक",
|
||||
"credentials": "साख",
|
||||
"connections": "कनेक्शन",
|
||||
"roleAdministrator": "प्रशासक",
|
||||
"roleUser": "उपयोगकर्ता"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "तेज़ स्क्रॉल संवेदनशीलता",
|
||||
"moshCommandLabel": "मोश कमांड",
|
||||
"startupSnippetLabel": "स्टार्टअप स्निपेट",
|
||||
"keepaliveIntervalLabel": "कीपअलाइव अंतराल",
|
||||
"keepaliveIntervalLabel": "कीपअलाइव अंतराल (सेकंड)",
|
||||
"maxKeepaliveMisses": "मैक्स कीपअलाइव चूक",
|
||||
"tunnelSettings": "टनल सेटिंग्स",
|
||||
"enableTunneling": "टनलिंग सक्षम करें",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL कॉपी हो गया",
|
||||
"telnetUrlCopied": "टेलनेट यूआरएल कॉपी हो गया",
|
||||
"remoteDesktopUrlCopied": "रिमोट डेस्कटॉप यूआरएल कॉपी हो गया",
|
||||
"wakeOnLanAction": "लैन पर जागो",
|
||||
"wakeOnLanSuccess": "मैजिक पैकेट {{name}} को भेजा गया",
|
||||
"wakeOnLanError": "मैजिक पैकेट भेजने में विफल",
|
||||
"cloneHostAction": "क्लोन होस्ट",
|
||||
"copyAddress": "पता कॉपी करें",
|
||||
"copyLink": "लिंक की प्रतिलिपि करें",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "आपकी खोज से मेल खाने वाले कोई होस्ट नहीं हैं",
|
||||
"hostNotFound": "होस्ट नहीं मिला",
|
||||
"searchHosts": "होस्ट खोजें...",
|
||||
"sortHosts": "होस्ट को क्रमबद्ध करें",
|
||||
"sortDefault": "डिफ़ॉल्ट ऑर्डर",
|
||||
"sortNameAsc": "नाम (अ से जेड तक)",
|
||||
"sortNameDesc": "नाम (Z → A)",
|
||||
"sortIpAsc": "आईपी पता (आरोही)",
|
||||
"sortIpDesc": "आईपी पता (विवरण)",
|
||||
"sortOnlineFirst": "ऑनलाइन फर्स्ट",
|
||||
"sortOfflineFirst": "ऑफ़लाइन प्रथम",
|
||||
"sortPinnedFirst": "पहले पिन किया गया",
|
||||
"filterHosts": "होस्ट को फ़िल्टर करें",
|
||||
"filterClearAll": "फ़िल्टर साफ़ करें",
|
||||
"filterStatusGroup": "स्थिति",
|
||||
"filterOnline": "ऑनलाइन",
|
||||
"filterOffline": "ऑफलाइन",
|
||||
"filterPinned": "पिन की गई",
|
||||
"filterAuthGroup": "प्राधिकरण प्रकार",
|
||||
"filterAuthPassword": "पासवर्ड",
|
||||
"filterAuthKey": "एसएसएच कुंजी",
|
||||
"filterAuthCredential": "क्रेडेंशियल",
|
||||
"filterAuthNone": "कोई नहीं",
|
||||
"filterAuthOpkssh": "ओपीकेएसएच",
|
||||
"filterProtocolGroup": "शिष्टाचार",
|
||||
"filterProtocolSsh": "एसएसएच",
|
||||
"filterProtocolRdp": "आरडीपी",
|
||||
"filterProtocolVnc": "वीएनसी",
|
||||
"filterProtocolTelnet": "टेलनेट",
|
||||
"filterFeaturesGroup": "विशेषताएँ",
|
||||
"filterFeatureTerminal": "टर्मिनल",
|
||||
"filterFeatureFileManager": "फ़ाइल मैनेजर",
|
||||
"filterFeatureTunnel": "सुरंग",
|
||||
"filterFeatureDocker": "डाक में काम करनेवाला मज़दूर",
|
||||
"filterTagsGroup": "टैग",
|
||||
"shareHost": "होस्ट साझा करें",
|
||||
"shareHostTitle": "शेयर करें: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "फलक {{index}} - खाली",
|
||||
"noTabAssigned": "कोई टैब आवंटित नहीं किया गया"
|
||||
"noTabAssigned": "कोई टैब आवंटित नहीं किया गया",
|
||||
"focusedPane": "सक्रिय फलक"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "कोई कनेक्शन नहीं",
|
||||
"noConnectionsDesc": "यहां कनेक्शन देखने के लिए टर्मिनल, फ़ाइल मैनेजर या रिमोट डेस्कटॉप खोलें।",
|
||||
"connectedFor": "{{duration}} के लिए कनेक्टेड",
|
||||
"connected": "जुड़े हुए",
|
||||
"disconnected": "डिस्कनेक्ट किया गया",
|
||||
"closeTab": "टैब बंद करें",
|
||||
"closeConnection": "घनिष्ठ संबंध",
|
||||
"forgetTab": "भूल जाओ",
|
||||
"removeBackground": "निकालना",
|
||||
"reconnect": "रिकनेक्ट",
|
||||
"reopenTab": "फिर से खोलना",
|
||||
"sectionOpen": "खुला",
|
||||
"sectionBackground": "पृष्ठभूमि",
|
||||
"backgroundDesc": "कनेक्शन टूटने के बाद भी सेशन 30 मिनट तक जारी रहता है और दोबारा कनेक्ट किया जा सकता है।",
|
||||
"persisted": "पृष्ठभूमि में बना रहा",
|
||||
"expiresIn": "समाप्ति तिथि {{duration}}",
|
||||
"search": "कनेक्शन खोजें...",
|
||||
"noSearchResults": "आपकी खोज से कोई संबंध नहीं मिला"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "{{type}} सत्र से कनेक्ट हो रहा है...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "पुन: प्रयास करें"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "एसएसएच सर्वर प्रबंधक",
|
||||
"tagline": "सेल्फ-होस्टेड एसएसएच और रिमोट डेस्कटॉप प्रबंधन",
|
||||
"loginTitle": "टर्मिक्स में लॉग इन करें",
|
||||
"registerTitle": "खाता बनाएं",
|
||||
"forgotPassword": "पासवर्ड भूल गए?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "प्रदर्शित होने वाले पैनलों की संख्या चुनें",
|
||||
"panesTitle": "फलक",
|
||||
"openTabsTitle": "टैब खोलें",
|
||||
"dragTabsHint": "टैब को ऊपर दिए गए फलकों में खींचें",
|
||||
"dragTabsHint": "टैब को ऊपर दिए गए पैन में खींचें, या क्विक असाइनमेंट का उपयोग करें।",
|
||||
"dropHere": "यहां छोड़ें",
|
||||
"emptyPane": "खाली",
|
||||
"dashboard": "डैशबोर्ड",
|
||||
"clearSplitScreen": "स्प्लिट स्क्रीन साफ़ करें"
|
||||
"clearSplitScreen": "स्प्लिट स्क्रीन साफ़ करें",
|
||||
"quickAssign": "त्वरित असाइनमेंट",
|
||||
"alreadyAssigned": "फलक {{index}}",
|
||||
"splitTab": "स्प्लिट टैब",
|
||||
"addToSplit": "स्प्लिट में जोड़ें",
|
||||
"removeFromSplit": "स्प्लिट से निकालें",
|
||||
"assignToPane": "फलक को असाइन करें"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "स्निपेट बनाएं",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "फ़ोल्डर को हटाने में विफल",
|
||||
"folderEditSuccess": "फ़ोल्डर सफलतापूर्वक अपडेट हो गया",
|
||||
"folderEditFailed": "फ़ोल्डर को अपडेट करने में विफल",
|
||||
"confirmRunMessage": "\"{{name}} \" चलाएँ?",
|
||||
"confirmRunButton": "दौड़ना",
|
||||
"runSuccess": "Ran \"{{name}}\" in {{count}} terminal(s)",
|
||||
"copySuccess": "क्लिपबोर्ड पर \"{{name}}\" कॉपी किया गया",
|
||||
"shareTitle": "स्निपेट साझा करें",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "वर्तमान पहुंच",
|
||||
"shareLoadError": "शेयर डेटा लोड करने में विफल",
|
||||
"loading": "लोड हो रहा है...",
|
||||
"close": "बंद करना"
|
||||
"close": "बंद करना",
|
||||
"reorderFailed": "स्निपेट क्रम को सहेजने में विफल"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "खाता",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "विषय",
|
||||
"fontSizeLabel": "फ़ॉन्ट आकार",
|
||||
"accentColorLabel": "एक्सेंट रंग",
|
||||
"settingsFileManager": "फ़ाइल मैनेजर",
|
||||
"fileColorCoding": "फ़ाइल रंग कोडिंग",
|
||||
"fileColorCodingDesc": "फ़ाइलों को उनके प्रकार के अनुसार रंग-कोडित करें",
|
||||
"settingsTerminal": "टर्मिनल",
|
||||
"commandAutocomplete": "कमांड ऑटो-कंप्लीट",
|
||||
"commandAutocompleteDesc": "टाइप करते समय ऑटो-कंप्लीट दिखाएं",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "टर्मिनल आउटपुट को हाइलाइट करें",
|
||||
"commandPalette": "कमांड पैलेट",
|
||||
"commandPaletteDesc": "कीबोर्ड शॉर्टकट सक्षम करें",
|
||||
"sessionPersistence": "सत्र दृढ़ता",
|
||||
"sessionPersistenceDesc": "पुनः कनेक्ट होने के बीच सत्र जारी रखें",
|
||||
"reopenTabsOnLogin": "लॉगिन करने पर टैब दोबारा खोलें",
|
||||
"reopenTabsOnLoginDesc": "लॉग इन करने या पेज को रिफ्रेश करने पर, यहां तक कि किसी अन्य डिवाइस से भी, आपके खुले टैब रीस्टोर हो जाएंगे।",
|
||||
"confirmTabClose": "टैब बंद करने की पुष्टि करें",
|
||||
"confirmTabCloseDesc": "टर्मिनल टैब बंद करने से पहले पूछें",
|
||||
"settingsSidebar": "साइड बार",
|
||||
"showHostTags": "शो होस्ट टैग",
|
||||
"showHostTagsDesc": "होस्ट सूची में टैग प्रदर्शित करें",
|
||||
"hostTrayOnClick": "होस्ट की कार्रवाइयों का विस्तार करने के लिए क्लिक करें",
|
||||
"hostTrayOnClickDesc": "होस्ट एक्शन को विस्तारित करने के लिए होवर के बजाय क्लिक की आवश्यकता होती है।",
|
||||
"pinAppRail": "पिन ऐप रेल",
|
||||
"pinAppRailDesc": "होवर करने पर विस्तार करने के बजाय, बाएँ साइडबार ऐप रेल को हमेशा विस्तारित रखें।",
|
||||
"settingsSnippets": "स्निपेट्स",
|
||||
"foldersCollapsed": "फ़ोल्डर बंद कर दिए गए",
|
||||
"foldersCollapsedDesc": "फ़ोल्डर डिफ़ॉल्ट रूप से बंद हो जाते हैं",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Kódrészletek",
|
||||
"hostManager": "Házigazda-kezelő",
|
||||
"credentials": "Hitelesítő adatok",
|
||||
"connections": "Kapcsolatok",
|
||||
"roleAdministrator": "Adminisztrátor",
|
||||
"roleUser": "Felhasználó"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Gyors görgetési érzékenység",
|
||||
"moshCommandLabel": "Mosh parancsnokság",
|
||||
"startupSnippetLabel": "Indítási kódrészlet",
|
||||
"keepaliveIntervalLabel": "Életben tartási intervallum",
|
||||
"keepaliveIntervalLabel": "Életben tartási időköz (másodperc)",
|
||||
"maxKeepaliveMisses": "Max Keepalive kihagyások",
|
||||
"tunnelSettings": "Alagútbeállítások",
|
||||
"enableTunneling": "Alagútkezelés engedélyezése",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL másolva",
|
||||
"telnetUrlCopied": "Telnet URL másolva",
|
||||
"remoteDesktopUrlCopied": "Távoli asztal URL-címe másolva",
|
||||
"wakeOnLanAction": "Ébresztés LAN-on keresztül",
|
||||
"wakeOnLanSuccess": "Mágikus csomag elküldve a {{name}} címre",
|
||||
"wakeOnLanError": "Nem sikerült elküldeni a mágikus csomagot",
|
||||
"cloneHostAction": "Klónozó gazdagép",
|
||||
"copyAddress": "Cím másolása",
|
||||
"copyLink": "Link másolása",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Nincsenek a keresésnek megfelelő házigazdák",
|
||||
"hostNotFound": "Nem található gazdagép",
|
||||
"searchHosts": "Gazdagépek keresése...",
|
||||
"sortHosts": "Gazdagépek rendezése",
|
||||
"sortDefault": "Alapértelmezett sorrend",
|
||||
"sortNameAsc": "Név (A → Z)",
|
||||
"sortNameDesc": "Név (Z → A)",
|
||||
"sortIpAsc": "IP-cím (növekvő)",
|
||||
"sortIpDesc": "IP-cím (leírás)",
|
||||
"sortOnlineFirst": "Online First",
|
||||
"sortOfflineFirst": "Offline First",
|
||||
"sortPinnedFirst": "Elsőként rögzítve",
|
||||
"filterHosts": "Szűrőállomások",
|
||||
"filterClearAll": "Szűrők törlése",
|
||||
"filterStatusGroup": "Állapot",
|
||||
"filterOnline": "Online",
|
||||
"filterOffline": "Offline",
|
||||
"filterPinned": "Rögzítve",
|
||||
"filterAuthGroup": "Aut. típus",
|
||||
"filterAuthPassword": "Jelszó",
|
||||
"filterAuthKey": "SSH-kulcs",
|
||||
"filterAuthCredential": "Hitelesítő adat",
|
||||
"filterAuthNone": "Egyik sem",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Jegyzőkönyv",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "Vidékfejlesztési Program",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Jellemzők",
|
||||
"filterFeatureTerminal": "Terminál",
|
||||
"filterFeatureFileManager": "Fájlkezelő",
|
||||
"filterFeatureTunnel": "Alagút",
|
||||
"filterFeatureDocker": "Dokkmunkás",
|
||||
"filterTagsGroup": "Címkék",
|
||||
"shareHost": "Megosztási házigazda",
|
||||
"shareHostTitle": "Megosztás: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "{{index}} ablaktábla - üres",
|
||||
"noTabAssigned": "Nincs hozzárendelve tabulátor"
|
||||
"noTabAssigned": "Nincs hozzárendelve tabulátor",
|
||||
"focusedPane": "Aktív ablaktábla"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Nincsenek kapcsolatok",
|
||||
"noConnectionsDesc": "Nyisson meg egy terminált, fájlkezelőt vagy távoli asztalt a kapcsolatok megtekintéséhez.",
|
||||
"connectedFor": "Csatlakoztatva ehhez: {{duration}}",
|
||||
"connected": "Csatlakoztatva",
|
||||
"disconnected": "Szétkapcsolt",
|
||||
"closeTab": "Lap bezárása",
|
||||
"closeConnection": "Szoros kapcsolat",
|
||||
"forgetTab": "Felejtsd el",
|
||||
"removeBackground": "Eltávolítás",
|
||||
"reconnect": "Újracsatlakozás",
|
||||
"reopenTab": "Újranyitás",
|
||||
"sectionOpen": "Nyitott",
|
||||
"sectionBackground": "Háttér",
|
||||
"backgroundDesc": "A munkamenetek a leválasztás után 30 percig megmaradnak, majd újracsatlakoztathatók.",
|
||||
"persisted": "A háttérben maradt",
|
||||
"expiresIn": "Lejár {{duration}} múlva",
|
||||
"search": "Kapcsolatok keresése...",
|
||||
"noSearchResults": "Nincsenek a keresésnek megfelelő kapcsolatok"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Kapcsolódás ehhez: {{type}} munkamenet...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Újrapróbálkozás"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH SZERVERKEZELŐ",
|
||||
"tagline": "Saját üzemeltetésű SSH és távoli asztalkezelés",
|
||||
"loginTitle": "Bejelentkezés a Termixbe",
|
||||
"registerTitle": "Fiók létrehozása",
|
||||
"forgotPassword": "Elfelejtett jelszó?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Válassza ki, hogy hány panelt szeretne megjeleníteni",
|
||||
"panesTitle": "Ablaktáblák",
|
||||
"openTabsTitle": "Nyitott lapok",
|
||||
"dragTabsHint": "Húzza a füleket a fenti panelekbe",
|
||||
"dragTabsHint": "Húzza a lapokat a fenti panelekbe, vagy használja a Gyors hozzárendelés funkciót",
|
||||
"dropHere": "Dobd ide",
|
||||
"emptyPane": "Üres",
|
||||
"dashboard": "Műszerfal",
|
||||
"clearSplitScreen": "Tiszta osztott képernyő"
|
||||
"clearSplitScreen": "Tiszta osztott képernyő",
|
||||
"quickAssign": "Gyors hozzárendelés",
|
||||
"alreadyAssigned": "{{index}} panel",
|
||||
"splitTab": "Osztott lap",
|
||||
"addToSplit": "Hozzáadás Splithez",
|
||||
"removeFromSplit": "Eltávolítás a felosztásból",
|
||||
"assignToPane": "Hozzárendelés panelhez"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Kódrészlet létrehozása",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Nem sikerült törölni a mappát",
|
||||
"folderEditSuccess": "Mappa sikeresen frissítve",
|
||||
"folderEditFailed": "Nem sikerült frissíteni a mappát",
|
||||
"confirmRunMessage": "Futtassa a \"{{name}} \" programot?",
|
||||
"confirmRunButton": "Fut",
|
||||
"runSuccess": "Futtatta a következőt: \"{{name}}\" {{count}} terminál(ok)ban",
|
||||
"copySuccess": "A(z) „{{name}}” a vágólapra másolva",
|
||||
"shareTitle": "Részlet megosztása",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Jelenlegi hozzáférés",
|
||||
"shareLoadError": "Nem sikerült betölteni a megosztott adatokat",
|
||||
"loading": "Terhelés...",
|
||||
"close": "Közeli"
|
||||
"close": "Közeli",
|
||||
"reorderFailed": "Nem sikerült menteni a kódrészlet sorrendjét"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Fiók",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Téma",
|
||||
"fontSizeLabel": "Betűméret",
|
||||
"accentColorLabel": "hangsúlyos szín",
|
||||
"settingsFileManager": "Fájlkezelő",
|
||||
"fileColorCoding": "Fájlszín-kódolás",
|
||||
"fileColorCodingDesc": "Fájlok színkódolása típus szerint",
|
||||
"settingsTerminal": "Terminál",
|
||||
"commandAutocomplete": "Parancs automatikus kiegészítése",
|
||||
"commandAutocompleteDesc": "Automatikus kiegészítés megjelenítése gépelés közben",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Jelölje ki a terminál kimenetét",
|
||||
"commandPalette": "Parancspaletta",
|
||||
"commandPaletteDesc": "Billentyűparancs engedélyezése",
|
||||
"sessionPersistence": "Munkamenet-perzisztencia",
|
||||
"sessionPersistenceDesc": "Tartsa meg a munkameneteket az újracsatlakozások között",
|
||||
"reopenTabsOnLogin": "Lapok újranyitása bejelentkezéskor",
|
||||
"reopenTabsOnLoginDesc": "Állítsa vissza a megnyitott lapokat bejelentkezéskor vagy az oldal frissítésekor, akár egy másik eszközről is",
|
||||
"confirmTabClose": "Lap bezárásának megerősítése",
|
||||
"confirmTabCloseDesc": "Kérdezzen rá a terminálfülek bezárása előtt",
|
||||
"settingsSidebar": "Oldalsáv",
|
||||
"showHostTags": "Gazdagép címkék megjelenítése",
|
||||
"showHostTagsDesc": "Címkék megjelenítése a gazdagépek listájában",
|
||||
"hostTrayOnClick": "Kattintson a Gazdagép-műveletek kibontásához",
|
||||
"hostTrayOnClickDesc": "Kattintás szükséges a gazdagép műveleteinek kibontásához az egérrel való rámutatás helyett",
|
||||
"pinAppRail": "Alkalmazássín rögzítése",
|
||||
"pinAppRailDesc": "A bal oldalsáv alkalmazássávjának mindig legyen kibontva, ne pedig az egérmutatóra történő kibontás esetén",
|
||||
"settingsSnippets": "Kódrészletek",
|
||||
"foldersCollapsed": "Mappák összecsukva",
|
||||
"foldersCollapsedDesc": "Mappák összecsukása alapértelmezés szerint",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Cuplikan",
|
||||
"hostManager": "Manajer Host",
|
||||
"credentials": "Kredensial",
|
||||
"connections": "Koneksi",
|
||||
"roleAdministrator": "Administrator",
|
||||
"roleUser": "Pengguna"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Sensitivitas Gulir Cepat",
|
||||
"moshCommandLabel": "Perintah Mosh",
|
||||
"startupSnippetLabel": "Cuplikan Startup",
|
||||
"keepaliveIntervalLabel": "Interval Menjaga Ketahanan",
|
||||
"keepaliveIntervalLabel": "Interval Keepalive (detik)",
|
||||
"maxKeepaliveMisses": "Max Keepalive Gagal",
|
||||
"tunnelSettings": "Pengaturan Terowongan",
|
||||
"enableTunneling": "Aktifkan Tunneling",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "URL VNC disalin",
|
||||
"telnetUrlCopied": "URL Telnet disalin",
|
||||
"remoteDesktopUrlCopied": "URL Desktop Jarak Jauh disalin",
|
||||
"wakeOnLanAction": "Wake on LAN",
|
||||
"wakeOnLanSuccess": "Paket ajaib dikirim ke {{name}}",
|
||||
"wakeOnLanError": "Gagal mengirim paket ajaib",
|
||||
"cloneHostAction": "Kloning Host",
|
||||
"copyAddress": "Salin Alamat",
|
||||
"copyLink": "Salin Tautan",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Tidak ada host yang sesuai dengan pencarian Anda.",
|
||||
"hostNotFound": "Host tidak ditemukan",
|
||||
"searchHosts": "Cari host...",
|
||||
"sortHosts": "Urutkan Host",
|
||||
"sortDefault": "Urutan Default",
|
||||
"sortNameAsc": "Nama (A → Z)",
|
||||
"sortNameDesc": "Nama (Z → A)",
|
||||
"sortIpAsc": "Alamat IP (Naik)",
|
||||
"sortIpDesc": "Alamat IP (Deskripsi)",
|
||||
"sortOnlineFirst": "Online Pertama",
|
||||
"sortOfflineFirst": "Offline First",
|
||||
"sortPinnedFirst": "Dipin Pertama",
|
||||
"filterHosts": "Filter Host",
|
||||
"filterClearAll": "Hapus Filter",
|
||||
"filterStatusGroup": "Status",
|
||||
"filterOnline": "On line",
|
||||
"filterOffline": "Offline",
|
||||
"filterPinned": "Disematkan",
|
||||
"filterAuthGroup": "Jenis Otorisasi",
|
||||
"filterAuthPassword": "Kata sandi",
|
||||
"filterAuthKey": "Kunci SSH",
|
||||
"filterAuthCredential": "Mandat",
|
||||
"filterAuthNone": "Tidak ada",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protokol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Fitur",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Pengelola File",
|
||||
"filterFeatureTunnel": "Terowongan",
|
||||
"filterFeatureDocker": "Buruh pelabuhan",
|
||||
"filterTagsGroup": "Tag",
|
||||
"shareHost": "Bagikan Host",
|
||||
"shareHostTitle": "Bagikan: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Panel {{index}} - kosong",
|
||||
"noTabAssigned": "Tidak ada tab yang ditetapkan"
|
||||
"noTabAssigned": "Tidak ada tab yang ditetapkan",
|
||||
"focusedPane": "Panel aktif"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Tidak ada koneksi",
|
||||
"noConnectionsDesc": "Buka terminal, pengelola file, atau desktop jarak jauh untuk melihat koneksi di sini.",
|
||||
"connectedFor": "Terhubung untuk {{duration}}",
|
||||
"connected": "Terhubung",
|
||||
"disconnected": "Terputus",
|
||||
"closeTab": "Tutup tab",
|
||||
"closeConnection": "Hubungan yang erat",
|
||||
"forgetTab": "Lupa",
|
||||
"removeBackground": "Menghapus",
|
||||
"reconnect": "Terhubung kembali",
|
||||
"reopenTab": "Buka kembali",
|
||||
"sectionOpen": "Membuka",
|
||||
"sectionBackground": "Latar belakang",
|
||||
"backgroundDesc": "Sesi tetap aktif selama 30 menit setelah terputus dan dapat disambungkan kembali.",
|
||||
"persisted": "Tetap ada di latar belakang",
|
||||
"expiresIn": "Berakhir dalam {{duration}}",
|
||||
"search": "Cari koneksi...",
|
||||
"noSearchResults": "Tidak ada koneksi yang sesuai dengan pencarian Anda."
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Menghubungkan ke sesi {{type}}...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Mencoba kembali"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "MANAJER SERVER SSH",
|
||||
"tagline": "Manajemen SSH dan desktop jarak jauh yang dihosting sendiri",
|
||||
"loginTitle": "Masuk ke Termix",
|
||||
"registerTitle": "Buat Akun",
|
||||
"forgotPassword": "Lupa kata sandi?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Pilih berapa banyak panel yang ingin ditampilkan.",
|
||||
"panesTitle": "Kaca",
|
||||
"openTabsTitle": "Buka Tab",
|
||||
"dragTabsHint": "Seret tab ke dalam panel di atas.",
|
||||
"dragTabsHint": "Seret tab ke panel di atas, atau gunakan Penugasan Cepat.",
|
||||
"dropHere": "Jatuhkan di sini",
|
||||
"emptyPane": "Kosong",
|
||||
"dashboard": "Dasbor",
|
||||
"clearSplitScreen": "Hapus Layar Terpisah"
|
||||
"clearSplitScreen": "Hapus Layar Terpisah",
|
||||
"quickAssign": "Penugasan Cepat",
|
||||
"alreadyAssigned": "Panel {{index}}",
|
||||
"splitTab": "Tab Terpisah",
|
||||
"addToSplit": "Tambahkan ke Split",
|
||||
"removeFromSplit": "Hapus dari Split",
|
||||
"assignToPane": "Tetapkan ke panel"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Buat Cuplikan",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Gagal menghapus folder",
|
||||
"folderEditSuccess": "Folder berhasil diperbarui.",
|
||||
"folderEditFailed": "Gagal memperbarui folder",
|
||||
"confirmRunMessage": "Jalankan \"{{name}}\"?",
|
||||
"confirmRunButton": "Berlari",
|
||||
"runSuccess": "Menjalankan \"{{name}}\" di {{count}} terminal",
|
||||
"copySuccess": "\"{{name}}\" disalin ke papan klip",
|
||||
"shareTitle": "Bagikan Cuplikan",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Akses Saat Ini",
|
||||
"shareLoadError": "Gagal memuat data berbagi.",
|
||||
"loading": "Memuat...",
|
||||
"close": "Menutup"
|
||||
"close": "Menutup",
|
||||
"reorderFailed": "Gagal menyimpan urutan cuplikan"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Akun",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Tema",
|
||||
"fontSizeLabel": "Ukuran Huruf",
|
||||
"accentColorLabel": "Warna Aksen",
|
||||
"settingsFileManager": "Pengelola File",
|
||||
"fileColorCoding": "Pengkodean Warna File",
|
||||
"fileColorCodingDesc": "Beri kode warna pada file berdasarkan jenisnya.",
|
||||
"settingsTerminal": "Terminal",
|
||||
"commandAutocomplete": "Pelengkapan Otomatis Perintah",
|
||||
"commandAutocompleteDesc": "Tampilkan fitur pelengkapan otomatis saat mengetik.",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Sorot keluaran terminal",
|
||||
"commandPalette": "Palet Perintah",
|
||||
"commandPaletteDesc": "Aktifkan pintasan keyboard",
|
||||
"sessionPersistence": "Ketahanan Sesi",
|
||||
"sessionPersistenceDesc": "Pertahankan sesi di antara penyambungan kembali.",
|
||||
"reopenTabsOnLogin": "Buka Kembali Tab saat Masuk",
|
||||
"reopenTabsOnLoginDesc": "Pulihkan tab yang terbuka saat Anda masuk atau menyegarkan halaman, bahkan dari perangkat lain.",
|
||||
"confirmTabClose": "Konfirmasi Penutupan Tab",
|
||||
"confirmTabCloseDesc": "Tanyakan terlebih dahulu sebelum menutup tab terminal.",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Tag Pembawa Acara",
|
||||
"showHostTagsDesc": "Tampilkan tag di daftar host",
|
||||
"hostTrayOnClick": "Klik untuk Memperluas Tindakan Host",
|
||||
"hostTrayOnClickDesc": "Perlu diklik untuk memperluas tindakan host, bukan dengan mengarahkan kursor.",
|
||||
"pinAppRail": "Pin App Rail",
|
||||
"pinAppRailDesc": "Pertahankan agar bilah sisi kiri aplikasi selalu terbuka, alih-alih terbuka saat diarahkan kursor.",
|
||||
"settingsSnippets": "Cuplikan",
|
||||
"foldersCollapsed": "Folder Dilipat",
|
||||
"foldersCollapsedDesc": "Secara default, folder akan diciutkan.",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Snippet",
|
||||
"hostManager": "Gestore Host",
|
||||
"credentials": "Credenziali",
|
||||
"connections": "Connessioni",
|
||||
"roleAdministrator": "Amministratore",
|
||||
"roleUser": "Utente"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Sensibilità Di Scorrimento Veloce",
|
||||
"moshCommandLabel": "Comando Mosh",
|
||||
"startupSnippetLabel": "Snippet Di Avvio",
|
||||
"keepaliveIntervalLabel": "Intervallo Di Mantenimento",
|
||||
"keepaliveIntervalLabel": "Intervallo di keep-alive (secondi)",
|
||||
"maxKeepaliveMisses": "Max Misses Keepalive",
|
||||
"tunnelSettings": "Impostazioni Tunnel",
|
||||
"enableTunneling": "Abilita Tunneling",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "URL VNC copiato",
|
||||
"telnetUrlCopied": "URL Telnet copiato",
|
||||
"remoteDesktopUrlCopied": "URL Desktop remoto copiato",
|
||||
"wakeOnLanAction": "Wake on LAN",
|
||||
"wakeOnLanSuccess": "Pacchetto magico inviato a {{name}}",
|
||||
"wakeOnLanError": "Invio del pacchetto magico non riuscito",
|
||||
"cloneHostAction": "Clona Host",
|
||||
"copyAddress": "Copia Indirizzo",
|
||||
"copyLink": "Copia Collegamento",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Nessun host corrisponde alla tua ricerca",
|
||||
"hostNotFound": "Host non trovato",
|
||||
"searchHosts": "Cerca host...",
|
||||
"sortHosts": "Ordina gli host",
|
||||
"sortDefault": "Ordine predefinito",
|
||||
"sortNameAsc": "Nome (A → Z)",
|
||||
"sortNameDesc": "Nome (Z → A)",
|
||||
"sortIpAsc": "Indirizzo IP (ascendente)",
|
||||
"sortIpDesc": "Indirizzo IP (Descrizione)",
|
||||
"sortOnlineFirst": "Online First",
|
||||
"sortOfflineFirst": "Prima offline",
|
||||
"sortPinnedFirst": "Fissato per primo",
|
||||
"filterHosts": "Host di filtro",
|
||||
"filterClearAll": "Filtri trasparenti",
|
||||
"filterStatusGroup": "Stato",
|
||||
"filterOnline": "Online",
|
||||
"filterOffline": "Offline",
|
||||
"filterPinned": "Fissato",
|
||||
"filterAuthGroup": "Tipo di autorizzazione",
|
||||
"filterAuthPassword": "Password",
|
||||
"filterAuthKey": "Chiave SSH",
|
||||
"filterAuthCredential": "Credenziali",
|
||||
"filterAuthNone": "Nessuno",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protocollo",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Internet",
|
||||
"filterFeaturesGroup": "Caratteristiche",
|
||||
"filterFeatureTerminal": "terminale",
|
||||
"filterFeatureFileManager": "Gestore file",
|
||||
"filterFeatureTunnel": "Tunnel",
|
||||
"filterFeatureDocker": "Docker",
|
||||
"filterTagsGroup": "Etichette",
|
||||
"shareHost": "Condividi Host",
|
||||
"shareHostTitle": "Condividi: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Nessuna scheda assegnata"
|
||||
"noTabAssigned": "Nessuna scheda assegnata",
|
||||
"focusedPane": "Riquadro attivo"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Nessuna connessione",
|
||||
"noConnectionsDesc": "Apri un terminale, un gestore di file o un desktop remoto per visualizzare le connessioni qui",
|
||||
"connectedFor": "Connesso per {{duration}}",
|
||||
"connected": "Collegato",
|
||||
"disconnected": "Disconnesso",
|
||||
"closeTab": "Chiudi la scheda",
|
||||
"closeConnection": "Stretto legame",
|
||||
"forgetTab": "Dimenticare",
|
||||
"removeBackground": "Rimuovere",
|
||||
"reconnect": "Ricollega",
|
||||
"reopenTab": "Riaprire",
|
||||
"sectionOpen": "Aprire",
|
||||
"sectionBackground": "Sfondo",
|
||||
"backgroundDesc": "Le sessioni rimangono attive per 30 minuti dopo la disconnessione e possono essere ristabilita.",
|
||||
"persisted": "Persisteva in sottofondo",
|
||||
"expiresIn": "Scade tra {{duration}}",
|
||||
"search": "Cerca connessioni...",
|
||||
"noSearchResults": "Nessun risultato corrispondente alla ricerca"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Connessione alla sessione {{type}}...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Riprova"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH SERVER MANAGER",
|
||||
"tagline": "Gestione SSH e desktop remoto self-hosted",
|
||||
"loginTitle": "Accedi a Termix",
|
||||
"registerTitle": "Crea Account",
|
||||
"forgotPassword": "Password Dimenticata?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Scegli quanti riquadri visualizzare",
|
||||
"panesTitle": "Riquadri",
|
||||
"openTabsTitle": "Apri Schede",
|
||||
"dragTabsHint": "Trascina le schede nei riquadri sopra",
|
||||
"dragTabsHint": "Trascina le schede nei riquadri sopra oppure usa Assegnazione rapida",
|
||||
"dropHere": "Rilascia qui",
|
||||
"emptyPane": "Vuoto",
|
||||
"dashboard": "Dashboard",
|
||||
"clearSplitScreen": "Cancella Schermo Diviso"
|
||||
"clearSplitScreen": "Cancella Schermo Diviso",
|
||||
"quickAssign": "Assegnazione rapida",
|
||||
"alreadyAssigned": "Pane {{index}}",
|
||||
"splitTab": "Scheda divisa",
|
||||
"addToSplit": "Aggiungi a Divide",
|
||||
"removeFromSplit": "Rimuovi da Split",
|
||||
"assignToPane": "Assegna al riquadro"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Crea Snippet",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Impossibile eliminare la cartella",
|
||||
"folderEditSuccess": "Cartella aggiornata con successo",
|
||||
"folderEditFailed": "Impossibile aggiornare la cartella",
|
||||
"confirmRunMessage": "Eseguire \"{{name}}\"?",
|
||||
"confirmRunButton": "Correre",
|
||||
"runSuccess": "Ran \"{{name}}\" in {{count}} terminale(i)",
|
||||
"copySuccess": "Copiato \"{{name}}\" negli appunti",
|
||||
"shareTitle": "Condividi Snippet",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Accesso Corrente",
|
||||
"shareLoadError": "Impossibile caricare i dati di condivisione",
|
||||
"loading": "Caricamento...",
|
||||
"close": "Chiudi"
|
||||
"close": "Chiudi",
|
||||
"reorderFailed": "Impossibile salvare l'ordine degli snippet"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Account",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Tema",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Colore Accento",
|
||||
"settingsFileManager": "Gestore File",
|
||||
"fileColorCoding": "Codifica Colore File",
|
||||
"fileColorCodingDesc": "File di codice colore per tipo",
|
||||
"settingsTerminal": "Terminale",
|
||||
"commandAutocomplete": "Completamento Automatico Dei Comandi",
|
||||
"commandAutocompleteDesc": "Mostra completamento automatico durante la digitazione",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Evidenzia uscita terminale",
|
||||
"commandPalette": "Tavolozza Comandi",
|
||||
"commandPaletteDesc": "Abilita scorciatoia da tastiera",
|
||||
"sessionPersistence": "Persistenza Sessione",
|
||||
"sessionPersistenceDesc": "Mantieni le sessioni tra le riconnessioni",
|
||||
"reopenTabsOnLogin": "Riapri le schede all'accesso",
|
||||
"reopenTabsOnLoginDesc": "Ripristina le schede aperte quando accedi o aggiorni la pagina, anche da un altro dispositivo.",
|
||||
"confirmTabClose": "Conferma Chiusura Scheda",
|
||||
"confirmTabCloseDesc": "Chiedi prima di chiudere le schede del terminale",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Mostra Tag Host",
|
||||
"showHostTagsDesc": "Mostra i tag nella lista host",
|
||||
"hostTrayOnClick": "Fai clic per espandere le azioni dell'host",
|
||||
"hostTrayOnClickDesc": "Richiedi un clic per espandere le azioni dell'host invece del passaggio del mouse",
|
||||
"pinAppRail": "Pin App Rail",
|
||||
"pinAppRailDesc": "Mantieni la barra laterale sinistra sempre espansa, anziché espanderla al passaggio del mouse.",
|
||||
"settingsSnippets": "Snippet",
|
||||
"foldersCollapsed": "Cartelle Compensate",
|
||||
"foldersCollapsedDesc": "Comprimi le cartelle come predefinite",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Snippets",
|
||||
"hostManager": "ホスト マネージャー",
|
||||
"credentials": "資格情報",
|
||||
"connections": "つながり",
|
||||
"roleAdministrator": "管理者",
|
||||
"roleUser": "ユーザー"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "高速スクロール感度",
|
||||
"moshCommandLabel": "Mosh Command",
|
||||
"startupSnippetLabel": "スタートアップスニペットを開く",
|
||||
"keepaliveIntervalLabel": "保持間隔",
|
||||
"keepaliveIntervalLabel": "キープアライブ間隔(秒)",
|
||||
"maxKeepaliveMisses": "最大Keep生存ミス数",
|
||||
"tunnelSettings": "トンネルの設定",
|
||||
"enableTunneling": "トンネリングを有効にする",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URLをコピーしました",
|
||||
"telnetUrlCopied": "Telnet URL をコピーしました",
|
||||
"remoteDesktopUrlCopied": "リモートデスクトップの URL をコピーしました",
|
||||
"wakeOnLanAction": "Wake on LAN",
|
||||
"wakeOnLanSuccess": "{{name}}宛てにマジックパケットが送信されました。",
|
||||
"wakeOnLanError": "マジックパケットの送信に失敗しました",
|
||||
"cloneHostAction": "ホストを複製",
|
||||
"copyAddress": "アドレスをコピー",
|
||||
"copyLink": "リンクをコピー",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "検索条件に一致するホストがありません",
|
||||
"hostNotFound": "ホストが見つかりません",
|
||||
"searchHosts": "ホストを検索...",
|
||||
"sortHosts": "ホストをソートする",
|
||||
"sortDefault": "デフォルトオーダー",
|
||||
"sortNameAsc": "名前(A~Z)",
|
||||
"sortNameDesc": "名前(Z → A)",
|
||||
"sortIpAsc": "IPアドレス(ASC)",
|
||||
"sortIpDesc": "IPアドレス(説明)",
|
||||
"sortOnlineFirst": "オンラインファースト",
|
||||
"sortOfflineFirst": "オフライン優先",
|
||||
"sortPinnedFirst": "ピン留めされた最初の",
|
||||
"filterHosts": "ホストをフィルタリング",
|
||||
"filterClearAll": "フィルターをクリア",
|
||||
"filterStatusGroup": "状態",
|
||||
"filterOnline": "オンライン",
|
||||
"filterOffline": "オフライン",
|
||||
"filterPinned": "ピン留め",
|
||||
"filterAuthGroup": "認証タイプ",
|
||||
"filterAuthPassword": "パスワード",
|
||||
"filterAuthKey": "SSHキー",
|
||||
"filterAuthCredential": "資格証明書",
|
||||
"filterAuthNone": "なし",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "プロトコル",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "特徴",
|
||||
"filterFeatureTerminal": "ターミナル",
|
||||
"filterFeatureFileManager": "ファイルマネージャー",
|
||||
"filterFeatureTunnel": "トンネル",
|
||||
"filterFeatureDocker": "ドッカー",
|
||||
"filterTagsGroup": "タグ",
|
||||
"shareHost": "ホストを共有",
|
||||
"shareHostTitle": "シェア: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "タブが割り当てられていません"
|
||||
"noTabAssigned": "タブが割り当てられていません",
|
||||
"focusedPane": "アクティブペイン"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "接続なし",
|
||||
"noConnectionsDesc": "ターミナル、ファイルマネージャ、またはリモートデスクトップを開いて、ここに接続状況を確認してください。",
|
||||
"connectedFor": "{{duration}} まで接続しました",
|
||||
"connected": "接続済み",
|
||||
"disconnected": "接続が切断されました",
|
||||
"closeTab": "タブを閉じる",
|
||||
"closeConnection": "密接なつながり",
|
||||
"forgetTab": "忘れる",
|
||||
"removeBackground": "取り除く",
|
||||
"reconnect": "再接続",
|
||||
"reopenTab": "再開",
|
||||
"sectionOpen": "開ける",
|
||||
"sectionBackground": "背景",
|
||||
"backgroundDesc": "セッションは切断後30分間維持され、再接続が可能です。",
|
||||
"persisted": "バックグラウンドで継続",
|
||||
"expiresIn": "有効期限は {{duration}}です",
|
||||
"search": "接続を検索...",
|
||||
"noSearchResults": "検索条件に一致する接続はありません"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "{{type}} セッションに接続中...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "再試行する"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSHサーバー管理",
|
||||
"tagline": "セルフホスト型のSSHおよびリモートデスクトップ管理",
|
||||
"loginTitle": "Termixにログイン",
|
||||
"registerTitle": "アカウントを作成",
|
||||
"forgotPassword": "パスワードをお忘れですか?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "表示するペインの数を選択",
|
||||
"panesTitle": "ペイン",
|
||||
"openTabsTitle": "タブを開く",
|
||||
"dragTabsHint": "上のペインにタブをドラッグ",
|
||||
"dragTabsHint": "タブを上のペインにドラッグするか、クイック割り当てを使用してください。",
|
||||
"dropHere": "ここにドロップ",
|
||||
"emptyPane": "なし",
|
||||
"dashboard": "ダッシュボード",
|
||||
"clearSplitScreen": "分割画面をクリア"
|
||||
"clearSplitScreen": "分割画面をクリア",
|
||||
"quickAssign": "クイック割り当て",
|
||||
"alreadyAssigned": "ペイン {{index}}",
|
||||
"splitTab": "分割タブ",
|
||||
"addToSplit": "分割に追加",
|
||||
"removeFromSplit": "分割から削除",
|
||||
"assignToPane": "ペインに割り当てる"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "スニペットを作成",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "フォルダの削除に失敗しました",
|
||||
"folderEditSuccess": "フォルダが正常に更新されました",
|
||||
"folderEditFailed": "フォルダの更新に失敗しました",
|
||||
"confirmRunMessage": "「{{name}}」を実行しますか?",
|
||||
"confirmRunButton": "走る",
|
||||
"runSuccess": "{{name}}ターミナルで \" {{count}} \" を実行しました",
|
||||
"copySuccess": "\"{{name}}\"をクリップボードにコピーしました",
|
||||
"shareTitle": "スニペットを共有",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "現在のアクセス",
|
||||
"shareLoadError": "共有データの読み込みに失敗しました",
|
||||
"loading": "読み込み中...",
|
||||
"close": "閉じる"
|
||||
"close": "閉じる",
|
||||
"reorderFailed": "スニペットの順序を保存できませんでした"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "アカウント",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "テーマ",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "アクセントカラー",
|
||||
"settingsFileManager": "ファイルマネージャー",
|
||||
"fileColorCoding": "ファイルカラーコーディング",
|
||||
"fileColorCodingDesc": "タイプ別のカラーコードファイル",
|
||||
"settingsTerminal": "ターミナル",
|
||||
"commandAutocomplete": "コマンドオートコンプリート",
|
||||
"commandAutocompleteDesc": "入力中にオートコンプリートを表示",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "端末の出力を強調表示する",
|
||||
"commandPalette": "コマンドパレット",
|
||||
"commandPaletteDesc": "キーボードショートカットを有効にする",
|
||||
"sessionPersistence": "セッションの永続性",
|
||||
"sessionPersistenceDesc": "再接続の間のセッションを保持",
|
||||
"reopenTabsOnLogin": "ログイン時にタブを再度開く",
|
||||
"reopenTabsOnLoginDesc": "別のデバイスからログインしたりページを更新したりしても、開いているタブを復元します。",
|
||||
"confirmTabClose": "タブを閉じる",
|
||||
"confirmTabCloseDesc": "ターミナルタブを閉じる前に確認する",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "ホストタグの表示",
|
||||
"showHostTagsDesc": "ホストリストにタグを表示する",
|
||||
"hostTrayOnClick": "ホストアクションを展開するにはクリックしてください",
|
||||
"hostTrayOnClickDesc": "ホバーではなくクリックでホストアクションを展開するようにする",
|
||||
"pinAppRail": "Pin App Rail",
|
||||
"pinAppRailDesc": "左サイドバーのアプリレールを、マウスオーバー時ではなく常に展開した状態にする。",
|
||||
"settingsSnippets": "Snippets",
|
||||
"foldersCollapsed": "折りたたまれたフォルダ",
|
||||
"foldersCollapsedDesc": "デフォルトでフォルダを閉じる",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "스니펫",
|
||||
"hostManager": "호스트 관리자",
|
||||
"credentials": "자격 증명",
|
||||
"connections": "사이",
|
||||
"roleAdministrator": "관리자",
|
||||
"roleUser": "사용자"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "빠른 스크롤 감도",
|
||||
"moshCommandLabel": "모쉬 커맨드",
|
||||
"startupSnippetLabel": "스타트업 스니펫",
|
||||
"keepaliveIntervalLabel": "킵얼라이브 인터벌",
|
||||
"keepaliveIntervalLabel": "유지 간격(초)",
|
||||
"maxKeepaliveMisses": "맥스 킵얼라이브 미스",
|
||||
"tunnelSettings": "터널 설정",
|
||||
"enableTunneling": "터널링 활성화",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL이 복사되었습니다.",
|
||||
"telnetUrlCopied": "텔넷 URL이 복사되었습니다.",
|
||||
"remoteDesktopUrlCopied": "원격 데스크톱 URL이 복사되었습니다.",
|
||||
"wakeOnLanAction": "LAN에서 웨이크업",
|
||||
"wakeOnLanSuccess": "매직 패킷이 {{name}}로 전송되었습니다.",
|
||||
"wakeOnLanError": "매직 패킷 전송에 실패했습니다.",
|
||||
"cloneHostAction": "클론 호스트",
|
||||
"copyAddress": "주소 복사",
|
||||
"copyLink": "링크 복사",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "검색 조건에 맞는 호스트가 없습니다.",
|
||||
"hostNotFound": "호스트를 찾을 수 없습니다",
|
||||
"searchHosts": "호스트를 검색하세요...",
|
||||
"sortHosts": "호스트 정렬",
|
||||
"sortDefault": "기본 주문",
|
||||
"sortNameAsc": "이름 (A → Z)",
|
||||
"sortNameDesc": "이름 (Z → A)",
|
||||
"sortIpAsc": "IP 주소(오름차순)",
|
||||
"sortIpDesc": "IP 주소 (설명)",
|
||||
"sortOnlineFirst": "온라인 우선",
|
||||
"sortOfflineFirst": "오프라인 우선",
|
||||
"sortPinnedFirst": "첫 번째 고정됨",
|
||||
"filterHosts": "필터 호스트",
|
||||
"filterClearAll": "필터 지우기",
|
||||
"filterStatusGroup": "상태",
|
||||
"filterOnline": "온라인",
|
||||
"filterOffline": "오프라인",
|
||||
"filterPinned": "고정됨",
|
||||
"filterAuthGroup": "인증 유형",
|
||||
"filterAuthPassword": "비밀번호",
|
||||
"filterAuthKey": "SSH 키",
|
||||
"filterAuthCredential": "신임장",
|
||||
"filterAuthNone": "없음",
|
||||
"filterAuthOpkssh": "오프크시",
|
||||
"filterProtocolGroup": "규약",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "텔넷",
|
||||
"filterFeaturesGroup": "특징",
|
||||
"filterFeatureTerminal": "단말기",
|
||||
"filterFeatureFileManager": "파일 관리자",
|
||||
"filterFeatureTunnel": "터널",
|
||||
"filterFeatureDocker": "도커",
|
||||
"filterTagsGroup": "태그",
|
||||
"shareHost": "호스트 공유",
|
||||
"shareHostTitle": "공유: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "패널 {{index}} - 비어 있음",
|
||||
"noTabAssigned": "탭이 지정되지 않았습니다."
|
||||
"noTabAssigned": "탭이 지정되지 않았습니다.",
|
||||
"focusedPane": "활성 창"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "연결 없음",
|
||||
"noConnectionsDesc": "터미널, 파일 관리자 또는 원격 데스크톱을 열어 연결 상태를 확인하세요.",
|
||||
"connectedFor": "{{duration}}에 연결되었습니다.",
|
||||
"connected": "연결됨",
|
||||
"disconnected": "연결이 끊어졌습니다",
|
||||
"closeTab": "닫기 탭",
|
||||
"closeConnection": "닫기",
|
||||
"forgetTab": "잊다",
|
||||
"removeBackground": "제거하다",
|
||||
"reconnect": "다시 연결",
|
||||
"reopenTab": "다시 열다",
|
||||
"sectionOpen": "열려 있는",
|
||||
"sectionBackground": "배경",
|
||||
"backgroundDesc": "연결이 끊어진 후에도 세션은 30분 동안 유지되며 다시 연결할 수 있습니다.",
|
||||
"persisted": "백그라운드에서 지속됨",
|
||||
"expiresIn": "{{duration}} 후에 만료됩니다.",
|
||||
"search": "검색 연결...",
|
||||
"noSearchResults": "검색 조건과 일치하는 결과가 없습니다."
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "{{type}} 세션에 연결 중...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "다시 해 보다"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH 서버 관리자",
|
||||
"tagline": "자체 호스팅 SSH 및 원격 데스크톱 관리",
|
||||
"loginTitle": "Termix에 로그인하세요",
|
||||
"registerTitle": "계정 생성",
|
||||
"forgotPassword": "비밀번호를 잊으셨나요?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "표시할 창의 개수를 선택하세요",
|
||||
"panesTitle": "판",
|
||||
"openTabsTitle": "열려있는 탭",
|
||||
"dragTabsHint": "탭을 위쪽 창으로 드래그하세요",
|
||||
"dragTabsHint": "탭을 위쪽 창으로 드래그하거나 빠른 할당 기능을 사용하세요.",
|
||||
"dropHere": "여기에 내려놓으세요",
|
||||
"emptyPane": "비어 있는",
|
||||
"dashboard": "계기반",
|
||||
"clearSplitScreen": "선명한 분할 화면"
|
||||
"clearSplitScreen": "선명한 분할 화면",
|
||||
"quickAssign": "빠른 배정",
|
||||
"alreadyAssigned": "패널 {{index}}",
|
||||
"splitTab": "분할 탭",
|
||||
"addToSplit": "분할에 추가",
|
||||
"removeFromSplit": "분할에서 제거",
|
||||
"assignToPane": "창에 할당"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "코드 조각 생성",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "폴더 삭제에 실패했습니다",
|
||||
"folderEditSuccess": "폴더가 성공적으로 업데이트되었습니다.",
|
||||
"folderEditFailed": "폴더 업데이트에 실패했습니다",
|
||||
"confirmRunMessage": "\"{{name}} \"를 실행하시겠습니까?",
|
||||
"confirmRunButton": "달리다",
|
||||
"runSuccess": "{{name}}\"을 {{count}} 터미널에서 실행했습니다.",
|
||||
"copySuccess": "\"{{name}}\"를 클립보드에 복사했습니다.",
|
||||
"shareTitle": "스니펫 공유",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "현재 액세스",
|
||||
"shareLoadError": "공유 데이터 불러오기에 실패했습니다",
|
||||
"loading": "로딩 중...",
|
||||
"close": "닫다"
|
||||
"close": "닫다",
|
||||
"reorderFailed": "스니펫 순서를 저장하는 데 실패했습니다."
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "계정",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "주제",
|
||||
"fontSizeLabel": "글꼴 크기",
|
||||
"accentColorLabel": "포인트 색상",
|
||||
"settingsFileManager": "파일 관리자",
|
||||
"fileColorCoding": "파일 색상 코드",
|
||||
"fileColorCodingDesc": "파일 유형별로 색상 코드를 지정하세요",
|
||||
"settingsTerminal": "단말기",
|
||||
"commandAutocomplete": "명령어 자동 완성",
|
||||
"commandAutocompleteDesc": "입력하는 동안 자동 완성 기능을 표시합니다.",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "터미널 출력 강조 표시",
|
||||
"commandPalette": "커맨드 팔레트",
|
||||
"commandPaletteDesc": "키보드 단축키 활성화",
|
||||
"sessionPersistence": "세션 지속성",
|
||||
"sessionPersistenceDesc": "재연결 사이에 세션을 유지하세요.",
|
||||
"reopenTabsOnLogin": "로그인 시 탭 다시 열기",
|
||||
"reopenTabsOnLoginDesc": "로그인하거나 페이지를 새로 고칠 때, 다른 기기에서 접속하더라도 열려 있던 탭을 복원할 수 있습니다.",
|
||||
"confirmTabClose": "확인 탭 닫기",
|
||||
"confirmTabCloseDesc": "터미널 탭을 닫기 전에 먼저 확인하세요.",
|
||||
"settingsSidebar": "사이드바",
|
||||
"showHostTags": "호스트 태그 표시",
|
||||
"showHostTagsDesc": "호스트 목록에 태그를 표시합니다.",
|
||||
"hostTrayOnClick": "호스트 작업 확장하려면 클릭하세요",
|
||||
"hostTrayOnClickDesc": "호스트 작업 영역을 확장하려면 마우스 오버 대신 클릭을 요구합니다.",
|
||||
"pinAppRail": "핀 앱 레일",
|
||||
"pinAppRailDesc": "왼쪽 사이드바 앱 레일을 마우스 오버 시 확장되는 대신 항상 확장된 상태로 유지합니다.",
|
||||
"settingsSnippets": "짧은 발췌",
|
||||
"foldersCollapsed": "폴더 접힘",
|
||||
"foldersCollapsedDesc": "기본적으로 폴더를 접습니다.",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Tekstbouwstenen",
|
||||
"hostManager": "Host Manager",
|
||||
"credentials": "Aanmeldgegevens",
|
||||
"connections": "Verbindingen",
|
||||
"roleAdministrator": "Beheerder",
|
||||
"roleUser": "Gebruiker"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Snelle Boekgevoeligheid",
|
||||
"moshCommandLabel": "Mosh commando",
|
||||
"startupSnippetLabel": "Opstarten snippet",
|
||||
"keepaliveIntervalLabel": "Keepalive interval",
|
||||
"keepaliveIntervalLabel": "Keepalive-interval (seconden)",
|
||||
"maxKeepaliveMisses": "Max Keepalive Gemist",
|
||||
"tunnelSettings": "Tunnel instellingen",
|
||||
"enableTunneling": "Tunneling inschakelen",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL gekopieerd",
|
||||
"telnetUrlCopied": "Telnet URL gekopieerd",
|
||||
"remoteDesktopUrlCopied": "URL op extern bureaublad gekopieerd",
|
||||
"wakeOnLanAction": "Wake on LAN",
|
||||
"wakeOnLanSuccess": "Magisch pakket verzonden naar {{name}}",
|
||||
"wakeOnLanError": "Het verzenden van het magic packet is mislukt.",
|
||||
"cloneHostAction": "Host klonen",
|
||||
"copyAddress": "Adres kopiëren",
|
||||
"copyLink": "Koppeling kopiëren",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Er zijn geen hosts die voldoen aan jouw zoekopdracht",
|
||||
"hostNotFound": "Host niet gevonden",
|
||||
"searchHosts": "Zoek hosts...",
|
||||
"sortHosts": "Sorteer hosts",
|
||||
"sortDefault": "Standaardbestelling",
|
||||
"sortNameAsc": "Naam (A → Z)",
|
||||
"sortNameDesc": "Naam (Z → A)",
|
||||
"sortIpAsc": "IP-adres (oplopend)",
|
||||
"sortIpDesc": "IP-adres (Beschrijving)",
|
||||
"sortOnlineFirst": "Eerst online",
|
||||
"sortOfflineFirst": "Eerst offline",
|
||||
"sortPinnedFirst": "Eerst vastgepind",
|
||||
"filterHosts": "Filter hosts",
|
||||
"filterClearAll": "Filters wissen",
|
||||
"filterStatusGroup": "Status",
|
||||
"filterOnline": "Online",
|
||||
"filterOffline": "Offline",
|
||||
"filterPinned": "Vastgepind",
|
||||
"filterAuthGroup": "Auth-type",
|
||||
"filterAuthPassword": "Wachtwoord",
|
||||
"filterAuthKey": "SSH-sleutel",
|
||||
"filterAuthCredential": "Referentie",
|
||||
"filterAuthNone": "Geen",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protocol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Functies",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Bestandsbeheerder",
|
||||
"filterFeatureTunnel": "Tunnel",
|
||||
"filterFeatureDocker": "Docker",
|
||||
"filterTagsGroup": "Tags",
|
||||
"shareHost": "Deel host",
|
||||
"shareHostTitle": "Delen: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Geen tabblad toegewezen"
|
||||
"noTabAssigned": "Geen tabblad toegewezen",
|
||||
"focusedPane": "Actief venster"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Geen verbindingen",
|
||||
"noConnectionsDesc": "Open een terminal, bestandsbeheerder of extern bureaublad om de verbindingen hier te bekijken.",
|
||||
"connectedFor": "Verbonden voor {{duration}}",
|
||||
"connected": "Aangesloten",
|
||||
"disconnected": "Verbinding verbroken",
|
||||
"closeTab": "Tab sluiten",
|
||||
"closeConnection": "Nauwe verbinding",
|
||||
"forgetTab": "Vergeten",
|
||||
"removeBackground": "Verwijderen",
|
||||
"reconnect": "Opnieuw verbinding maken",
|
||||
"reopenTab": "Heropenen",
|
||||
"sectionOpen": "Open",
|
||||
"sectionBackground": "Achtergrond",
|
||||
"backgroundDesc": "Sessies blijven 30 minuten na de onderbreking actief en kunnen daarna opnieuw worden verbonden.",
|
||||
"persisted": "Bleef op de achtergrond aanwezig",
|
||||
"expiresIn": "Verloopt over {{duration}}",
|
||||
"search": "Zoek naar verbanden...",
|
||||
"noSearchResults": "Er zijn geen resultaten gevonden die overeenkomen met uw zoekopdracht."
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Verbinden met {{type}} sessie...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Opnieuw"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH SERVER MANAGER",
|
||||
"tagline": "Zelf gehoste SSH- en beheerservices voor externe bureaubladen",
|
||||
"loginTitle": "Inloggen op Termix",
|
||||
"registerTitle": "Account Aanmaken",
|
||||
"forgotPassword": "Wachtwoord Vergeten?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Kies hoeveel panelen getoond moeten worden",
|
||||
"panesTitle": "Vensters",
|
||||
"openTabsTitle": "Tabbladen openen",
|
||||
"dragTabsHint": "Sleep tabbladen in panelen hierboven",
|
||||
"dragTabsHint": "Sleep tabbladen naar de vensters hierboven, of gebruik Snel toewijzen",
|
||||
"dropHere": "Hier neerzetten",
|
||||
"emptyPane": "Leeg",
|
||||
"dashboard": "Dashboard",
|
||||
"clearSplitScreen": "Wis Split Scherm"
|
||||
"clearSplitScreen": "Wis Split Scherm",
|
||||
"quickAssign": "Snel toewijzen",
|
||||
"alreadyAssigned": "Paneel {{index}}",
|
||||
"splitTab": "Gesplitst tabblad",
|
||||
"addToSplit": "Toevoegen aan splitsen",
|
||||
"removeFromSplit": "Verwijderen uit Split",
|
||||
"assignToPane": "Toewijzen aan paneel"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Snippet aanmaken",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Map verwijderen mislukt",
|
||||
"folderEditSuccess": "Map succesvol bijgewerkt",
|
||||
"folderEditFailed": "Bijwerken map mislukt",
|
||||
"confirmRunMessage": "Run \"{{name}}\"?",
|
||||
"confirmRunButton": "Loop",
|
||||
"runSuccess": "Ran \"{{name}}\" in {{count}} terminal(s)",
|
||||
"copySuccess": "Gekopieerd \"{{name}}\" naar klembord",
|
||||
"shareTitle": "Deel snippet",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Huidige Toegang",
|
||||
"shareLoadError": "Kan deelgegevens niet laden",
|
||||
"loading": "Laden...",
|
||||
"close": "Afsluiten"
|
||||
"close": "Afsluiten",
|
||||
"reorderFailed": "Het opslaan van de volgorde van de codefragmenten is mislukt."
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Rekening",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Thema",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Accent kleur",
|
||||
"settingsFileManager": "Bestands Beheer",
|
||||
"fileColorCoding": "Bestandkleur codering",
|
||||
"fileColorCodingDesc": "Kleur-code bestanden per type",
|
||||
"settingsTerminal": "Terminal",
|
||||
"commandAutocomplete": "Commando automatisch aanvullen",
|
||||
"commandAutocompleteDesc": "Automatisch aanvullen weergeven tijdens typen",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Terminal uitvoer markeren",
|
||||
"commandPalette": "Command palet",
|
||||
"commandPaletteDesc": "Sneltoets inschakelen",
|
||||
"sessionPersistence": "Vasthouden sessie",
|
||||
"sessionPersistenceDesc": "Sessies tussen opnieuw verbinden bewaren",
|
||||
"reopenTabsOnLogin": "Tabbladen opnieuw openen bij inloggen",
|
||||
"reopenTabsOnLoginDesc": "Herstel je geopende tabbladen wanneer je inlogt of de pagina vernieuwt, zelfs vanaf een ander apparaat.",
|
||||
"confirmTabClose": "Tabblad sluiten bevestigen",
|
||||
"confirmTabCloseDesc": "Vragen voordat terminal tabbladen worden gesloten",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Toon Host Tags",
|
||||
"showHostTagsDesc": "Toon tags in hostlijst",
|
||||
"hostTrayOnClick": "Klik om de hostacties uit te breiden",
|
||||
"hostTrayOnClickDesc": "Vereis een klik om hostacties uit te vouwen in plaats van hoveren.",
|
||||
"pinAppRail": "Pin App Rail",
|
||||
"pinAppRailDesc": "Houd de app-rail aan de linkerkant altijd uitgevouwen in plaats van deze uit te vouwen wanneer je er met de muis overheen beweegt.",
|
||||
"settingsSnippets": "Tekstbouwstenen",
|
||||
"foldersCollapsed": "Mappen samengevouwen",
|
||||
"foldersCollapsedDesc": "Mappen standaard samenvouwen",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Snippets",
|
||||
"hostManager": "Vertadministrator",
|
||||
"credentials": "Legitimasjon",
|
||||
"connections": "Tilkoblinger",
|
||||
"roleAdministrator": "Administratorsiden",
|
||||
"roleUser": "Bruker"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Rask rullefølsomhet",
|
||||
"moshCommandLabel": "Mosh kommando",
|
||||
"startupSnippetLabel": "Oppstart tekstutdrag",
|
||||
"keepaliveIntervalLabel": "Keepalive intervall",
|
||||
"keepaliveIntervalLabel": "Keepalive-intervall (sekunder)",
|
||||
"maxKeepaliveMisses": "Maksimum antall tap av Keepi",
|
||||
"tunnelSettings": "Innstillinger for tunnel",
|
||||
"enableTunneling": "Aktiver tunellering",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL kopiert",
|
||||
"telnetUrlCopied": "Telnet URL kopiert",
|
||||
"remoteDesktopUrlCopied": "URL for eksternt skrivebord",
|
||||
"wakeOnLanAction": "Vekk på LAN",
|
||||
"wakeOnLanSuccess": "Magisk pakke sendt til {{name}}",
|
||||
"wakeOnLanError": "Kunne ikke sende magisk pakke",
|
||||
"cloneHostAction": "Klon vert",
|
||||
"copyAddress": "Kopier adresse",
|
||||
"copyLink": "Kopier link",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Ingen verter samsvarer med søket ditt",
|
||||
"hostNotFound": "Verten ikke funnet",
|
||||
"searchHosts": "Søk verter...",
|
||||
"sortHosts": "Sorter verter",
|
||||
"sortDefault": "Standardordre",
|
||||
"sortNameAsc": "Navn (A → Å)",
|
||||
"sortNameDesc": "Navn (Å → A)",
|
||||
"sortIpAsc": "IP-adresse (stigende)",
|
||||
"sortIpDesc": "IP-adresse (beskrivelse)",
|
||||
"sortOnlineFirst": "På nett først",
|
||||
"sortOfflineFirst": "Frakoblet først",
|
||||
"sortPinnedFirst": "Festet først",
|
||||
"filterHosts": "Filtrer verter",
|
||||
"filterClearAll": "Fjern filtre",
|
||||
"filterStatusGroup": "Status",
|
||||
"filterOnline": "På nett",
|
||||
"filterOffline": "Frakoblet",
|
||||
"filterPinned": "Festet",
|
||||
"filterAuthGroup": "Autorisasjonstype",
|
||||
"filterAuthPassword": "Passord",
|
||||
"filterAuthKey": "SSH-nøkkel",
|
||||
"filterAuthCredential": "Legitimasjon",
|
||||
"filterAuthNone": "Ingen",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protokoll",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Funksjoner",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Filbehandler",
|
||||
"filterFeatureTunnel": "Tunnel",
|
||||
"filterFeatureDocker": "Dokker",
|
||||
"filterTagsGroup": "Tagger",
|
||||
"shareHost": "Del verten",
|
||||
"shareHostTitle": "Del: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Ingen fane tildelt"
|
||||
"noTabAssigned": "Ingen fane tildelt",
|
||||
"focusedPane": "Aktiv rute"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Ingen tilkoblinger",
|
||||
"noConnectionsDesc": "Åpne en terminal, filbehandler eller eksternt skrivebord for å se tilkoblinger her",
|
||||
"connectedFor": "Tilkoblet for {{duration}}",
|
||||
"connected": "Tilkoblet",
|
||||
"disconnected": "Frakoblet",
|
||||
"closeTab": "Lukk fanen",
|
||||
"closeConnection": "Lukk forbindelse",
|
||||
"forgetTab": "Glemme",
|
||||
"removeBackground": "Fjerne",
|
||||
"reconnect": "Koble til igjen",
|
||||
"reopenTab": "Åpne igjen",
|
||||
"sectionOpen": "Åpne",
|
||||
"sectionBackground": "Bakgrunn",
|
||||
"backgroundDesc": "Øktene varer i 30 minutter etter frakobling og kan kobles til på nytt.",
|
||||
"persisted": "Fortsatt i bakgrunnen",
|
||||
"expiresIn": "Utløper om {{duration}}",
|
||||
"search": "Søk etter tilkoblinger...",
|
||||
"noSearchResults": "Ingen forbindelser samsvarer med søket ditt"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Kobler til {{type}} økt...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Prøv igjen"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH-SERVERMANAGER",
|
||||
"tagline": "Selvhostet SSH og ekstern skrivebordsadministrasjon",
|
||||
"loginTitle": "Logg inn i Termix",
|
||||
"registerTitle": "Opprett konto",
|
||||
"forgotPassword": "Glemt passord?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Velg hvor mange paneler som skal vises",
|
||||
"panesTitle": "Paneler",
|
||||
"openTabsTitle": "Åpne faner",
|
||||
"dragTabsHint": "Dra fanene inn i ruter over",
|
||||
"dragTabsHint": "Dra faner inn i rutene ovenfor, eller bruk Hurtigtildeling",
|
||||
"dropHere": "Slipp her",
|
||||
"emptyPane": "Tom",
|
||||
"dashboard": "Kontrollpanel",
|
||||
"clearSplitScreen": "Tøm delt skjerm"
|
||||
"clearSplitScreen": "Tøm delt skjerm",
|
||||
"quickAssign": "Hurtigtildeling",
|
||||
"alreadyAssigned": "Rute {{index}}",
|
||||
"splitTab": "Del fane",
|
||||
"addToSplit": "Legg til i deling",
|
||||
"removeFromSplit": "Fjern fra Split",
|
||||
"assignToPane": "Tilordne til rute"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Opprett tekstutdrag",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Kunne ikke slette mappe",
|
||||
"folderEditSuccess": "Mappen ble oppdatert",
|
||||
"folderEditFailed": "Kunne ikke oppdatere mappen",
|
||||
"confirmRunMessage": "Kjør \"{{name}}\"?",
|
||||
"confirmRunButton": "Løp",
|
||||
"runSuccess": "Ran \"{{name}}\" i terminaler for {{count}}",
|
||||
"copySuccess": "Kopiert \"{{name}}\" til utklippstavle",
|
||||
"shareTitle": "Del tekstutdrag",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Gjeldende tilgang",
|
||||
"shareLoadError": "Kunne ikke laste delingsdata",
|
||||
"loading": "Laster...",
|
||||
"close": "Lukk"
|
||||
"close": "Lukk",
|
||||
"reorderFailed": "Kunne ikke lagre rekkefølgen på kodeutdraget"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Konto",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Tema",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Inngående farge",
|
||||
"settingsFileManager": "Fil Behandler",
|
||||
"fileColorCoding": "Programmering av filfarger",
|
||||
"fileColorCodingDesc": "Farge-kode filer etter type",
|
||||
"settingsTerminal": "Terminal",
|
||||
"commandAutocomplete": "Kommando Autofullfør",
|
||||
"commandAutocompleteDesc": "Vis autofullfør mens du skriver",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Fremhev terminal utgang",
|
||||
"commandPalette": "Kommandobalett palett",
|
||||
"commandPaletteDesc": "Aktiver hurtigtast",
|
||||
"sessionPersistence": "Økt Persistens",
|
||||
"sessionPersistenceDesc": "Holde sesjoner mellom tilkoblinger",
|
||||
"reopenTabsOnLogin": "Åpne faner igjen ved pålogging",
|
||||
"reopenTabsOnLoginDesc": "Gjenopprett åpne faner når du logger inn eller oppdaterer siden, selv fra en annen enhet",
|
||||
"confirmTabClose": "Bekreft lukking av fane",
|
||||
"confirmTabCloseDesc": "Spør før lukking av terminalfaner",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Vis vertsmerker",
|
||||
"showHostTagsDesc": "Vis emneord i vertslisten",
|
||||
"hostTrayOnClick": "Klikk for å utvide vertshandlinger",
|
||||
"hostTrayOnClickDesc": "Krev et klikk for å utvide vertshandlinger i stedet for å holde musepekeren over",
|
||||
"pinAppRail": "Pin App Rail",
|
||||
"pinAppRailDesc": "Hold appskinnen i venstre sidefelt alltid utvidet i stedet for å utvides når du holder musepekeren over den",
|
||||
"settingsSnippets": "Snippets",
|
||||
"foldersCollapsed": "Mapper samlet",
|
||||
"foldersCollapsedDesc": "Skjul mapper som standard",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Fragmenty",
|
||||
"hostManager": "Host Manager",
|
||||
"credentials": "Dane logowania",
|
||||
"connections": "Znajomości",
|
||||
"roleAdministrator": "Administrator",
|
||||
"roleUser": "Użytkownik"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Czułość szybkiego przewijania",
|
||||
"moshCommandLabel": "Dowództwo Mosh",
|
||||
"startupSnippetLabel": "Snippet startowy",
|
||||
"keepaliveIntervalLabel": "Interwał Keepalive",
|
||||
"keepaliveIntervalLabel": "Interwał utrzymywania połączenia (sekundy)",
|
||||
"maxKeepaliveMisses": "Maks. ilość pominięć Keepalive",
|
||||
"tunnelSettings": "Ustawienia tunelu",
|
||||
"enableTunneling": "Włącz Tunelowanie",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "Adres URL VNC skopiowany",
|
||||
"telnetUrlCopied": "Skopiowano adres URL Telnet",
|
||||
"remoteDesktopUrlCopied": "Adres URL zdalnego pulpitu skopiowany",
|
||||
"wakeOnLanAction": "Wake on LAN",
|
||||
"wakeOnLanSuccess": "Pakiet magiczny wysłany do {{name}}",
|
||||
"wakeOnLanError": "Nie udało się wysłać pakietu magicznego",
|
||||
"cloneHostAction": "Klonuj hosta",
|
||||
"copyAddress": "Kopiuj adres",
|
||||
"copyLink": "Kopiuj link",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Brak hostów pasujących do Twojego wyszukiwania",
|
||||
"hostNotFound": "Nie znaleziono hosta",
|
||||
"searchHosts": "Szukaj hostów...",
|
||||
"sortHosts": "Sortuj hosty",
|
||||
"sortDefault": "Domyślna kolejność",
|
||||
"sortNameAsc": "Imię (A → Z)",
|
||||
"sortNameDesc": "Imię (Z → A)",
|
||||
"sortIpAsc": "Adres IP (rosnąco)",
|
||||
"sortIpDesc": "Adres IP (opis)",
|
||||
"sortOnlineFirst": "Pierwszy online",
|
||||
"sortOfflineFirst": "Najpierw offline",
|
||||
"sortPinnedFirst": "Przypięte jako pierwsze",
|
||||
"filterHosts": "Filtruj hosty",
|
||||
"filterClearAll": "Wyczyść filtry",
|
||||
"filterStatusGroup": "Status",
|
||||
"filterOnline": "W sieci",
|
||||
"filterOffline": "Niedostępny",
|
||||
"filterPinned": "Przypięte",
|
||||
"filterAuthGroup": "Typ autoryzacji",
|
||||
"filterAuthPassword": "Hasło",
|
||||
"filterAuthKey": "Klucz SSH",
|
||||
"filterAuthCredential": "Mandat",
|
||||
"filterAuthNone": "Nic",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protokół",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Cechy",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Menedżer plików",
|
||||
"filterFeatureTunnel": "Tunel",
|
||||
"filterFeatureDocker": "Doker",
|
||||
"filterTagsGroup": "Tagi",
|
||||
"shareHost": "Udostępnij hosta",
|
||||
"shareHostTitle": "Udostępnianie: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Brak przypisanych kart"
|
||||
"noTabAssigned": "Brak przypisanych kart",
|
||||
"focusedPane": "Aktywny panel"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Brak połączeń",
|
||||
"noConnectionsDesc": "Otwórz terminal, menedżera plików lub pulpit zdalny, aby zobaczyć połączenia tutaj",
|
||||
"connectedFor": "Połączono dla {{duration}}",
|
||||
"connected": "Połączony",
|
||||
"disconnected": "Bezładny",
|
||||
"closeTab": "Zamknij kartę",
|
||||
"closeConnection": "Bliskie połączenie",
|
||||
"forgetTab": "Zapominać",
|
||||
"removeBackground": "Usunąć",
|
||||
"reconnect": "Na nowo połączyć",
|
||||
"reopenTab": "Otworzyć na nowo",
|
||||
"sectionOpen": "Otwarte",
|
||||
"sectionBackground": "Tło",
|
||||
"backgroundDesc": "Sesje trwają przez 30 minut od momentu rozłączenia, po czym można je ponownie nawiązać.",
|
||||
"persisted": "Trwały w tle",
|
||||
"expiresIn": "Wygasa za {{duration}}",
|
||||
"search": "Wyszukaj połączenia...",
|
||||
"noSearchResults": "Brak połączeń odpowiadających Twojemu wyszukiwaniu"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Łączenie z sesją {{type}}...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Ponów próbę"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "ZARZĄDZANIE SERWEREM SSH",
|
||||
"tagline": "Samodzielnie hostowany SSH i zdalne zarządzanie pulpitem",
|
||||
"loginTitle": "Zaloguj się do Termix",
|
||||
"registerTitle": "Utwórz konto",
|
||||
"forgotPassword": "Zapomniałeś hasła?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Wybierz ile paneli chcesz wyświetlić",
|
||||
"panesTitle": "Panele",
|
||||
"openTabsTitle": "Otwórz karty",
|
||||
"dragTabsHint": "Przeciągnij karty do paneli powyżej",
|
||||
"dragTabsHint": "Przeciągnij karty do paneli powyżej lub użyj funkcji Szybkie przypisywanie",
|
||||
"dropHere": "Upuść tutaj",
|
||||
"emptyPane": "Pusty",
|
||||
"dashboard": "Pulpit",
|
||||
"clearSplitScreen": "Wyczyść dzielony ekran"
|
||||
"clearSplitScreen": "Wyczyść dzielony ekran",
|
||||
"quickAssign": "Szybkie przypisanie",
|
||||
"alreadyAssigned": "Panel {{index}}",
|
||||
"splitTab": "Podziel kartę",
|
||||
"addToSplit": "Dodaj do podziału",
|
||||
"removeFromSplit": "Usuń z podziału",
|
||||
"assignToPane": "Przypisz do panelu"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Utwórz Snippet",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Nie udało się usunąć folderu",
|
||||
"folderEditSuccess": "Folder zaktualizowany pomyślnie",
|
||||
"folderEditFailed": "Nie udało się zaktualizować folderu",
|
||||
"confirmRunMessage": "Uruchomić \"{{name}}\"?",
|
||||
"confirmRunButton": "Uruchomić",
|
||||
"runSuccess": "Ran \"{{name}}\" w terminalach {{count}}",
|
||||
"copySuccess": "Skopiowano \"{{name}}\" do schowka",
|
||||
"shareTitle": "Udostępnij Snippet",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Bieżący dostęp",
|
||||
"shareLoadError": "Nie udało się załadować udostępnionych danych",
|
||||
"loading": "Ładowanie...",
|
||||
"close": "Zamknij"
|
||||
"close": "Zamknij",
|
||||
"reorderFailed": "Nie udało się zapisać kolejności fragmentów"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Konto",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Motyw",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Kolor akcentu",
|
||||
"settingsFileManager": "Menedżer plików",
|
||||
"fileColorCoding": "Kodowanie kolorów plików",
|
||||
"fileColorCodingDesc": "Pliki kodów kolorów według typu",
|
||||
"settingsTerminal": "Terminal",
|
||||
"commandAutocomplete": "Autouzupełnianie polecenia",
|
||||
"commandAutocompleteDesc": "Pokaż autouzupełnianie podczas pisania",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Podświetl terminal wyjściowy",
|
||||
"commandPalette": "Paleta Poleceń",
|
||||
"commandPaletteDesc": "Włącz skrót klawiaturowy",
|
||||
"sessionPersistence": "Trwałość sesji",
|
||||
"sessionPersistenceDesc": "Zachowaj sesje między ponownymi połączeniami",
|
||||
"reopenTabsOnLogin": "Ponowne otwieranie kart podczas logowania",
|
||||
"reopenTabsOnLoginDesc": "Przywracaj otwarte karty po zalogowaniu się lub odświeżeniu strony, nawet z innego urządzenia",
|
||||
"confirmTabClose": "Potwierdź zamknięcie karty",
|
||||
"confirmTabCloseDesc": "Zapytaj przed zamknięciem kart terminali",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Pokaż tagi hosta",
|
||||
"showHostTagsDesc": "Wyświetl tagi na liście hostów",
|
||||
"hostTrayOnClick": "Kliknij, aby rozwinąć działania hosta",
|
||||
"hostTrayOnClickDesc": "Wymagaj kliknięcia, aby rozwinąć działania hosta, zamiast najechania kursorem",
|
||||
"pinAppRail": "Aplikacja Pin Rail",
|
||||
"pinAppRailDesc": "Utrzymuj zawsze rozwinięty lewy pasek boczny aplikacji zamiast rozwijać go po najechaniu kursorem",
|
||||
"settingsSnippets": "Fragmenty",
|
||||
"foldersCollapsed": "Foldery upadły",
|
||||
"foldersCollapsedDesc": "Domyślnie zwiń foldery",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Trechos",
|
||||
"hostManager": "Gerenciador de Host",
|
||||
"credentials": "Credenciais",
|
||||
"connections": "Conexões",
|
||||
"roleAdministrator": "Administrador",
|
||||
"roleUser": "Usuário"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Sensibilidade de rolagem rápida",
|
||||
"moshCommandLabel": "Comando Mosh",
|
||||
"startupSnippetLabel": "Trecho de Inicialização",
|
||||
"keepaliveIntervalLabel": "Intervalo de Mantícora",
|
||||
"keepaliveIntervalLabel": "Intervalo de Keepalive (segundos)",
|
||||
"maxKeepaliveMisses": "Máximo de Perdas Keepalive",
|
||||
"tunnelSettings": "Configurações de túnel",
|
||||
"enableTunneling": "Ativar Túnel",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "URL VNC copiada",
|
||||
"telnetUrlCopied": "URL da rede copiada",
|
||||
"remoteDesktopUrlCopied": "URL da área de trabalho remoto copiada",
|
||||
"wakeOnLanAction": "Wake on LAN",
|
||||
"wakeOnLanSuccess": "Pacote mágico enviado para {{name}}",
|
||||
"wakeOnLanError": "Falha ao enviar o pacote mágico",
|
||||
"cloneHostAction": "Clonar Host",
|
||||
"copyAddress": "Copiar Endereço",
|
||||
"copyLink": "Copiar link",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Nenhum host corresponde à sua pesquisa",
|
||||
"hostNotFound": "Host não encontrado",
|
||||
"searchHosts": "Procurar hosts...",
|
||||
"sortHosts": "Classificar hosts",
|
||||
"sortDefault": "Ordem padrão",
|
||||
"sortNameAsc": "Nome (A → Z)",
|
||||
"sortNameDesc": "Nome (Z → A)",
|
||||
"sortIpAsc": "Endereço IP (Asc)",
|
||||
"sortIpDesc": "Endereço IP (Descrição)",
|
||||
"sortOnlineFirst": "Online First",
|
||||
"sortOfflineFirst": "Primeiro offline",
|
||||
"sortPinnedFirst": "Fixado primeiro",
|
||||
"filterHosts": "Filtrar hosts",
|
||||
"filterClearAll": "Limpar filtros",
|
||||
"filterStatusGroup": "Status",
|
||||
"filterOnline": "On-line",
|
||||
"filterOffline": "Offline",
|
||||
"filterPinned": "Fixado",
|
||||
"filterAuthGroup": "Tipo de autenticação",
|
||||
"filterAuthPassword": "Senha",
|
||||
"filterAuthKey": "Chave SSH",
|
||||
"filterAuthCredential": "Credencial",
|
||||
"filterAuthNone": "Nenhum",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protocolo",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Características",
|
||||
"filterFeatureTerminal": "terminal",
|
||||
"filterFeatureFileManager": "Gerenciador de arquivos",
|
||||
"filterFeatureTunnel": "Túnel",
|
||||
"filterFeatureDocker": "Docker",
|
||||
"filterTagsGroup": "Etiquetas",
|
||||
"shareHost": "Host de compartilhamento",
|
||||
"shareHostTitle": "Compartilhar: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Nenhuma aba atribuída"
|
||||
"noTabAssigned": "Nenhuma aba atribuída",
|
||||
"focusedPane": "Painel ativo"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Sem conexões",
|
||||
"noConnectionsDesc": "Abra um terminal, um gerenciador de arquivos ou uma área de trabalho remota para ver as conexões aqui.",
|
||||
"connectedFor": "Conectado por {{duration}}",
|
||||
"connected": "Conectado",
|
||||
"disconnected": "Desconectado",
|
||||
"closeTab": "Fechar aba",
|
||||
"closeConnection": "Conexão próxima",
|
||||
"forgetTab": "Esquecer",
|
||||
"removeBackground": "Remover",
|
||||
"reconnect": "Reconectar",
|
||||
"reopenTab": "Reabrir",
|
||||
"sectionOpen": "Abrir",
|
||||
"sectionBackground": "Fundo",
|
||||
"backgroundDesc": "As sessões permanecem ativas por 30 minutos após a desconexão e podem ser reconectadas.",
|
||||
"persisted": "Persistiu em segundo plano",
|
||||
"expiresIn": "Expira em {{duration}}",
|
||||
"search": "Buscar conexões...",
|
||||
"noSearchResults": "Nenhuma conexão corresponde à sua pesquisa."
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Conectando à sessão {{type}}",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Repetir"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "GERENCIADOR DE SERVIDOR SSH",
|
||||
"tagline": "SSH autohospedado e gerenciamento de desktop remoto",
|
||||
"loginTitle": "Faça login para Termix",
|
||||
"registerTitle": "Criar conta",
|
||||
"forgotPassword": "Esqueceu a senha?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Escolha quantos painéis mostrar",
|
||||
"panesTitle": "Painéis",
|
||||
"openTabsTitle": "Abas abertas",
|
||||
"dragTabsHint": "Arraste as abas para os painéis acima",
|
||||
"dragTabsHint": "Arraste as guias para os painéis acima ou use a Atribuição Rápida.",
|
||||
"dropHere": "Solte aqui",
|
||||
"emptyPane": "Vazio",
|
||||
"dashboard": "Painel",
|
||||
"clearSplitScreen": "Limpar Tela dividida"
|
||||
"clearSplitScreen": "Limpar Tela dividida",
|
||||
"quickAssign": "Atribuição rápida",
|
||||
"alreadyAssigned": "Painel {{index}}",
|
||||
"splitTab": "Aba dividida",
|
||||
"addToSplit": "Adicionar à divisão",
|
||||
"removeFromSplit": "Remover da divisão",
|
||||
"assignToPane": "Atribuir ao painel"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Criar Snippet",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Falha ao excluir pasta",
|
||||
"folderEditSuccess": "Pasta atualizada com sucesso",
|
||||
"folderEditFailed": "Falha ao atualizar a pasta",
|
||||
"confirmRunMessage": "Executar \"{{name}}\"?",
|
||||
"confirmRunButton": "Correr",
|
||||
"runSuccess": "Executado \"{{name}}\" em {{count}} terminal(is)",
|
||||
"copySuccess": "Copiado \"{{name}}\" para área de transferência",
|
||||
"shareTitle": "Compartilhar Snippet",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Acesso atual",
|
||||
"shareLoadError": "Não foi possível carregar os dados de compartilhamento",
|
||||
"loading": "Carregandochar@@0",
|
||||
"close": "FECHAR"
|
||||
"close": "FECHAR",
|
||||
"reorderFailed": "Falha ao salvar a ordem do trecho de código"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "conta",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Tema",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Cor do acento",
|
||||
"settingsFileManager": "Gerenciador de Arquivos",
|
||||
"fileColorCoding": "Codificação de cores",
|
||||
"fileColorCodingDesc": "Arquivos coloridos por tipo de código",
|
||||
"settingsTerminal": "Terminal",
|
||||
"commandAutocomplete": "Auto-completar comando",
|
||||
"commandAutocompleteDesc": "Mostrar autocomplete durante a digitação",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Destacar saída do terminal",
|
||||
"commandPalette": "Paleta de comando",
|
||||
"commandPaletteDesc": "Ativar atalho de teclado",
|
||||
"sessionPersistence": "Persistência de Sessão",
|
||||
"sessionPersistenceDesc": "Manter sessões entre reconexões",
|
||||
"reopenTabsOnLogin": "Reabrir abas ao fazer login",
|
||||
"reopenTabsOnLoginDesc": "Restaure suas abas abertas ao fazer login ou atualizar a página, mesmo em outro dispositivo.",
|
||||
"confirmTabClose": "Confirmar Fechar Aba",
|
||||
"confirmTabCloseDesc": "Perguntar antes de fechar os guias dos terminais",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Exibir Tags do Host",
|
||||
"showHostTagsDesc": "Exibir tags na lista de endereços",
|
||||
"hostTrayOnClick": "Clique para expandir as ações do anfitrião",
|
||||
"hostTrayOnClickDesc": "Exigir um clique para expandir as ações do host em vez de passar o cursor sobre elas.",
|
||||
"pinAppRail": "Aplicativo Pin Rail",
|
||||
"pinAppRailDesc": "Mantenha a barra lateral esquerda do aplicativo sempre expandida, em vez de expandir ao passar o cursor sobre ela.",
|
||||
"settingsSnippets": "Trechos",
|
||||
"foldersCollapsed": "Pastas colapsadas",
|
||||
"foldersCollapsedDesc": "Recolher pastas por padrão",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Trechos",
|
||||
"hostManager": "Gerenciador de Host",
|
||||
"credentials": "Credenciais",
|
||||
"connections": "Conexões",
|
||||
"roleAdministrator": "Administrador",
|
||||
"roleUser": "Usuário"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Sensibilidade de rolagem rápida",
|
||||
"moshCommandLabel": "Comando Mosh",
|
||||
"startupSnippetLabel": "Trecho de Inicialização",
|
||||
"keepaliveIntervalLabel": "Intervalo de Mantícora",
|
||||
"keepaliveIntervalLabel": "Intervalo de Keepalive (segundos)",
|
||||
"maxKeepaliveMisses": "Máximo de Perdas Keepalive",
|
||||
"tunnelSettings": "Configurações de túnel",
|
||||
"enableTunneling": "Ativar Túnel",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "URL VNC copiada",
|
||||
"telnetUrlCopied": "URL da rede copiada",
|
||||
"remoteDesktopUrlCopied": "URL da área de trabalho remoto copiada",
|
||||
"wakeOnLanAction": "Wake on LAN",
|
||||
"wakeOnLanSuccess": "Pacote mágico enviado para {{name}}",
|
||||
"wakeOnLanError": "Falha ao enviar o pacote mágico",
|
||||
"cloneHostAction": "Clonar Host",
|
||||
"copyAddress": "Copiar Endereço",
|
||||
"copyLink": "Copiar link",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Nenhum host corresponde à sua pesquisa",
|
||||
"hostNotFound": "Host não encontrado",
|
||||
"searchHosts": "Procurar hosts...",
|
||||
"sortHosts": "Classificar hosts",
|
||||
"sortDefault": "Ordem padrão",
|
||||
"sortNameAsc": "Nome (A → Z)",
|
||||
"sortNameDesc": "Nome (Z → A)",
|
||||
"sortIpAsc": "Endereço IP (Asc)",
|
||||
"sortIpDesc": "Endereço IP (Descrição)",
|
||||
"sortOnlineFirst": "Online Primeiro",
|
||||
"sortOfflineFirst": "Primeiro offline",
|
||||
"sortPinnedFirst": "Fixado primeiro",
|
||||
"filterHosts": "Filtrar hosts",
|
||||
"filterClearAll": "Limpar filtros",
|
||||
"filterStatusGroup": "Estatuto",
|
||||
"filterOnline": "Online",
|
||||
"filterOffline": "Offline",
|
||||
"filterPinned": "Fixado",
|
||||
"filterAuthGroup": "Tipo de autenticação",
|
||||
"filterAuthPassword": "Palavra-passe",
|
||||
"filterAuthKey": "Chave SSH",
|
||||
"filterAuthCredential": "Credencial",
|
||||
"filterAuthNone": "Nenhum",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protocolo",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Funcionalidades",
|
||||
"filterFeatureTerminal": "terminal",
|
||||
"filterFeatureFileManager": "Gestor de arquivos",
|
||||
"filterFeatureTunnel": "Túnel",
|
||||
"filterFeatureDocker": "Docker",
|
||||
"filterTagsGroup": "Etiquetas",
|
||||
"shareHost": "Host de compartilhamento",
|
||||
"shareHostTitle": "Compartilhar: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Nenhuma aba atribuída"
|
||||
"noTabAssigned": "Nenhuma aba atribuída",
|
||||
"focusedPane": "Painel ativo"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Sem ligações",
|
||||
"noConnectionsDesc": "Abra um terminal, um gestor de ficheiros ou uma área de trabalho remota para ver aqui as ligações.",
|
||||
"connectedFor": "Ligado por {{duration}}",
|
||||
"connected": "Conectado",
|
||||
"disconnected": "Desconectado",
|
||||
"closeTab": "Fechar aba",
|
||||
"closeConnection": "Conexão próxima",
|
||||
"forgetTab": "Esquecer-se",
|
||||
"removeBackground": "Remover",
|
||||
"reconnect": "Reconectar",
|
||||
"reopenTab": "Reabrir",
|
||||
"sectionOpen": "Aberto",
|
||||
"sectionBackground": "Fundo",
|
||||
"backgroundDesc": "As sessões permanecem ativas durante 30 minutos após a desconexão e podem ser reconectadas.",
|
||||
"persisted": "Persistiu em segundo plano",
|
||||
"expiresIn": "Expira em {{duration}}",
|
||||
"search": "Procurar ligações...",
|
||||
"noSearchResults": "Nenhuma ligação corresponde à sua pesquisa."
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Conectando à sessão {{type}}",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Repetir"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "GERENCIADOR DE SERVIDOR SSH",
|
||||
"tagline": "SSH auto-hospedado e gestão de desktop remoto",
|
||||
"loginTitle": "Faça login para Termix",
|
||||
"registerTitle": "Criar conta",
|
||||
"forgotPassword": "Esqueceu a senha?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Escolha quantos painéis mostrar",
|
||||
"panesTitle": "Painéis",
|
||||
"openTabsTitle": "Abas abertas",
|
||||
"dragTabsHint": "Arraste as abas para os painéis acima",
|
||||
"dragTabsHint": "Arraste os separadores para os painéis acima ou utilize a Atribuição Rápida.",
|
||||
"dropHere": "Solte aqui",
|
||||
"emptyPane": "Vazio",
|
||||
"dashboard": "Painel",
|
||||
"clearSplitScreen": "Limpar Tela dividida"
|
||||
"clearSplitScreen": "Limpar Tela dividida",
|
||||
"quickAssign": "Atribuição rápida",
|
||||
"alreadyAssigned": "Painel {{index}}",
|
||||
"splitTab": "Aba dividida",
|
||||
"addToSplit": "Adicionar à divisão",
|
||||
"removeFromSplit": "Remover da divisão",
|
||||
"assignToPane": "Atribuir ao painel"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Criar Snippet",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Falha ao excluir pasta",
|
||||
"folderEditSuccess": "Pasta atualizada com sucesso",
|
||||
"folderEditFailed": "Falha ao atualizar a pasta",
|
||||
"confirmRunMessage": "Executar \"{{name}}\"?",
|
||||
"confirmRunButton": "Correr",
|
||||
"runSuccess": "Executado \"{{name}}\" em {{count}} terminal(is)",
|
||||
"copySuccess": "Copiado \"{{name}}\" para área de transferência",
|
||||
"shareTitle": "Compartilhar Snippet",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Acesso atual",
|
||||
"shareLoadError": "Não foi possível carregar os dados de compartilhamento",
|
||||
"loading": "Carregandochar@@0",
|
||||
"close": "FECHAR"
|
||||
"close": "FECHAR",
|
||||
"reorderFailed": "Falha ao guardar a ordem do trecho de código"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "conta",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Tema",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Cor do acento",
|
||||
"settingsFileManager": "Gerenciador de Arquivos",
|
||||
"fileColorCoding": "Codificação de cores",
|
||||
"fileColorCodingDesc": "Arquivos coloridos por tipo de código",
|
||||
"settingsTerminal": "Terminal",
|
||||
"commandAutocomplete": "Auto-completar comando",
|
||||
"commandAutocompleteDesc": "Mostrar autocomplete durante a digitação",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Destacar saída do terminal",
|
||||
"commandPalette": "Paleta de comando",
|
||||
"commandPaletteDesc": "Ativar atalho de teclado",
|
||||
"sessionPersistence": "Persistência de Sessão",
|
||||
"sessionPersistenceDesc": "Manter sessões entre reconexões",
|
||||
"reopenTabsOnLogin": "Reabrir separadores ao fazer login",
|
||||
"reopenTabsOnLoginDesc": "Restaure as suas abas abertas ao iniciar sessão ou atualizar a página, mesmo noutro dispositivo.",
|
||||
"confirmTabClose": "Confirmar Fechar Aba",
|
||||
"confirmTabCloseDesc": "Perguntar antes de fechar os guias dos terminais",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Exibir Tags do Host",
|
||||
"showHostTagsDesc": "Exibir tags na lista de endereços",
|
||||
"hostTrayOnClick": "Clique para expandir as ações do anfitrião",
|
||||
"hostTrayOnClickDesc": "Exigir um clique para expandir as ações do host em vez de passar o cursor sobre elas.",
|
||||
"pinAppRail": "Aplicação Pin Rail",
|
||||
"pinAppRailDesc": "Mantenha a barra lateral esquerda da aplicação sempre expandida, em vez de expandir ao passar o cursor sobre a mesma.",
|
||||
"settingsSnippets": "Trechos",
|
||||
"foldersCollapsed": "Pastas colapsadas",
|
||||
"foldersCollapsedDesc": "Recolher pastas por padrão",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Snippet-uri",
|
||||
"hostManager": "Manager Gazdă",
|
||||
"credentials": "Acreditări",
|
||||
"connections": "Conexiuni",
|
||||
"roleAdministrator": "Administrator",
|
||||
"roleUser": "Utilizator"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Sensibilitate derulare rapidă",
|
||||
"moshCommandLabel": "Comanda Moscheii",
|
||||
"startupSnippetLabel": "Snippet de pornire",
|
||||
"keepaliveIntervalLabel": "Interval Keepalive",
|
||||
"keepaliveIntervalLabel": "Interval Keepalive (secunde)",
|
||||
"maxKeepaliveMisses": "Max Keepalive ratează",
|
||||
"tunnelSettings": "Setări tunel",
|
||||
"enableTunneling": "Activează tunelarea",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "URL VNC copiat",
|
||||
"telnetUrlCopied": "URL telnet copiat",
|
||||
"remoteDesktopUrlCopied": "URL la distanță pentru Desktop copiat",
|
||||
"wakeOnLanAction": "Trezire prin LAN",
|
||||
"wakeOnLanSuccess": "Pachet magic trimis către {{name}}",
|
||||
"wakeOnLanError": "Nu s-a putut trimite pachetul magic",
|
||||
"cloneHostAction": "Gazda Clonei",
|
||||
"copyAddress": "Copiază adresa",
|
||||
"copyLink": "Copiază link-ul",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Nicio gazdă nu se potrivește cu căutarea ta",
|
||||
"hostNotFound": "Gazda nu a fost găsită",
|
||||
"searchHosts": "Caută gazde...",
|
||||
"sortHosts": "Sortează gazdele",
|
||||
"sortDefault": "Ordine implicită",
|
||||
"sortNameAsc": "Nume (A → Z)",
|
||||
"sortNameDesc": "Nume (Z → A)",
|
||||
"sortIpAsc": "Adresă IP (Asc)",
|
||||
"sortIpDesc": "Adresă IP (Desc)",
|
||||
"sortOnlineFirst": "Online First",
|
||||
"sortOfflineFirst": "Offline First",
|
||||
"sortPinnedFirst": "Fixat primul",
|
||||
"filterHosts": "Filtrare gazde",
|
||||
"filterClearAll": "Ștergeți filtrele",
|
||||
"filterStatusGroup": "Stare",
|
||||
"filterOnline": "Online",
|
||||
"filterOffline": "Offline",
|
||||
"filterPinned": "Fixat",
|
||||
"filterAuthGroup": "Tip de autentificare",
|
||||
"filterAuthPassword": "Parolă",
|
||||
"filterAuthKey": "Cheie SSH",
|
||||
"filterAuthCredential": "Acreditare",
|
||||
"filterAuthNone": "Nici unul",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protocol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Caracteristici",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Manager de fișiere",
|
||||
"filterFeatureTunnel": "Tunel",
|
||||
"filterFeatureDocker": "Docher",
|
||||
"filterTagsGroup": "Etichete",
|
||||
"shareHost": "Distribuie gazda",
|
||||
"shareHostTitle": "Partajare: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Nici o filă atribuită"
|
||||
"noTabAssigned": "Nici o filă atribuită",
|
||||
"focusedPane": "Panou activ"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Fără conexiuni",
|
||||
"noConnectionsDesc": "Deschideți un terminal, un manager de fișiere sau un desktop la distanță pentru a vedea conexiunile aici",
|
||||
"connectedFor": "Conectat pentru {{duration}}",
|
||||
"connected": "Conectat",
|
||||
"disconnected": "Deconectat",
|
||||
"closeTab": "Închide fila",
|
||||
"closeConnection": "Conexiune strânsă",
|
||||
"forgetTab": "Uita",
|
||||
"removeBackground": "Elimina",
|
||||
"reconnect": "Reconectați-vă",
|
||||
"reopenTab": "Redeschide",
|
||||
"sectionOpen": "Deschide",
|
||||
"sectionBackground": "Fundal",
|
||||
"backgroundDesc": "Sesiunile persistă timp de 30 de minute după deconectare și pot fi reconectate.",
|
||||
"persisted": "A persistat în fundal",
|
||||
"expiresIn": "Expiră în {{duration}}",
|
||||
"search": "Căutați conexiuni...",
|
||||
"noSearchResults": "Nicio conexiune nu corespunde căutării dvs."
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Conectare la {{type}} sesiune...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Reîncercați"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "GESTIONAREA SERVER SSH",
|
||||
"tagline": "Gestionare SSH și desktop la distanță auto-găzduită",
|
||||
"loginTitle": "Autentificare în Termix",
|
||||
"registerTitle": "Creare cont",
|
||||
"forgotPassword": "Parolă uitată?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Alege câte panouri să fie afișate",
|
||||
"panesTitle": "Panouri",
|
||||
"openTabsTitle": "Deschide filele",
|
||||
"dragTabsHint": "Trage filele în panourile de mai sus",
|
||||
"dragTabsHint": "Trageți filele în panourile de mai sus sau utilizați Atribuire rapidă",
|
||||
"dropHere": "Plasează aici",
|
||||
"emptyPane": "Golire",
|
||||
"dashboard": "Panou",
|
||||
"clearSplitScreen": "Golește ecranul împărțit"
|
||||
"clearSplitScreen": "Golește ecranul împărțit",
|
||||
"quickAssign": "Atribuire rapidă",
|
||||
"alreadyAssigned": "Panou {{index}}",
|
||||
"splitTab": "Tabulă divizată",
|
||||
"addToSplit": "Adăugați la divizare",
|
||||
"removeFromSplit": "Eliminați din Split",
|
||||
"assignToPane": "Atribuire panoului"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Creează fragmente",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Eroare la ștergerea dosarului",
|
||||
"folderEditSuccess": "Dosar actualizat cu succes",
|
||||
"folderEditFailed": "Actualizarea dosarului a eșuat",
|
||||
"confirmRunMessage": "Executați „{{name}}”?",
|
||||
"confirmRunButton": "Aleargă",
|
||||
"runSuccess": "Ran \"{{name}}\" în terminalul(ele) {{count}}",
|
||||
"copySuccess": "A copiat \"{{name}}\" în clipboard",
|
||||
"shareTitle": "Distribuie fragmentul",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Acces curent",
|
||||
"shareLoadError": "Încărcarea datelor partajate a eșuat",
|
||||
"loading": "Încărcare...",
|
||||
"close": "Inchide"
|
||||
"close": "Inchide",
|
||||
"reorderFailed": "Nu s-a putut salva ordinea fragmentelor."
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Cont",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Tema",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Culoare accent",
|
||||
"settingsFileManager": "Manager fişiere",
|
||||
"fileColorCoding": "Codificarea culorii fișierului",
|
||||
"fileColorCodingDesc": "Fişiere cu coduri de culoare după tip",
|
||||
"settingsTerminal": "Terminal",
|
||||
"commandAutocomplete": "Comanda Autocompletare",
|
||||
"commandAutocompleteDesc": "Arată autocompletarea în timpul tastării",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Evidențiază ieșirea terminalului",
|
||||
"commandPalette": "Paletă de comandă",
|
||||
"commandPaletteDesc": "Activează scurtătura pentru tastatură",
|
||||
"sessionPersistence": "Persistența sesiunii",
|
||||
"sessionPersistenceDesc": "Păstrați sesiunile între reconectări",
|
||||
"reopenTabsOnLogin": "Redeschideți filele la conectare",
|
||||
"reopenTabsOnLoginDesc": "Restaurați filele deschise atunci când vă conectați sau reîmprospătați pagina, chiar și de pe un alt dispozitiv",
|
||||
"confirmTabClose": "Confirmă fila închisă",
|
||||
"confirmTabCloseDesc": "Întreabă înainte de a închide filele terminale",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Arată etichetele gazdei",
|
||||
"showHostTagsDesc": "Afișează etichetele în lista de host-uri",
|
||||
"hostTrayOnClick": "Faceți clic pentru a extinde acțiunile gazdei",
|
||||
"hostTrayOnClickDesc": "Necesită un clic pentru a extinde acțiunile gazdei în loc de plasarea cursorului",
|
||||
"pinAppRail": "Fixează aplicația Pin Rail",
|
||||
"pinAppRailDesc": "Mențineți șina aplicației din bara laterală stângă mereu extinsă, în loc să se extindă la trecerea cu mouse-ul peste mouse",
|
||||
"settingsSnippets": "Snippet-uri",
|
||||
"foldersCollapsed": "Dosare Restrânse",
|
||||
"foldersCollapsedDesc": "Restrânge implicit dosarele",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Сниппеты",
|
||||
"hostManager": "Менеджер хостов",
|
||||
"credentials": "Учетные данные",
|
||||
"connections": "Связи",
|
||||
"roleAdministrator": "Администратор",
|
||||
"roleUser": "Пользователь"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Быстрая чувствительность прокрутки",
|
||||
"moshCommandLabel": "Мош Командование",
|
||||
"startupSnippetLabel": "Сниппет запуска",
|
||||
"keepaliveIntervalLabel": "Интервал хранения",
|
||||
"keepaliveIntervalLabel": "Интервал поддержания соединения (секунды)",
|
||||
"maxKeepaliveMisses": "Максимальное количество пропусков",
|
||||
"tunnelSettings": "Настройки Туннеля",
|
||||
"enableTunneling": "Включить туннелирование",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL скопирован",
|
||||
"telnetUrlCopied": "Скопирован Telnet URL",
|
||||
"remoteDesktopUrlCopied": "URL удаленного рабочего стола скопирован",
|
||||
"wakeOnLanAction": "Пробуждение по локальной сети",
|
||||
"wakeOnLanSuccess": "Волшебный пакет отправлен на {{name}}",
|
||||
"wakeOnLanError": "Не удалось отправить волшебный пакет.",
|
||||
"cloneHostAction": "Клонировать хост",
|
||||
"copyAddress": "Копировать адрес",
|
||||
"copyLink": "Скопировать ссылку",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Нет хостов, соответствующих вашему запросу",
|
||||
"hostNotFound": "Хост не найден",
|
||||
"searchHosts": "Поиск узлов...",
|
||||
"sortHosts": "Сортировка хостов",
|
||||
"sortDefault": "Порядок по умолчанию",
|
||||
"sortNameAsc": "Имя (от А до Я)",
|
||||
"sortNameDesc": "Имя (Z → A)",
|
||||
"sortIpAsc": "IP-адрес (восходящий)",
|
||||
"sortIpDesc": "IP-адрес (описание)",
|
||||
"sortOnlineFirst": "Онлайн прежде всего",
|
||||
"sortOfflineFirst": "Офлайн-первый",
|
||||
"sortPinnedFirst": "Закреплено первым",
|
||||
"filterHosts": "Фильтрация хостов",
|
||||
"filterClearAll": "Очистить фильтры",
|
||||
"filterStatusGroup": "Статус",
|
||||
"filterOnline": "Онлайн",
|
||||
"filterOffline": "Офлайн",
|
||||
"filterPinned": "Закреплено",
|
||||
"filterAuthGroup": "Тип аутентификации",
|
||||
"filterAuthPassword": "Пароль",
|
||||
"filterAuthKey": "Ключ SSH",
|
||||
"filterAuthCredential": "Удостоверение личности",
|
||||
"filterAuthNone": "Никто",
|
||||
"filterAuthOpkssh": "ОПКШ",
|
||||
"filterProtocolGroup": "Протокол",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "РДП",
|
||||
"filterProtocolVnc": "ВНК",
|
||||
"filterProtocolTelnet": "Телнет",
|
||||
"filterFeaturesGroup": "Функции",
|
||||
"filterFeatureTerminal": "Терминал",
|
||||
"filterFeatureFileManager": "Файловый менеджер",
|
||||
"filterFeatureTunnel": "Туннель",
|
||||
"filterFeatureDocker": "Docker",
|
||||
"filterTagsGroup": "Теги",
|
||||
"shareHost": "Поделиться хостом",
|
||||
"shareHostTitle": "Поделиться: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Вкладка не назначена"
|
||||
"noTabAssigned": "Вкладка не назначена",
|
||||
"focusedPane": "Активная панель"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Нет подключений",
|
||||
"noConnectionsDesc": "Откройте терминал, файловый менеджер или удаленный рабочий стол, чтобы увидеть подключения здесь.",
|
||||
"connectedFor": "Подключено для {{duration}}",
|
||||
"connected": "Подключено",
|
||||
"disconnected": "Отключено",
|
||||
"closeTab": "Закрыть вкладку",
|
||||
"closeConnection": "Тесная связь",
|
||||
"forgetTab": "Забывать",
|
||||
"removeBackground": "Удалять",
|
||||
"reconnect": "Переподключитесь",
|
||||
"reopenTab": "Возобновить работу",
|
||||
"sectionOpen": "Открыть",
|
||||
"sectionBackground": "Фон",
|
||||
"backgroundDesc": "После отключения сеанс связи длится 30 минут, после чего его можно повторно подключить.",
|
||||
"persisted": "Сохранялся на заднем плане",
|
||||
"expiresIn": "Срок действия истекает через {{duration}}",
|
||||
"search": "Поиск связей...",
|
||||
"noSearchResults": "Поисковые запросы не соответствуют вашим запросам."
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Подключение к {{type}} сессии...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Повторить"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "УПРАВЛЕНИЕ СЕРВЕРА ССИ",
|
||||
"tagline": "Самостоятельно размещаемые SSH-серверы и системы удаленного управления рабочим столом",
|
||||
"loginTitle": "Войти в Термикс",
|
||||
"registerTitle": "Создать Аккаунт",
|
||||
"forgotPassword": "Забыли пароль?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Выберите количество панелей для отображения",
|
||||
"panesTitle": "Панели",
|
||||
"openTabsTitle": "Открытые вкладки",
|
||||
"dragTabsHint": "Перетащите вкладки на панели сверху",
|
||||
"dragTabsHint": "Перетащите вкладки в панели выше или воспользуйтесь функцией быстрого назначения.",
|
||||
"dropHere": "Перетащите сюда",
|
||||
"emptyPane": "Пусто",
|
||||
"dashboard": "Панель",
|
||||
"clearSplitScreen": "Очистить разделение экрана"
|
||||
"clearSplitScreen": "Очистить разделение экрана",
|
||||
"quickAssign": "Быстрое назначение",
|
||||
"alreadyAssigned": "Панель {{index}}",
|
||||
"splitTab": "Разделить вкладку",
|
||||
"addToSplit": "Добавить в раздел",
|
||||
"removeFromSplit": "Удалить из Сплита",
|
||||
"assignToPane": "Назначить на панель"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Создать сниппет",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Не удалось удалить папку",
|
||||
"folderEditSuccess": "Папка успешно обновлена",
|
||||
"folderEditFailed": "Не удалось обновить папку",
|
||||
"confirmRunMessage": "Запустить \"{{name}}\"?",
|
||||
"confirmRunButton": "Бегать",
|
||||
"runSuccess": "Ранг \"{{name}}\" в терминалах {{count}}",
|
||||
"copySuccess": "Скопировано \"{{name}}\" в буфер обмена",
|
||||
"shareTitle": "Поделиться сниппетом",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Текущий доступ",
|
||||
"shareLoadError": "Не удалось загрузить данные общего доступа",
|
||||
"loading": "Загрузка...",
|
||||
"close": "Закрыть"
|
||||
"close": "Закрыть",
|
||||
"reorderFailed": "Не удалось сохранить порядок фрагментов."
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Аккаунт",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Тема",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Цвет акцента",
|
||||
"settingsFileManager": "Файловый менеджер",
|
||||
"fileColorCoding": "Кодировка цвета файла",
|
||||
"fileColorCodingDesc": "Файлы цветового кода по типу",
|
||||
"settingsTerminal": "Терминал",
|
||||
"commandAutocomplete": "Автозаполнение команды",
|
||||
"commandAutocompleteDesc": "Показывать автозаполнение при вводе текста",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Выделить вывод терминала",
|
||||
"commandPalette": "Палитра команд",
|
||||
"commandPaletteDesc": "Включить горячую клавишу",
|
||||
"sessionPersistence": "Сохранение сессии",
|
||||
"sessionPersistenceDesc": "Сохранять сеансы между повторными подключениями",
|
||||
"reopenTabsOnLogin": "Открывайте вкладки повторно при входе в систему.",
|
||||
"reopenTabsOnLoginDesc": "Восстанавливайте открытые вкладки при входе в систему или обновлении страницы, даже с другого устройства.",
|
||||
"confirmTabClose": "Подтвердить закрытие вкладки",
|
||||
"confirmTabCloseDesc": "Спрашивать перед закрытием вкладок терминала",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Показать теги хоста",
|
||||
"showHostTagsDesc": "Отображать теги в списке узлов",
|
||||
"hostTrayOnClick": "Нажмите, чтобы развернуть действия с хостом.",
|
||||
"hostTrayOnClickDesc": "Для развертывания действий хоста теперь требуется щелчок мышью, а не наведение курсора.",
|
||||
"pinAppRail": "Панель приложений Pin",
|
||||
"pinAppRailDesc": "Левая боковая панель приложения всегда должна быть развернута, а не расширяться при наведении курсора.",
|
||||
"settingsSnippets": "Сниппеты",
|
||||
"foldersCollapsed": "Папки свернуты",
|
||||
"foldersCollapsedDesc": "Свернуть папки по умолчанию",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Исечци",
|
||||
"hostManager": "Менаџер домаћина",
|
||||
"credentials": "Акредитиви",
|
||||
"connections": "Везе",
|
||||
"roleAdministrator": "Администратор",
|
||||
"roleUser": "Корисник"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Осетљивост брзог скроловања",
|
||||
"moshCommandLabel": "Мош команда",
|
||||
"startupSnippetLabel": "Исечак за покретање",
|
||||
"keepaliveIntervalLabel": "Интервал одржавања активности",
|
||||
"keepaliveIntervalLabel": "Интервал одржавања активности (секунде)",
|
||||
"maxKeepaliveMisses": "Максимални промашаји у Keepalive-у",
|
||||
"tunnelSettings": "Подешавања тунела",
|
||||
"enableTunneling": "Омогући тунелирање",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL је копиран",
|
||||
"telnetUrlCopied": "УРЛ адреса Телнета је копирана",
|
||||
"remoteDesktopUrlCopied": "URL удаљене радне површине копиран",
|
||||
"wakeOnLanAction": "Wake on LAN",
|
||||
"wakeOnLanSuccess": "Магични пакет послат на {{name}}",
|
||||
"wakeOnLanError": "Слање магичног пакета није успело",
|
||||
"cloneHostAction": "Клонирај хост",
|
||||
"copyAddress": "Копирај адресу",
|
||||
"copyLink": "Копирај линк",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Ниједан хост не одговара вашој претрази",
|
||||
"hostNotFound": "Хост није пронађен",
|
||||
"searchHosts": "Претражи хостове...",
|
||||
"sortHosts": "Сортирај хостове",
|
||||
"sortDefault": "Подразумевани редослед",
|
||||
"sortNameAsc": "Име (А → Ш)",
|
||||
"sortNameDesc": "Име (Z → A)",
|
||||
"sortIpAsc": "IP адреса (узлазно)",
|
||||
"sortIpDesc": "IP адреса (опис)",
|
||||
"sortOnlineFirst": "Прво онлајн",
|
||||
"sortOfflineFirst": "Прво офлајн",
|
||||
"sortPinnedFirst": "Прво закачено",
|
||||
"filterHosts": "Филтрирај хостове",
|
||||
"filterClearAll": "Обриши филтере",
|
||||
"filterStatusGroup": "Статус",
|
||||
"filterOnline": "Онлајн",
|
||||
"filterOffline": "Офлајн",
|
||||
"filterPinned": "Закачено",
|
||||
"filterAuthGroup": "Тип овлашћења",
|
||||
"filterAuthPassword": "Лозинка",
|
||||
"filterAuthKey": "SSH кључ",
|
||||
"filterAuthCredential": "Акредитив",
|
||||
"filterAuthNone": "Ниједан",
|
||||
"filterAuthOpkssh": "ОПКШ",
|
||||
"filterProtocolGroup": "Протокол",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "РДП",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Телнет",
|
||||
"filterFeaturesGroup": "Карактеристике",
|
||||
"filterFeatureTerminal": "Терминал",
|
||||
"filterFeatureFileManager": "Менаџер датотека",
|
||||
"filterFeatureTunnel": "Тунел",
|
||||
"filterFeatureDocker": "Докер",
|
||||
"filterTagsGroup": "Ознаке",
|
||||
"shareHost": "Дели хост",
|
||||
"shareHostTitle": "Подели: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Окно {{index}} - празно",
|
||||
"noTabAssigned": "Није додељен таб"
|
||||
"noTabAssigned": "Није додељен таб",
|
||||
"focusedPane": "Активни панел"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Нема веза",
|
||||
"noConnectionsDesc": "Отворите терминал, менаџер датотека или удаљену радну површину да бисте видели везе овде",
|
||||
"connectedFor": "Повезано за {{duration}}",
|
||||
"connected": "Повезано",
|
||||
"disconnected": "Искључено",
|
||||
"closeTab": "Затвори картицу",
|
||||
"closeConnection": "Затвори везу",
|
||||
"forgetTab": "Заборави",
|
||||
"removeBackground": "Уклони",
|
||||
"reconnect": "Поново се повежите",
|
||||
"reopenTab": "Поново отвори",
|
||||
"sectionOpen": "Отворено",
|
||||
"sectionBackground": "Позадина",
|
||||
"backgroundDesc": "Сесије трају 30 минута након прекида везе и могу се поново повезати.",
|
||||
"persisted": "Истрајало у позадини",
|
||||
"expiresIn": "Истиче за {{duration}}",
|
||||
"search": "Претражи везе...",
|
||||
"noSearchResults": "Ниједна веза не одговара вашој претрази"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Повезивање са {{type}} сесијом...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Покушај поново"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH МЕНАЏЕР СЕРВЕРА",
|
||||
"tagline": "Самостално хостовано SSH и управљање удаљеном радном површином",
|
||||
"loginTitle": "Пријавите се на Termix",
|
||||
"registerTitle": "Направи налог",
|
||||
"forgotPassword": "Заборавили сте лозинку?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Изаберите колико панела ће се приказивати",
|
||||
"panesTitle": "Оквири",
|
||||
"openTabsTitle": "Отворене картице",
|
||||
"dragTabsHint": "Превуците картице у окна изнад",
|
||||
"dragTabsHint": "Превуците картице у горње окне или користите Брзо додељивање",
|
||||
"dropHere": "Спусти овде",
|
||||
"emptyPane": "Празно",
|
||||
"dashboard": "Контролна табла",
|
||||
"clearSplitScreen": "Обриши подељени екран"
|
||||
"clearSplitScreen": "Обриши подељени екран",
|
||||
"quickAssign": "Брзо додељивање",
|
||||
"alreadyAssigned": "Окно {{index}}",
|
||||
"splitTab": "Подели картицу",
|
||||
"addToSplit": "Додај у Сплит",
|
||||
"removeFromSplit": "Уклони из Сплита",
|
||||
"assignToPane": "Додели окну"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Направи исечак",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Брисање фасцикле није успело",
|
||||
"folderEditSuccess": "Фолдер је успешно ажуриран",
|
||||
"folderEditFailed": "Ажурирање фасцикле није успело",
|
||||
"confirmRunMessage": "Покрени „{{name}}“?",
|
||||
"confirmRunButton": "Трчи",
|
||||
"runSuccess": "Покренуо је „{{name}}“ у {{count}} терминалу(има)",
|
||||
"copySuccess": "Копирано „{{name}}“ у међуспремник",
|
||||
"shareTitle": "Дели исечак",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Тренутни приступ",
|
||||
"shareLoadError": "Учитавање података о дељењу није успело",
|
||||
"loading": "Учитавање...",
|
||||
"close": "Затвори"
|
||||
"close": "Затвори",
|
||||
"reorderFailed": "Чување редоследа исечака није успело"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Налог",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Тема",
|
||||
"fontSizeLabel": "Величина фонта",
|
||||
"accentColorLabel": "Акцентна боја",
|
||||
"settingsFileManager": "Менаџер датотека",
|
||||
"fileColorCoding": "Кодирање бојама датотека",
|
||||
"fileColorCodingDesc": "Означите датотеке бојама по типу",
|
||||
"settingsTerminal": "Терминал",
|
||||
"commandAutocomplete": "Аутоматско довршавање команди",
|
||||
"commandAutocompleteDesc": "Прикажи аутоматско довршавање током куцања",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Истакните излаз терминала",
|
||||
"commandPalette": "Палета команди",
|
||||
"commandPaletteDesc": "Омогући пречицу на тастатури",
|
||||
"sessionPersistence": "Трајност сесије",
|
||||
"sessionPersistenceDesc": "Одржавајте сесије између поновних повезивања",
|
||||
"reopenTabsOnLogin": "Поново отвори картице при пријављивању",
|
||||
"reopenTabsOnLoginDesc": "Вратите отворене картице када се пријавите или освежите страницу, чак и са другог уређаја",
|
||||
"confirmTabClose": "Потврди затварање картице",
|
||||
"confirmTabCloseDesc": "Питај пре затварања картица терминала",
|
||||
"settingsSidebar": "Бочна трака",
|
||||
"showHostTags": "Прикажи ознаке хоста",
|
||||
"showHostTagsDesc": "Приказ ознака на листи хостова",
|
||||
"hostTrayOnClick": "Кликните да проширите Акције хоста",
|
||||
"hostTrayOnClickDesc": "Захтевајте клик за проширивање радњи хоста уместо преласка курсором миша",
|
||||
"pinAppRail": "Закачи шину апликација",
|
||||
"pinAppRailDesc": "Нека лева бочна трака апликације увек буде проширена уместо да се проширује при преласку мишем преко ње",
|
||||
"settingsSnippets": "Исечци",
|
||||
"foldersCollapsed": "Фасцикле су склопљене",
|
||||
"foldersCollapsedDesc": "Сажми фасцикле подразумевано",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Textmoduler",
|
||||
"hostManager": "Värdhanterare",
|
||||
"credentials": "Användaruppgifter",
|
||||
"connections": "Anslutningar",
|
||||
"roleAdministrator": "Administratör",
|
||||
"roleUser": "Användare"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Snabb rullningskänslighet",
|
||||
"moshCommandLabel": "Mosh Kommando",
|
||||
"startupSnippetLabel": "Starta textmodulen",
|
||||
"keepaliveIntervalLabel": "Håll tidsintervall",
|
||||
"keepaliveIntervalLabel": "Keepalive-intervall (sekunder)",
|
||||
"maxKeepaliveMisses": "Max antal missar",
|
||||
"tunnelSettings": "Inställningar för tunneln",
|
||||
"enableTunneling": "Aktivera tunnel",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC-URL kopierad",
|
||||
"telnetUrlCopied": "Telnet URL kopierad",
|
||||
"remoteDesktopUrlCopied": "URL för fjärrskrivbord har kopierats",
|
||||
"wakeOnLanAction": "Vakna på LAN",
|
||||
"wakeOnLanSuccess": "Magiskt paket skickat till {{name}}",
|
||||
"wakeOnLanError": "Misslyckades med att skicka magiskt paket",
|
||||
"cloneHostAction": "Klona Värd",
|
||||
"copyAddress": "Kopiera adress",
|
||||
"copyLink": "Kopiera länk",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Inga värdar matchar din sökning",
|
||||
"hostNotFound": "Värden hittades inte",
|
||||
"searchHosts": "Sök värdar...",
|
||||
"sortHosts": "Sortera värdar",
|
||||
"sortDefault": "Standardbeställning",
|
||||
"sortNameAsc": "Namn (A → Ö)",
|
||||
"sortNameDesc": "Namn (Ö → A)",
|
||||
"sortIpAsc": "IP-adress (stigande)",
|
||||
"sortIpDesc": "IP-adress (beskrivning)",
|
||||
"sortOnlineFirst": "Online först",
|
||||
"sortOfflineFirst": "Offline först",
|
||||
"sortPinnedFirst": "Fäst först",
|
||||
"filterHosts": "Filtrera värdar",
|
||||
"filterClearAll": "Rensa filter",
|
||||
"filterStatusGroup": "Status",
|
||||
"filterOnline": "Online",
|
||||
"filterOffline": "Off-line",
|
||||
"filterPinned": "Fäst",
|
||||
"filterAuthGroup": "Aut.typ",
|
||||
"filterAuthPassword": "Lösenord",
|
||||
"filterAuthKey": "SSH-nyckel",
|
||||
"filterAuthCredential": "Referenser",
|
||||
"filterAuthNone": "Ingen",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protokoll",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "Landsbygdsutveckling",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Drag",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Filhanteraren",
|
||||
"filterFeatureTunnel": "Tunnel",
|
||||
"filterFeatureDocker": "Hamnarbetare",
|
||||
"filterTagsGroup": "Taggar",
|
||||
"shareHost": "Dela värd",
|
||||
"shareHostTitle": "Dela: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Ingen flik tilldelad"
|
||||
"noTabAssigned": "Ingen flik tilldelad",
|
||||
"focusedPane": "Aktiv ruta"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Inga anslutningar",
|
||||
"noConnectionsDesc": "Öppna en terminal, filhanterare eller fjärrskrivbord för att se anslutningar här",
|
||||
"connectedFor": "Ansluten för {{duration}}",
|
||||
"connected": "Ansluten",
|
||||
"disconnected": "Osammanhängande",
|
||||
"closeTab": "Stäng fliken",
|
||||
"closeConnection": "Stäng anslutning",
|
||||
"forgetTab": "Glömma",
|
||||
"removeBackground": "Ta bort",
|
||||
"reconnect": "Återanslut",
|
||||
"reopenTab": "Öppna igen",
|
||||
"sectionOpen": "Öppna",
|
||||
"sectionBackground": "Bakgrund",
|
||||
"backgroundDesc": "Sessioner kvarstår i 30 minuter efter frånkoppling och kan återanslutas.",
|
||||
"persisted": "Fortsatte i bakgrunden",
|
||||
"expiresIn": "Utgår om {{duration}}",
|
||||
"search": "Sök efter kopplingar...",
|
||||
"noSearchResults": "Inga kontakter matchar din sökning"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Ansluter till {{type}} session...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Försök igen"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH SERVER MANAGER",
|
||||
"tagline": "Självhostad SSH och fjärrskrivbordshantering",
|
||||
"loginTitle": "Logga in till Termix",
|
||||
"registerTitle": "Skapa konto",
|
||||
"forgotPassword": "Glömt lösenordet?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Välj hur många rutor som ska visas",
|
||||
"panesTitle": "Paneler",
|
||||
"openTabsTitle": "Öppna flikar",
|
||||
"dragTabsHint": "Dra flikar i rutorna ovanför",
|
||||
"dragTabsHint": "Dra flikar till rutor ovan eller använd Snabbtilldelning",
|
||||
"dropHere": "Släpp här",
|
||||
"emptyPane": "Tom",
|
||||
"dashboard": "Instrumentpanel",
|
||||
"clearSplitScreen": "Rensa delad skärm"
|
||||
"clearSplitScreen": "Rensa delad skärm",
|
||||
"quickAssign": "Snabbtilldelning",
|
||||
"alreadyAssigned": "Ruta {{index}}",
|
||||
"splitTab": "Dela fliken",
|
||||
"addToSplit": "Lägg till i delning",
|
||||
"removeFromSplit": "Ta bort från Split",
|
||||
"assignToPane": "Tilldela till ruta"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Skapa textmodulen",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Kunde inte ta bort mapp",
|
||||
"folderEditSuccess": "Mappen har uppdaterats",
|
||||
"folderEditFailed": "Det gick inte att uppdatera mappen",
|
||||
"confirmRunMessage": "Kör \"{{name}}\"?",
|
||||
"confirmRunButton": "Sikt",
|
||||
"runSuccess": "Ran \"{{name}}\" i {{count}} terminal(er)",
|
||||
"copySuccess": "Kopierade \"{{name}}\" till urklipp",
|
||||
"shareTitle": "Dela textmodulen",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Aktuell åtkomst",
|
||||
"shareLoadError": "Misslyckades att ladda delningsdata",
|
||||
"loading": "Laddar...",
|
||||
"close": "Stäng"
|
||||
"close": "Stäng",
|
||||
"reorderFailed": "Det gick inte att spara kodavsnittets ordning"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Konto",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Tema",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Accentfärg",
|
||||
"settingsFileManager": "Filhanterare",
|
||||
"fileColorCoding": "Filfärg kodning",
|
||||
"fileColorCodingDesc": "Färgkod filer efter typ",
|
||||
"settingsTerminal": "Terminal",
|
||||
"commandAutocomplete": "Kommandot Autokomplettering",
|
||||
"commandAutocompleteDesc": "Visa autocomplete medan du skriver",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Markera terminalutgång",
|
||||
"commandPalette": "Kommando palett",
|
||||
"commandPaletteDesc": "Aktivera kortkommando",
|
||||
"sessionPersistence": "Sessionens uthållighet",
|
||||
"sessionPersistenceDesc": "Behåll sessioner mellan återanslutningar",
|
||||
"reopenTabsOnLogin": "Öppna flikar igen vid inloggning",
|
||||
"reopenTabsOnLoginDesc": "Återställ dina öppna flikar när du loggar in eller uppdaterar sidan, även från en annan enhet",
|
||||
"confirmTabClose": "Bekräfta flikstängning",
|
||||
"confirmTabCloseDesc": "Fråga innan terminalflikar stängs",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Visa värdtaggar",
|
||||
"showHostTagsDesc": "Visa taggar i värdlistan",
|
||||
"hostTrayOnClick": "Klicka för att expandera värdåtgärder",
|
||||
"hostTrayOnClickDesc": "Kräv ett klick för att expandera värdåtgärder istället för att hålla muspekaren över",
|
||||
"pinAppRail": "Pin App Rail",
|
||||
"pinAppRailDesc": "Håll applist i vänster sidofält alltid expanderad istället för att expandera när du håller muspekaren över den",
|
||||
"settingsSnippets": "Textmoduler",
|
||||
"foldersCollapsed": "Mappar kollapsade",
|
||||
"foldersCollapsedDesc": "Kollapsa mappar som standard",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "เศษเสี้ยว",
|
||||
"hostManager": "ผู้จัดการโฮสต์",
|
||||
"credentials": "คุณสมบัติ",
|
||||
"connections": "การเชื่อมต่อ",
|
||||
"roleAdministrator": "ผู้ดูแลระบบ",
|
||||
"roleUser": "ผู้ใช้"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "ความไวในการเลื่อนเร็ว",
|
||||
"moshCommandLabel": "ม็อช คอมมานโด",
|
||||
"startupSnippetLabel": "ตัวอย่างโค้ดสำหรับเริ่มต้น",
|
||||
"keepaliveIntervalLabel": "ช่วงเวลาการเชื่อมต่อ",
|
||||
"keepaliveIntervalLabel": "ช่วงเวลาการเชื่อมต่อ (วินาที)",
|
||||
"maxKeepaliveMisses": "แม็กซ์ คีทอะไลฟ์ พลาด",
|
||||
"tunnelSettings": "การตั้งค่าอุโมงค์",
|
||||
"enableTunneling": "เปิดใช้งานการสร้างอุโมงค์",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "คัดลอก URL ของ VNC แล้ว",
|
||||
"telnetUrlCopied": "คัดลอก URL ของ Telnet แล้ว",
|
||||
"remoteDesktopUrlCopied": "คัดลอก URL ของเดสก์ท็อประยะไกลแล้ว",
|
||||
"wakeOnLanAction": "เปิดใช้งานผ่าน LAN",
|
||||
"wakeOnLanSuccess": "แพ็กเก็ตเวทมนตร์ถูกส่งไปยัง {{name}}",
|
||||
"wakeOnLanError": "ส่งแพ็กเก็ตเวทมนตร์ไม่สำเร็จ",
|
||||
"cloneHostAction": "โคลนโฮสต์",
|
||||
"copyAddress": "คัดลอกที่อยู่",
|
||||
"copyLink": "คัดลอกลิงก์",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "ไม่พบโฮสต์ใดตรงกับผลการค้นหาของคุณ",
|
||||
"hostNotFound": "ไม่พบโฮสต์",
|
||||
"searchHosts": "ค้นหาโฮสต์...",
|
||||
"sortHosts": "เรียงลำดับโฮสต์",
|
||||
"sortDefault": "ลำดับเริ่มต้น",
|
||||
"sortNameAsc": "ชื่อ (A → Z)",
|
||||
"sortNameDesc": "ชื่อ (Z → A)",
|
||||
"sortIpAsc": "ที่อยู่ IP (จากน้อยไปมาก)",
|
||||
"sortIpDesc": "ที่อยู่ IP (คำอธิบาย)",
|
||||
"sortOnlineFirst": "ออนไลน์ก่อน",
|
||||
"sortOfflineFirst": "ออฟไลน์ก่อน",
|
||||
"sortPinnedFirst": "ปักหมุดไว้ก่อน",
|
||||
"filterHosts": "ตัวกรองโฮสต์",
|
||||
"filterClearAll": "ล้างตัวกรอง",
|
||||
"filterStatusGroup": "สถานะ",
|
||||
"filterOnline": "ออนไลน์",
|
||||
"filterOffline": "ออฟไลน์",
|
||||
"filterPinned": "ปักหมุด",
|
||||
"filterAuthGroup": "ประเภทการตรวจสอบสิทธิ์",
|
||||
"filterAuthPassword": "รหัสผ่าน",
|
||||
"filterAuthKey": "คีย์ SSH",
|
||||
"filterAuthCredential": "ใบรับรอง",
|
||||
"filterAuthNone": "ไม่มี",
|
||||
"filterAuthOpkssh": "โอพีเคเอสเอช",
|
||||
"filterProtocolGroup": "โปรโตคอล",
|
||||
"filterProtocolSsh": "เอสเอช",
|
||||
"filterProtocolRdp": "อาร์ดีพี",
|
||||
"filterProtocolVnc": "วีเอ็นซี",
|
||||
"filterProtocolTelnet": "เทลเน็ต",
|
||||
"filterFeaturesGroup": "คุณสมบัติ",
|
||||
"filterFeatureTerminal": "เทอร์มินัล",
|
||||
"filterFeatureFileManager": "ตัวจัดการไฟล์",
|
||||
"filterFeatureTunnel": "อุโมงค์",
|
||||
"filterFeatureDocker": "ด็อกเกอร์",
|
||||
"filterTagsGroup": "แท็ก",
|
||||
"shareHost": "แชร์โฮสต์",
|
||||
"shareHostTitle": "แชร์: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "บานหน้าต่าง {{index}} - ว่างเปล่า",
|
||||
"noTabAssigned": "ไม่มีการกำหนดแท็บ"
|
||||
"noTabAssigned": "ไม่มีการกำหนดแท็บ",
|
||||
"focusedPane": "หน้าต่างที่ใช้งานอยู่"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "ไม่มีการเชื่อมต่อ",
|
||||
"noConnectionsDesc": "เปิดเทอร์มินัล โปรแกรมจัดการไฟล์ หรือเดสก์ท็อประยะไกล เพื่อดูการเชื่อมต่อได้ที่นี่",
|
||||
"connectedFor": "เชื่อมต่อเป็นเวลา {{duration}}",
|
||||
"connected": "เชื่อมต่อแล้ว",
|
||||
"disconnected": "ตัดการเชื่อมต่อ",
|
||||
"closeTab": "ปิดแท็บ",
|
||||
"closeConnection": "ความสัมพันธ์ใกล้ชิด",
|
||||
"forgetTab": "ลืม",
|
||||
"removeBackground": "ลบ",
|
||||
"reconnect": "เชื่อมต่อใหม่",
|
||||
"reopenTab": "เปิดใหม่",
|
||||
"sectionOpen": "เปิด",
|
||||
"sectionBackground": "พื้นหลัง",
|
||||
"backgroundDesc": "การเชื่อมต่อจะยังคงอยู่เป็นเวลา 30 นาทีหลังจากตัดการเชื่อมต่อ และสามารถเชื่อมต่อใหม่ได้",
|
||||
"persisted": "ยังคงอยู่ในพื้นหลัง",
|
||||
"expiresIn": "หมดอายุใน {{duration}}",
|
||||
"search": "ค้นหาการเชื่อมต่อ...",
|
||||
"noSearchResults": "ไม่พบผลลัพธ์ที่ตรงกับการค้นหาของคุณ"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "กำลังเชื่อมต่อกับเซสชัน {{type}}...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "ลองใหม่อีกครั้ง"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "ตัวจัดการเซิร์ฟเวอร์ SSH",
|
||||
"tagline": "การจัดการ SSH และเดสก์ท็อประยะไกลแบบโฮสต์เอง",
|
||||
"loginTitle": "เข้าสู่ระบบ Termix",
|
||||
"registerTitle": "สร้างบัญชี",
|
||||
"forgotPassword": "ลืมรหัสผ่านใช่ไหม?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "เลือกจำนวนบานหน้าต่างที่จะแสดง",
|
||||
"panesTitle": "บานหน้าต่าง",
|
||||
"openTabsTitle": "แท็บที่เปิดอยู่",
|
||||
"dragTabsHint": "ลากแท็บลงในบานหน้าต่างด้านบน",
|
||||
"dragTabsHint": "ลากแท็บลงในบานหน้าต่างด้านบน หรือใช้ฟังก์ชันกำหนดด่วน",
|
||||
"dropHere": "ปล่อยตรงนี้",
|
||||
"emptyPane": "ว่างเปล่า",
|
||||
"dashboard": "แดชบอร์ด",
|
||||
"clearSplitScreen": "ล้างหน้าจอแยก"
|
||||
"clearSplitScreen": "ล้างหน้าจอแยก",
|
||||
"quickAssign": "มอบหมายอย่างรวดเร็ว",
|
||||
"alreadyAssigned": "บานหน้าต่าง {{index}}",
|
||||
"splitTab": "แท็บแยก",
|
||||
"addToSplit": "เพิ่มลงในการแบ่ง",
|
||||
"removeFromSplit": "ลบออกจากการแบ่ง",
|
||||
"assignToPane": "กำหนดให้กับบานหน้าต่าง"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "สร้างโค้ดสั้น",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "ไม่สามารถลบโฟลเดอร์ได้",
|
||||
"folderEditSuccess": "อัปเดตโฟลเดอร์สำเร็จแล้ว",
|
||||
"folderEditFailed": "ไม่สามารถอัปเดตโฟลเดอร์ได้",
|
||||
"confirmRunMessage": "เรียกใช้ \"{{name}}\"?",
|
||||
"confirmRunButton": "วิ่ง",
|
||||
"runSuccess": "Ran \"{{name}}\" ในเทอร์มินัล {{count}} เครื่อง",
|
||||
"copySuccess": "คัดลอก \"{{name}}\" ไปยังคลิปบอร์ด",
|
||||
"shareTitle": "แชร์ตัวอย่าง",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "การเข้าถึงปัจจุบัน",
|
||||
"shareLoadError": "ไม่สามารถโหลดข้อมูลการแชร์ได้",
|
||||
"loading": "กำลังโหลด...",
|
||||
"close": "ปิด"
|
||||
"close": "ปิด",
|
||||
"reorderFailed": "ไม่สามารถบันทึกลำดับของโค้ดตัวอย่างได้"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "บัญชี",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "ธีม",
|
||||
"fontSizeLabel": "ขนาดตัวอักษร",
|
||||
"accentColorLabel": "สีเน้น",
|
||||
"settingsFileManager": "ตัวจัดการไฟล์",
|
||||
"fileColorCoding": "การกำหนดรหัสสีไฟล์",
|
||||
"fileColorCodingDesc": "กำหนดรหัสสีให้กับไฟล์ตามประเภท",
|
||||
"settingsTerminal": "เทอร์มินัล",
|
||||
"commandAutocomplete": "การเติมคำสั่งอัตโนมัติ",
|
||||
"commandAutocompleteDesc": "แสดงคำแนะนำอัตโนมัติขณะพิมพ์",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "เน้นเอาต์พุตเทอร์มินัล",
|
||||
"commandPalette": "พาเลทคำสั่ง",
|
||||
"commandPaletteDesc": "เปิดใช้งานปุ่มลัดแป้นพิมพ์",
|
||||
"sessionPersistence": "การคงอยู่ของเซสชัน",
|
||||
"sessionPersistenceDesc": "ควรเว้นช่วงเวลาการเชื่อมต่อแต่ละครั้งให้นานที่สุด",
|
||||
"reopenTabsOnLogin": "เปิดแท็บใหม่เมื่อเข้าสู่ระบบ",
|
||||
"reopenTabsOnLoginDesc": "กู้คืนแท็บที่เปิดอยู่ของคุณเมื่อคุณเข้าสู่ระบบหรือรีเฟรชหน้าเว็บ แม้กระทั่งจากอุปกรณ์อื่น",
|
||||
"confirmTabClose": "ยืนยันการปิดแท็บ",
|
||||
"confirmTabCloseDesc": "ถามก่อนปิดแท็บเทอร์มินัล",
|
||||
"settingsSidebar": "แถบด้านข้าง",
|
||||
"showHostTags": "แสดงแท็กโฮสต์",
|
||||
"showHostTagsDesc": "แสดงแท็กในรายการโฮสต์",
|
||||
"hostTrayOnClick": "คลิกเพื่อขยายการดำเนินการของโฮสต์",
|
||||
"hostTrayOnClickDesc": "ต้องคลิกเพื่อขยายการดำเนินการของโฮสต์ แทนการวางเมาส์เหนือวัตถุ",
|
||||
"pinAppRail": "พินแอปเรล",
|
||||
"pinAppRailDesc": "ควรตั้งค่าให้แถบด้านข้างซ้ายของแอปแสดงอยู่ตลอดเวลาโดยไม่ต้องขยายเมื่อวางเมาส์เหนือแถบ",
|
||||
"settingsSnippets": "เศษเสี้ยว",
|
||||
"foldersCollapsed": "โฟลเดอร์ถูกยุบ",
|
||||
"foldersCollapsedDesc": "ยุบโฟลเดอร์โดยค่าเริ่มต้น",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Kısa bölümler",
|
||||
"hostManager": "Sunucu Yöneticisi",
|
||||
"credentials": "Kimlik Bilgileri",
|
||||
"connections": "Bağlantılar",
|
||||
"roleAdministrator": "Yönetici",
|
||||
"roleUser": "Kullanıcı"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Hızlı Kaydırma Hassasiyeti",
|
||||
"moshCommandLabel": "Mosh Komutanlığı",
|
||||
"startupSnippetLabel": "Başlangıç Kodu Parçası",
|
||||
"keepaliveIntervalLabel": "Keepalive Aralığı",
|
||||
"keepaliveIntervalLabel": "Keepalive Aralığı (saniye)",
|
||||
"maxKeepaliveMisses": "Max Keepalive'ı kaçırdı",
|
||||
"tunnelSettings": "Tünel Ayarları",
|
||||
"enableTunneling": "Tünelleme özelliğini etkinleştirin",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL'si kopyalandı",
|
||||
"telnetUrlCopied": "Telnet URL'si kopyalandı",
|
||||
"remoteDesktopUrlCopied": "Uzak Masaüstü URL'si kopyalandı",
|
||||
"wakeOnLanAction": "Wake on LAN",
|
||||
"wakeOnLanSuccess": "{{name}} adresine sihirli paket gönderildi.",
|
||||
"wakeOnLanError": "Sihirli paket gönderilemedi.",
|
||||
"cloneHostAction": "Klon Ana Bilgisayar",
|
||||
"copyAddress": "Adresi Kopyala",
|
||||
"copyLink": "Bağlantıyı Kopyala",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Arama kriterlerinize uyan ev sahibi bulunamadı.",
|
||||
"hostNotFound": "Sunucu bulunamadı.",
|
||||
"searchHosts": "Sunucu ara...",
|
||||
"sortHosts": "Sunucuları Sırala",
|
||||
"sortDefault": "Varsayılan Sipariş",
|
||||
"sortNameAsc": "İsim (A → Z)",
|
||||
"sortNameDesc": "İsim (Z → A)",
|
||||
"sortIpAsc": "IP Adresi (Artan)",
|
||||
"sortIpDesc": "IP Adresi (Açıklama)",
|
||||
"sortOnlineFirst": "Çevrimiçi Öncelikli",
|
||||
"sortOfflineFirst": "Çevrimdışı Önce",
|
||||
"sortPinnedFirst": "İlk olarak sabitlendi",
|
||||
"filterHosts": "Sunucuları Filtrele",
|
||||
"filterClearAll": "Filtreleri Temizle",
|
||||
"filterStatusGroup": "Durum",
|
||||
"filterOnline": "Çevrimiçi",
|
||||
"filterOffline": "Çevrimdışı",
|
||||
"filterPinned": "Sabitlendi",
|
||||
"filterAuthGroup": "Kimlik Doğrulama Türü",
|
||||
"filterAuthPassword": "Şifre",
|
||||
"filterAuthKey": "SSH Anahtarı",
|
||||
"filterAuthCredential": "Kimlik belgesi",
|
||||
"filterAuthNone": "Hiçbiri",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Protokol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Özellikler",
|
||||
"filterFeatureTerminal": "terminal",
|
||||
"filterFeatureFileManager": "Dosya Yöneticisi",
|
||||
"filterFeatureTunnel": "Tünel",
|
||||
"filterFeatureDocker": "Liman işçisi",
|
||||
"filterTagsGroup": "Etiketler",
|
||||
"shareHost": "Paylaşımlı Sunucu",
|
||||
"shareHostTitle": "Paylaş: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Bölme {{index}} - boş",
|
||||
"noTabAssigned": "Hiçbir sekme atanmadı."
|
||||
"noTabAssigned": "Hiçbir sekme atanmadı.",
|
||||
"focusedPane": "Aktif bölme"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Bağlantı yok.",
|
||||
"noConnectionsDesc": "Buradaki bağlantıları görmek için bir terminal, dosya yöneticisi veya uzak masaüstü açın.",
|
||||
"connectedFor": "{{duration}} için bağlantı kuruldu.",
|
||||
"connected": "Bağlı",
|
||||
"disconnected": "Bağlantı kesildi",
|
||||
"closeTab": "Sekmeyi kapat",
|
||||
"closeConnection": "Yakın bağlantı",
|
||||
"forgetTab": "Unutmak",
|
||||
"removeBackground": "Kaldırmak",
|
||||
"reconnect": "Yeniden bağlan",
|
||||
"reopenTab": "Yeniden açın",
|
||||
"sectionOpen": "Açık",
|
||||
"sectionBackground": "Arka plan",
|
||||
"backgroundDesc": "Bağlantı kesildikten sonra oturumlar 30 dakika boyunca devam eder ve yeniden bağlanabilir.",
|
||||
"persisted": "Arka planda devam etti",
|
||||
"expiresIn": "{{duration}} içinde sona eriyor",
|
||||
"search": "Arama bağlantıları...",
|
||||
"noSearchResults": "Arama kriterlerinize uyan bağlantı bulunamadı."
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "{{type}} oturumuna bağlanılıyor...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Tekrar dene"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH SUNUCU YÖNETİCİSİ",
|
||||
"tagline": "Kendi sunucunuzda barındırılan SSH ve uzaktan masaüstü yönetimi",
|
||||
"loginTitle": "Termix'e giriş yapın",
|
||||
"registerTitle": "Hesap oluşturmak",
|
||||
"forgotPassword": "Parolanızı mı unuttunuz?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Görüntülenecek bölme sayısını seçin.",
|
||||
"panesTitle": "Camlar",
|
||||
"openTabsTitle": "Açık Sekmeler",
|
||||
"dragTabsHint": "Sekmeleri yukarıdaki bölmelere sürükleyin.",
|
||||
"dragTabsHint": "Sekmeleri yukarıdaki bölmelere sürükleyin veya Hızlı Atama özelliğini kullanın.",
|
||||
"dropHere": "Buraya bırakın",
|
||||
"emptyPane": "Boş",
|
||||
"dashboard": "Kontrol Paneli",
|
||||
"clearSplitScreen": "Ekranı Bölünmüş Hale Getir"
|
||||
"clearSplitScreen": "Ekranı Bölünmüş Hale Getir",
|
||||
"quickAssign": "Hızlı Atama",
|
||||
"alreadyAssigned": "Bölme {{index}}",
|
||||
"splitTab": "Bölünmüş Sekme",
|
||||
"addToSplit": "Bölünmüş bölüme ekle",
|
||||
"removeFromSplit": "Bölünmüş halden çıkarın",
|
||||
"assignToPane": "Bölmeye ata"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Kod Parçası Oluştur",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Klasör silme işlemi başarısız oldu.",
|
||||
"folderEditSuccess": "Klasör başarıyla güncellendi.",
|
||||
"folderEditFailed": "Klasör güncellemesi başarısız oldu.",
|
||||
"confirmRunMessage": "\"{{name}} \" çalıştır?",
|
||||
"confirmRunButton": "Koşmak",
|
||||
"runSuccess": "\"{{name}}\" ifadesi {{count}} terminal(ler)inde çalıştırıldı",
|
||||
"copySuccess": "\"{{name}}\" panoya kopyalandı",
|
||||
"shareTitle": "Paylaşılan Parça",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Mevcut Erişim",
|
||||
"shareLoadError": "Paylaşılan veriler yüklenemedi.",
|
||||
"loading": "Yükleniyor...",
|
||||
"close": "Kapalı"
|
||||
"close": "Kapalı",
|
||||
"reorderFailed": "Kod parçacığının sırasını kaydetme başarısız oldu."
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Hesap",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Tema",
|
||||
"fontSizeLabel": "Yazı Tipi Boyutu",
|
||||
"accentColorLabel": "Vurgu Rengi",
|
||||
"settingsFileManager": "Dosya Yöneticisi",
|
||||
"fileColorCoding": "Dosya Renk Kodlaması",
|
||||
"fileColorCodingDesc": "Dosyaları türlerine göre renk kodlarıyla işaretleyin.",
|
||||
"settingsTerminal": "terminal",
|
||||
"commandAutocomplete": "Komut Otomatik Tamamlama",
|
||||
"commandAutocompleteDesc": "Yazarken otomatik tamamlama özelliğini göster",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Terminal çıktısını vurgula",
|
||||
"commandPalette": "Komut Paleti",
|
||||
"commandPaletteDesc": "Klavye kısayolunu etkinleştir",
|
||||
"sessionPersistence": "Oturum Kalıcılığı",
|
||||
"sessionPersistenceDesc": "Yeniden bağlantılar arasındaki oturumları sürdürün.",
|
||||
"reopenTabsOnLogin": "Giriş yaptıktan sonra sekmeleri yeniden açın",
|
||||
"reopenTabsOnLoginDesc": "Başka bir cihazdan giriş yapsanız bile, oturum açtığınızda veya sayfayı yenilediğinizde açık sekmelerinizi geri yükleyin.",
|
||||
"confirmTabClose": "Sekmeyi Kapatmayı Onayla",
|
||||
"confirmTabCloseDesc": "Terminal sekmelerini kapatmadan önce izin isteyin.",
|
||||
"settingsSidebar": "Kenar Çubuğu",
|
||||
"showHostTags": "Sunucu Etiketlerini Göster",
|
||||
"showHostTagsDesc": "Ana bilgisayar listesinde etiketleri görüntüle",
|
||||
"hostTrayOnClick": "Sunucu İşlemlerini Genişletmek için Tıklayın",
|
||||
"hostTrayOnClickDesc": "Ana bilgisayar eylemlerini genişletmek için fareyle üzerine gelmek yerine tıklamak gerekir.",
|
||||
"pinAppRail": "Pin Uygulama Rayını",
|
||||
"pinAppRailDesc": "Sol kenar çubuğundaki uygulama çubuğunu fareyle üzerine gelindiğinde genişletmek yerine her zaman açık tutun.",
|
||||
"settingsSnippets": "Kısa bölümler",
|
||||
"foldersCollapsed": "Klasörler Daraltıldı",
|
||||
"foldersCollapsedDesc": "Klasörleri varsayılan olarak daralt",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Сніпети",
|
||||
"hostManager": "Менеджер хостів",
|
||||
"credentials": "Дані доступу",
|
||||
"connections": "З'єднання",
|
||||
"roleAdministrator": "Адміністратор",
|
||||
"roleUser": "Користувач"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Чутливість швидкого прокручування",
|
||||
"moshCommandLabel": "Командування Мосха",
|
||||
"startupSnippetLabel": "Сніпет запуску",
|
||||
"keepaliveIntervalLabel": "Інтервал перевірки активності",
|
||||
"keepaliveIntervalLabel": "Інтервал підтримки активності (секунди)",
|
||||
"maxKeepaliveMisses": "Максимальна кількість пропусків Keepalive",
|
||||
"tunnelSettings": "Налаштування тунелю",
|
||||
"enableTunneling": "Увімкнути тунелювання",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL скопійовано",
|
||||
"telnetUrlCopied": "URL Telnet скопійовано",
|
||||
"remoteDesktopUrlCopied": "URL віддаленого робочого столу скопійовано",
|
||||
"wakeOnLanAction": "Пробудження по локальній мережі",
|
||||
"wakeOnLanSuccess": "Чарівний пакет надіслано на {{name}}",
|
||||
"wakeOnLanError": "Не вдалося надіслати магічний пакет",
|
||||
"cloneHostAction": "Клонувати хост",
|
||||
"copyAddress": "Копіювати адресу",
|
||||
"copyLink": "Копіювати посилання",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Немає хостів, які відповідають вашому пошуку",
|
||||
"hostNotFound": "Хост не знайдено",
|
||||
"searchHosts": "Шукати...",
|
||||
"sortHosts": "Сортувати хости",
|
||||
"sortDefault": "Порядок за замовчуванням",
|
||||
"sortNameAsc": "Ім'я (А → Я)",
|
||||
"sortNameDesc": "Ім'я (Я → А)",
|
||||
"sortIpAsc": "IP-адреса (зростання)",
|
||||
"sortIpDesc": "IP-адреса (опис)",
|
||||
"sortOnlineFirst": "Спочатку онлайн",
|
||||
"sortOfflineFirst": "Спочатку офлайн",
|
||||
"sortPinnedFirst": "Закріплено першим",
|
||||
"filterHosts": "Фільтрувати хости",
|
||||
"filterClearAll": "Очистити фільтри",
|
||||
"filterStatusGroup": "Статус",
|
||||
"filterOnline": "Онлайн",
|
||||
"filterOffline": "Офлайн",
|
||||
"filterPinned": "Закріплено",
|
||||
"filterAuthGroup": "Тип автентифікації",
|
||||
"filterAuthPassword": "Пароль",
|
||||
"filterAuthKey": "SSH-ключ",
|
||||
"filterAuthCredential": "Посвідчення",
|
||||
"filterAuthNone": "Жоден",
|
||||
"filterAuthOpkssh": "ОПКШ",
|
||||
"filterProtocolGroup": "Протокол",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "ПРП",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Телнет",
|
||||
"filterFeaturesGroup": "Особливості",
|
||||
"filterFeatureTerminal": "Термінал",
|
||||
"filterFeatureFileManager": "Файловий менеджер",
|
||||
"filterFeatureTunnel": "Тунель",
|
||||
"filterFeatureDocker": "Докер",
|
||||
"filterTagsGroup": "Теги",
|
||||
"shareHost": "Поділитися хостом",
|
||||
"shareHostTitle": "Поділитися: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "Жодної вкладки не призначено"
|
||||
"noTabAssigned": "Жодної вкладки не призначено",
|
||||
"focusedPane": "Активна панель"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Немає з'єднань",
|
||||
"noConnectionsDesc": "Відкрийте термінал, файловий менеджер або віддалений робочий стіл, щоб побачити підключення тут",
|
||||
"connectedFor": "Підключено для {{duration}}",
|
||||
"connected": "Підключено",
|
||||
"disconnected": "Відключено",
|
||||
"closeTab": "Закрити вкладку",
|
||||
"closeConnection": "Закрити з'єднання",
|
||||
"forgetTab": "Забути",
|
||||
"removeBackground": "Видалити",
|
||||
"reconnect": "Знову підключитися",
|
||||
"reopenTab": "Знову відкрити",
|
||||
"sectionOpen": "ВІДЧИНЕНО",
|
||||
"sectionBackground": "Передісторія",
|
||||
"backgroundDesc": "Сеанси тривають 30 хвилин після відключення та можуть бути підключені знову.",
|
||||
"persisted": "Зберігається у фоновому режимі",
|
||||
"expiresIn": "Термін дії закінчується через {{duration}}",
|
||||
"search": "Пошук з'єднань...",
|
||||
"noSearchResults": "Немає з'єднань, що відповідають вашому пошуку"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Підключення до сеансу {{type}}...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Повторити спробу"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH КЕРУВАННЯ СЕРВЕР",
|
||||
"tagline": "Власне керування SSH та віддаленим робочим столом",
|
||||
"loginTitle": "Увійти до Termix",
|
||||
"registerTitle": "Створити обл. запис",
|
||||
"forgotPassword": "Забули пароль?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Виберіть кількість панелей для відображення",
|
||||
"panesTitle": "Панелі",
|
||||
"openTabsTitle": "Відкрити вкладки",
|
||||
"dragTabsHint": "Перетягніть вкладки в панелі вище",
|
||||
"dragTabsHint": "Перетягніть вкладки на панелі вище або скористайтеся функцією швидкого призначення",
|
||||
"dropHere": "Перетягніть сюди",
|
||||
"emptyPane": "Порожньо",
|
||||
"dashboard": "Приладна дошка",
|
||||
"clearSplitScreen": "Очистити розділений екран"
|
||||
"clearSplitScreen": "Очистити розділений екран",
|
||||
"quickAssign": "Швидке призначення",
|
||||
"alreadyAssigned": "Панель {{index}}",
|
||||
"splitTab": "Розділити вкладку",
|
||||
"addToSplit": "Додати до Спліта",
|
||||
"removeFromSplit": "Видалити зі Спліта",
|
||||
"assignToPane": "Призначити панелі"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Створити сніпет",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Не вдалося вилучити теку",
|
||||
"folderEditSuccess": "Папка успішно змінена",
|
||||
"folderEditFailed": "Не вдалося оновити теку",
|
||||
"confirmRunMessage": "Виконати \"{{name}}\"?",
|
||||
"confirmRunButton": "Бігти",
|
||||
"runSuccess": "Виконано \"{{name}}\" у {{count}} терміналі(ах)",
|
||||
"copySuccess": "Скопійовано \"{{name}}\" в буфер обміну",
|
||||
"shareTitle": "Поділитися сніпетом",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Поточний доступ",
|
||||
"shareLoadError": "Не вдалося завантажити дані обміну",
|
||||
"loading": "Завантажується...",
|
||||
"close": "Закрити"
|
||||
"close": "Закрити",
|
||||
"reorderFailed": "Не вдалося зберегти порядок фрагментів"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Обліковий запис",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Тема",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "Колір акценту",
|
||||
"settingsFileManager": "Файловий менеджер",
|
||||
"fileColorCoding": "Колір файлу",
|
||||
"fileColorCodingDesc": "По типу файлів кольорових кодів",
|
||||
"settingsTerminal": "Термінал",
|
||||
"commandAutocomplete": "Команду Автозаповнення",
|
||||
"commandAutocompleteDesc": "Показати автодоповнення під час введення тексту",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Видобуток терміналу",
|
||||
"commandPalette": "Командна палітра",
|
||||
"commandPaletteDesc": "Увімкнути гарячі клавіші",
|
||||
"sessionPersistence": "Постійна сесія",
|
||||
"sessionPersistenceDesc": "Залишати сеанси між повторним з'єднанням",
|
||||
"reopenTabsOnLogin": "Повторно відкривати вкладки після входу",
|
||||
"reopenTabsOnLoginDesc": "Відновлюйте відкриті вкладки після входу в систему або оновлення сторінки, навіть з іншого пристрою",
|
||||
"confirmTabClose": "Підтвердити закриття вкладки",
|
||||
"confirmTabCloseDesc": "Запитувати перед закриттям вкладок терміналу",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "Показати теги хостів",
|
||||
"showHostTagsDesc": "Відображення тегів у списку хостів",
|
||||
"hostTrayOnClick": "Натисніть, щоб розгорнути дії хоста",
|
||||
"hostTrayOnClickDesc": "Вимагати клацання для розгортання дій хоста замість наведення курсора",
|
||||
"pinAppRail": "Закріпити рейку додатків",
|
||||
"pinAppRailDesc": "Залишати ліву бічну панель програм завжди розгорнутою, а не при наведенні курсора",
|
||||
"settingsSnippets": "Сніпети",
|
||||
"foldersCollapsed": "Теки вилучені",
|
||||
"foldersCollapsedDesc": "Згорнути теки за замовчуванням",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "Những đoạn trích",
|
||||
"hostManager": "Quản lý máy chủ",
|
||||
"credentials": "Thông tin xác thực",
|
||||
"connections": "Kết nối",
|
||||
"roleAdministrator": "Quản trị viên",
|
||||
"roleUser": "Người dùng"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "Độ nhạy cuộn nhanh",
|
||||
"moshCommandLabel": "Bộ chỉ huy Mosh",
|
||||
"startupSnippetLabel": "Đoạn mã khởi nghiệp",
|
||||
"keepaliveIntervalLabel": "Khoảng thời gian duy trì kết nối",
|
||||
"keepaliveIntervalLabel": "Khoảng thời gian duy trì kết nối (giây)",
|
||||
"maxKeepaliveMisses": "Max Keepalive Misses",
|
||||
"tunnelSettings": "Cài đặt đường hầm",
|
||||
"enableTunneling": "Bật tính năng tạo đường hầm",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "URL VNC đã được sao chép",
|
||||
"telnetUrlCopied": "URL Telnet đã được sao chép",
|
||||
"remoteDesktopUrlCopied": "URL máy tính từ xa đã được sao chép",
|
||||
"wakeOnLanAction": "Wake on LAN",
|
||||
"wakeOnLanSuccess": "Gói ma thuật đã được gửi đến {{name}}",
|
||||
"wakeOnLanError": "Không thể gửi gói tin ma thuật.",
|
||||
"cloneHostAction": "Máy chủ nhân bản",
|
||||
"copyAddress": "Sao chép địa chỉ",
|
||||
"copyLink": "Sao chép liên kết",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "Không có máy chủ nào phù hợp với tìm kiếm của bạn",
|
||||
"hostNotFound": "Không tìm thấy máy chủ",
|
||||
"searchHosts": "Tìm kiếm máy chủ...",
|
||||
"sortHosts": "Sắp xếp máy chủ",
|
||||
"sortDefault": "Thứ tự mặc định",
|
||||
"sortNameAsc": "Tên (A → Z)",
|
||||
"sortNameDesc": "Tên (Z → A)",
|
||||
"sortIpAsc": "Địa chỉ IP (Tăng dần)",
|
||||
"sortIpDesc": "Địa chỉ IP (Mô tả)",
|
||||
"sortOnlineFirst": "Ưu tiên trực tuyến",
|
||||
"sortOfflineFirst": "Ưu tiên ngoại tuyến",
|
||||
"sortPinnedFirst": "Được ghim đầu tiên",
|
||||
"filterHosts": "Lọc máy chủ",
|
||||
"filterClearAll": "Xóa bộ lọc",
|
||||
"filterStatusGroup": "Trạng thái",
|
||||
"filterOnline": "Trực tuyến",
|
||||
"filterOffline": "Ngoại tuyến",
|
||||
"filterPinned": "Đã ghim",
|
||||
"filterAuthGroup": "Loại xác thực",
|
||||
"filterAuthPassword": "Mật khẩu",
|
||||
"filterAuthKey": "Khóa SSH",
|
||||
"filterAuthCredential": "Chứng chỉ",
|
||||
"filterAuthNone": "Không có",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "Giao thức",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Đặc trưng",
|
||||
"filterFeatureTerminal": "Phần cuối",
|
||||
"filterFeatureFileManager": "Trình quản lý tập tin",
|
||||
"filterFeatureTunnel": "Đường hầm",
|
||||
"filterFeatureDocker": "Docker",
|
||||
"filterTagsGroup": "Thẻ",
|
||||
"shareHost": "Chia sẻ máy chủ",
|
||||
"shareHostTitle": "Chia sẻ: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - trống",
|
||||
"noTabAssigned": "Không có tab nào được chỉ định"
|
||||
"noTabAssigned": "Không có tab nào được chỉ định",
|
||||
"focusedPane": "Khung đang hoạt động"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "Không có kết nối",
|
||||
"noConnectionsDesc": "Mở cửa sổ dòng lệnh, trình quản lý tập tin hoặc máy tính từ xa để xem các kết nối tại đây.",
|
||||
"connectedFor": "Đã kết nối cho {{duration}}",
|
||||
"connected": "Đã kết nối",
|
||||
"disconnected": "Đã ngắt kết nối",
|
||||
"closeTab": "Đóng tab",
|
||||
"closeConnection": "Mối liên hệ mật thiết",
|
||||
"forgetTab": "Quên",
|
||||
"removeBackground": "Di dời",
|
||||
"reconnect": "Kết nối lại",
|
||||
"reopenTab": "Mở cửa trở lại",
|
||||
"sectionOpen": "Mở",
|
||||
"sectionBackground": "Lý lịch",
|
||||
"backgroundDesc": "Phiên làm việc sẽ vẫn duy trì trong 30 phút sau khi ngắt kết nối và có thể được kết nối lại.",
|
||||
"persisted": "Vẫn tồn tại ở chế độ nền",
|
||||
"expiresIn": "Hết hạn sau {{duration}}",
|
||||
"search": "Tìm kiếm các kết nối...",
|
||||
"noSearchResults": "Không có kết nối nào phù hợp với tìm kiếm của bạn."
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Đang kết nối đến phiên {{type}}...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "Thử lại"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "QUẢN LÝ MÁY CHỦ SSH",
|
||||
"tagline": "Quản lý SSH và máy tính từ xa tự lưu trữ",
|
||||
"loginTitle": "Đăng nhập vào Termix",
|
||||
"registerTitle": "Tạo tài khoản",
|
||||
"forgotPassword": "Quên mật khẩu?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "Chọn số lượng khung hiển thị",
|
||||
"panesTitle": "Cửa kính",
|
||||
"openTabsTitle": "Các tab đang mở",
|
||||
"dragTabsHint": "Kéo các tab vào các khung phía trên",
|
||||
"dragTabsHint": "Kéo các tab vào các khung phía trên hoặc sử dụng chức năng Gán nhanh.",
|
||||
"dropHere": "Thả xuống đây",
|
||||
"emptyPane": "Trống",
|
||||
"dashboard": "Bảng điều khiển",
|
||||
"clearSplitScreen": "Xóa màn hình chia đôi"
|
||||
"clearSplitScreen": "Xóa màn hình chia đôi",
|
||||
"quickAssign": "Phân công nhanh",
|
||||
"alreadyAssigned": "Pane {{index}}",
|
||||
"splitTab": "Chia tab",
|
||||
"addToSplit": "Thêm vào phần chia",
|
||||
"removeFromSplit": "Xóa khỏi phần Chia",
|
||||
"assignToPane": "Gán vào ngăn"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "Tạo đoạn mã",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "Không thể xóa thư mục",
|
||||
"folderEditSuccess": "Thư mục đã được cập nhật thành công.",
|
||||
"folderEditFailed": "Không thể cập nhật thư mục.",
|
||||
"confirmRunMessage": "Chạy \"{{name}}\"?",
|
||||
"confirmRunButton": "Chạy",
|
||||
"runSuccess": "Ran \"{{name}}\" in {{count}} terminal(s)",
|
||||
"copySuccess": "Đã sao chép \"{{name}}\" vào clipboard",
|
||||
"shareTitle": "Chia sẻ đoạn trích",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "Truy cập hiện tại",
|
||||
"shareLoadError": "Không thể tải dữ liệu chia sẻ",
|
||||
"loading": "Đang tải...",
|
||||
"close": "Đóng"
|
||||
"close": "Đóng",
|
||||
"reorderFailed": "Không thể lưu thứ tự đoạn mã."
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "Tài khoản",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "Chủ đề",
|
||||
"fontSizeLabel": "Kích thước phông chữ",
|
||||
"accentColorLabel": "Màu nhấn",
|
||||
"settingsFileManager": "Trình quản lý tập tin",
|
||||
"fileColorCoding": "Mã màu tệp",
|
||||
"fileColorCodingDesc": "Phân loại tệp theo màu sắc.",
|
||||
"settingsTerminal": "Phần cuối",
|
||||
"commandAutocomplete": "Tự động hoàn thành lệnh",
|
||||
"commandAutocompleteDesc": "Hiển thị tính năng tự động hoàn thành khi đang gõ",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "Đánh dấu đầu ra thiết bị đầu cuối",
|
||||
"commandPalette": "Bảng lệnh",
|
||||
"commandPaletteDesc": "Bật phím tắt",
|
||||
"sessionPersistence": "Duy trì phiên",
|
||||
"sessionPersistenceDesc": "Giữ nguyên các phiên giữa các lần kết nối lại.",
|
||||
"reopenTabsOnLogin": "Mở lại các tab khi đăng nhập",
|
||||
"reopenTabsOnLoginDesc": "Khôi phục các tab đang mở khi bạn đăng nhập hoặc làm mới trang, ngay cả khi bạn đang sử dụng thiết bị khác.",
|
||||
"confirmTabClose": "Xác nhận đóng tab",
|
||||
"confirmTabCloseDesc": "Hãy hỏi trước khi đóng các tab cửa sổ terminal.",
|
||||
"settingsSidebar": "Thanh bên",
|
||||
"showHostTags": "Thẻ người dẫn chương trình",
|
||||
"showHostTagsDesc": "Hiển thị thẻ trong danh sách máy chủ",
|
||||
"hostTrayOnClick": "Nhấp chuột để mở rộng các hành động của máy chủ",
|
||||
"hostTrayOnClickDesc": "Yêu cầu nhấp chuột để mở rộng các thao tác trên máy chủ thay vì di chuột.",
|
||||
"pinAppRail": "Ghim ứng dụng Rail",
|
||||
"pinAppRailDesc": "Giữ cho thanh bên trái của ứng dụng luôn mở rộng thay vì chỉ mở rộng khi di chuột.",
|
||||
"settingsSnippets": "Những đoạn trích",
|
||||
"foldersCollapsed": "Các thư mục đã được thu gọn",
|
||||
"foldersCollapsedDesc": "Thu gọn thư mục theo mặc định",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "片段",
|
||||
"hostManager": "主机管理器",
|
||||
"credentials": "凭证",
|
||||
"connections": "连接",
|
||||
"roleAdministrator": "管理员",
|
||||
"roleUser": "用户"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "快速滚动灵敏度",
|
||||
"moshCommandLabel": "Mosh 命令",
|
||||
"startupSnippetLabel": "启动代码片段",
|
||||
"keepaliveIntervalLabel": "保持存活间隔",
|
||||
"keepaliveIntervalLabel": "保持连接间隔(秒)",
|
||||
"maxKeepaliveMisses": "最大保留生命失败",
|
||||
"tunnelSettings": "隧道设置",
|
||||
"enableTunneling": "启用 Tunneling",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "已复制 VNC URL",
|
||||
"telnetUrlCopied": "已复制Telnet URL",
|
||||
"remoteDesktopUrlCopied": "已复制远程桌面 URL",
|
||||
"wakeOnLanAction": "局域网唤醒",
|
||||
"wakeOnLanSuccess": "魔法数据包已发送至 {{name}}",
|
||||
"wakeOnLanError": "发送魔术包失败",
|
||||
"cloneHostAction": "复制主机",
|
||||
"copyAddress": "复制地址",
|
||||
"copyLink": "复制链接",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "没有符合您搜索条件的主机",
|
||||
"hostNotFound": "找不到主机",
|
||||
"searchHosts": "搜索主机...",
|
||||
"sortHosts": "排序主机",
|
||||
"sortDefault": "默认订单",
|
||||
"sortNameAsc": "姓名(A → Z)",
|
||||
"sortNameDesc": "姓名(Z → A)",
|
||||
"sortIpAsc": "IP地址(升序)",
|
||||
"sortIpDesc": "IP地址(描述)",
|
||||
"sortOnlineFirst": "在线优先",
|
||||
"sortOfflineFirst": "离线优先",
|
||||
"sortPinnedFirst": "置顶",
|
||||
"filterHosts": "过滤主机",
|
||||
"filterClearAll": "清除筛选条件",
|
||||
"filterStatusGroup": "地位",
|
||||
"filterOnline": "在线的",
|
||||
"filterOffline": "离线",
|
||||
"filterPinned": "置顶",
|
||||
"filterAuthGroup": "身份验证类型",
|
||||
"filterAuthPassword": "密码",
|
||||
"filterAuthKey": "SSH密钥",
|
||||
"filterAuthCredential": "凭据",
|
||||
"filterAuthNone": "没有任何",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "协议",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "远程桌面协议",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "特征",
|
||||
"filterFeatureTerminal": "终端",
|
||||
"filterFeatureFileManager": "文件管理器",
|
||||
"filterFeatureTunnel": "隧道",
|
||||
"filterFeatureDocker": "Docker",
|
||||
"filterTagsGroup": "标签",
|
||||
"shareHost": "共享主机",
|
||||
"shareHostTitle": "分享: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "Pane {{index}} - empty",
|
||||
"noTabAssigned": "未分配标签"
|
||||
"noTabAssigned": "未分配标签",
|
||||
"focusedPane": "活动窗格"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "没有连接",
|
||||
"noConnectionsDesc": "打开终端、文件管理器或远程桌面即可在此处查看连接。",
|
||||
"connectedFor": "已连接 {{duration}}",
|
||||
"connected": "已连接",
|
||||
"disconnected": "断开连接",
|
||||
"closeTab": "关闭标签页",
|
||||
"closeConnection": "紧密联系",
|
||||
"forgetTab": "忘记",
|
||||
"removeBackground": "消除",
|
||||
"reconnect": "重新连接",
|
||||
"reopenTab": "重新开放",
|
||||
"sectionOpen": "打开",
|
||||
"sectionBackground": "背景",
|
||||
"backgroundDesc": "断开连接后,会话仍会保持 30 分钟,并且可以重新连接。",
|
||||
"persisted": "在后台持续运行",
|
||||
"expiresIn": "有效期至 {{duration}}",
|
||||
"search": "搜索关联...",
|
||||
"noSearchResults": "没有找到与您的搜索匹配的结果。"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "正在连接到 {{type}} 会话...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "重试"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH 服务器管理器",
|
||||
"tagline": "自托管 SSH 和远程桌面管理",
|
||||
"loginTitle": "登录 Termix",
|
||||
"registerTitle": "创建账户",
|
||||
"forgotPassword": "忘记密码?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "选择要显示多少窗格",
|
||||
"panesTitle": "面包",
|
||||
"openTabsTitle": "打开标签",
|
||||
"dragTabsHint": "拖动标签到上面的窗格",
|
||||
"dragTabsHint": "将标签拖入上方窗格,或使用快速分配功能。",
|
||||
"dropHere": "拖放到这里",
|
||||
"emptyPane": "空的",
|
||||
"dashboard": "仪表板",
|
||||
"clearSplitScreen": "清除分屏"
|
||||
"clearSplitScreen": "清除分屏",
|
||||
"quickAssign": "快速分配",
|
||||
"alreadyAssigned": "窗格 {{index}}",
|
||||
"splitTab": "分页标签",
|
||||
"addToSplit": "添加到拆分",
|
||||
"removeFromSplit": "从拆分中移除",
|
||||
"assignToPane": "分配到窗格"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "创建代码片段",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "删除文件夹失败",
|
||||
"folderEditSuccess": "文件夹更新成功",
|
||||
"folderEditFailed": "更新文件夹失败",
|
||||
"confirmRunMessage": "运行“{{name}}”?",
|
||||
"confirmRunButton": "跑步",
|
||||
"runSuccess": "在 {{count}} 终端中运行“{{name}}”",
|
||||
"copySuccess": "已将“{{name}}”复制到剪贴板",
|
||||
"shareTitle": "共享代码片段",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "当前访问",
|
||||
"shareLoadError": "加载共享数据失败",
|
||||
"loading": "加载中...",
|
||||
"close": "关闭"
|
||||
"close": "关闭",
|
||||
"reorderFailed": "保存代码片段顺序失败"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "账户",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "主题",
|
||||
"fontSizeLabel": "Font Size",
|
||||
"accentColorLabel": "亮度颜色",
|
||||
"settingsFileManager": "文件管理器",
|
||||
"fileColorCoding": "文件颜色编码",
|
||||
"fileColorCodingDesc": "按类型显示颜色代码",
|
||||
"settingsTerminal": "终端",
|
||||
"commandAutocomplete": "命令自动完成",
|
||||
"commandAutocompleteDesc": "输入时显示自动完成",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "高亮终端输出",
|
||||
"commandPalette": "命令调色板",
|
||||
"commandPaletteDesc": "启用键盘快捷键",
|
||||
"sessionPersistence": "会话可持续性",
|
||||
"sessionPersistenceDesc": "在重新连接之间保持会话",
|
||||
"reopenTabsOnLogin": "登录后重新打开标签页",
|
||||
"reopenTabsOnLoginDesc": "即使从其他设备登录或刷新页面,也能恢复您打开的标签页。",
|
||||
"confirmTabClose": "确认关闭标签",
|
||||
"confirmTabCloseDesc": "关闭终端标签前询问",
|
||||
"settingsSidebar": "Sidebar",
|
||||
"showHostTags": "显示主机标签",
|
||||
"showHostTagsDesc": "在主机列表中显示标签",
|
||||
"hostTrayOnClick": "点击展开主机操作",
|
||||
"hostTrayOnClickDesc": "需要点击才能展开主机操作,而不是鼠标悬停。",
|
||||
"pinAppRail": "Pin App Rail",
|
||||
"pinAppRailDesc": "保持左侧边栏应用导轨始终展开,而不是鼠标悬停时才展开。",
|
||||
"settingsSnippets": "片段",
|
||||
"foldersCollapsed": "折叠文件夹",
|
||||
"foldersCollapsedDesc": "默认折叠文件夹",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"snippets": "片段",
|
||||
"hostManager": "主機管理器",
|
||||
"credentials": "證書",
|
||||
"connections": "連接",
|
||||
"roleAdministrator": "行政人員",
|
||||
"roleUser": "使用者"
|
||||
},
|
||||
@@ -342,7 +343,7 @@
|
||||
"fastScrollSensitivityLabel": "快速滾動靈敏度",
|
||||
"moshCommandLabel": "Mosh Command",
|
||||
"startupSnippetLabel": "啟動片段",
|
||||
"keepaliveIntervalLabel": "保持連線間隔",
|
||||
"keepaliveIntervalLabel": "保持連線間隔(秒)",
|
||||
"maxKeepaliveMisses": "最大保命失誤",
|
||||
"tunnelSettings": "隧道設定",
|
||||
"enableTunneling": "啟用隧道",
|
||||
@@ -464,6 +465,9 @@
|
||||
"vncUrlCopied": "VNC URL 已複製",
|
||||
"telnetUrlCopied": "Telnet URL 已複製",
|
||||
"remoteDesktopUrlCopied": "已複製遠端桌面 URL",
|
||||
"wakeOnLanAction": "區域網路喚醒",
|
||||
"wakeOnLanSuccess": "魔法資料包已寄至 {{name}}",
|
||||
"wakeOnLanError": "發送魔術包失敗",
|
||||
"cloneHostAction": "複製主機",
|
||||
"copyAddress": "副本地址",
|
||||
"copyLink": "複製連結",
|
||||
@@ -564,6 +568,38 @@
|
||||
"noHostsMatchSearch": "沒有符合您搜尋條件的主機",
|
||||
"hostNotFound": "未找到主機",
|
||||
"searchHosts": "搜尋主機...",
|
||||
"sortHosts": "排序主機",
|
||||
"sortDefault": "預設訂單",
|
||||
"sortNameAsc": "姓名(A → Z)",
|
||||
"sortNameDesc": "姓名(Z → A)",
|
||||
"sortIpAsc": "IP位址(升序)",
|
||||
"sortIpDesc": "IP位址(說明)",
|
||||
"sortOnlineFirst": "線上優先",
|
||||
"sortOfflineFirst": "離線優先",
|
||||
"sortPinnedFirst": "置頂",
|
||||
"filterHosts": "過濾主機",
|
||||
"filterClearAll": "清除篩選條件",
|
||||
"filterStatusGroup": "地位",
|
||||
"filterOnline": "在線的",
|
||||
"filterOffline": "離線",
|
||||
"filterPinned": "置頂",
|
||||
"filterAuthGroup": "身份驗證類型",
|
||||
"filterAuthPassword": "密碼",
|
||||
"filterAuthKey": "SSH金鑰",
|
||||
"filterAuthCredential": "憑證",
|
||||
"filterAuthNone": "沒有任何",
|
||||
"filterAuthOpkssh": "OPKSSH",
|
||||
"filterProtocolGroup": "協定",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "遠端桌面協定",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "特徵",
|
||||
"filterFeatureTerminal": "終端",
|
||||
"filterFeatureFileManager": "文件管理器",
|
||||
"filterFeatureTunnel": "隧道",
|
||||
"filterFeatureDocker": "Docker",
|
||||
"filterTagsGroup": "標籤",
|
||||
"shareHost": "共享主機",
|
||||
"shareHostTitle": "分享: {{name}}",
|
||||
"sharing": {
|
||||
@@ -740,7 +776,28 @@
|
||||
},
|
||||
"splitScreen": {
|
||||
"paneEmpty": "窗格 {{index}} - 空",
|
||||
"noTabAssigned": "未分配標籤頁"
|
||||
"noTabAssigned": "未分配標籤頁",
|
||||
"focusedPane": "活動窗格"
|
||||
},
|
||||
"connections": {
|
||||
"noConnections": "沒有連接",
|
||||
"noConnectionsDesc": "開啟終端機、檔案總管或遠端桌面即可在此處查看連線。",
|
||||
"connectedFor": "已連線 {{duration}}",
|
||||
"connected": "已連接",
|
||||
"disconnected": "斷開連接",
|
||||
"closeTab": "關閉標籤頁",
|
||||
"closeConnection": "緊密聯繫",
|
||||
"forgetTab": "忘記",
|
||||
"removeBackground": "消除",
|
||||
"reconnect": "重新連接",
|
||||
"reopenTab": "重新開放",
|
||||
"sectionOpen": "打開",
|
||||
"sectionBackground": "背景",
|
||||
"backgroundDesc": "斷開連線後,會話仍會保持 30 分鐘,並且可以重新連線。",
|
||||
"persisted": "在背景持續運行",
|
||||
"expiresIn": "有效期限至 {{duration}}",
|
||||
"search": "搜尋關聯...",
|
||||
"noSearchResults": "沒有找到與您的搜尋相符的結果。"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "正在連線到 {{type}} 會話...",
|
||||
@@ -1238,7 +1295,7 @@
|
||||
"retry": "重試"
|
||||
},
|
||||
"auth": {
|
||||
"tagline": "SSH 伺服器管理員",
|
||||
"tagline": "自託管 SSH 和遠端桌面管理",
|
||||
"loginTitle": "登入 Termix",
|
||||
"registerTitle": "建立帳戶",
|
||||
"forgotPassword": "忘記密碼?",
|
||||
@@ -1797,11 +1854,17 @@
|
||||
"selectLayoutHint": "選擇要顯示的窗格數量",
|
||||
"panesTitle": "窗格",
|
||||
"openTabsTitle": "開啟標籤頁",
|
||||
"dragTabsHint": "將標籤拖入上方窗格。",
|
||||
"dragTabsHint": "將標籤拖曳到上方窗格,或使用快速指派功能。",
|
||||
"dropHere": "落在這裡",
|
||||
"emptyPane": "空的",
|
||||
"dashboard": "儀表板",
|
||||
"clearSplitScreen": "清晰的分割畫面"
|
||||
"clearSplitScreen": "清晰的分割畫面",
|
||||
"quickAssign": "快速分配",
|
||||
"alreadyAssigned": "窗格 {{index}}",
|
||||
"splitTab": "分頁標籤",
|
||||
"addToSplit": "添加到拆分",
|
||||
"removeFromSplit": "從分割中移除",
|
||||
"assignToPane": "指派到窗格"
|
||||
},
|
||||
"snippets": {
|
||||
"createSnippetTitle": "建立程式碼片段",
|
||||
@@ -1855,6 +1918,8 @@
|
||||
"folderDeleteFailed": "刪除資料夾失敗",
|
||||
"folderEditSuccess": "資料夾已成功更新",
|
||||
"folderEditFailed": "更新資料夾失敗",
|
||||
"confirmRunMessage": "運行“{{name}}”?",
|
||||
"confirmRunButton": "跑步",
|
||||
"runSuccess": "在 {{count}} 終端機中執行“{{name}}”",
|
||||
"copySuccess": "已將「{{name}}」複製到剪貼簿",
|
||||
"shareTitle": "分享片段",
|
||||
@@ -1869,7 +1934,8 @@
|
||||
"currentAccess": "目前存取權限",
|
||||
"shareLoadError": "共享資料載入失敗",
|
||||
"loading": "載入中...",
|
||||
"close": "關閉"
|
||||
"close": "關閉",
|
||||
"reorderFailed": "儲存程式碼片段順序失敗"
|
||||
},
|
||||
"userProfile": {
|
||||
"sectionAccount": "帳戶",
|
||||
@@ -1896,9 +1962,6 @@
|
||||
"themeLabel": "主題",
|
||||
"fontSizeLabel": "字體大小",
|
||||
"accentColorLabel": "強調色",
|
||||
"settingsFileManager": "文件管理器",
|
||||
"fileColorCoding": "文件顏色編碼",
|
||||
"fileColorCodingDesc": "按類型對文件進行顏色編碼",
|
||||
"settingsTerminal": "終端",
|
||||
"commandAutocomplete": "命令自動完成",
|
||||
"commandAutocompleteDesc": "輸入時顯示自動完成功能",
|
||||
@@ -1908,13 +1971,17 @@
|
||||
"syntaxHighlightingDesc": "高亮顯示終端輸出",
|
||||
"commandPalette": "命令面板",
|
||||
"commandPaletteDesc": "啟用鍵盤快速鍵",
|
||||
"sessionPersistence": "會話持久性",
|
||||
"sessionPersistenceDesc": "保持重新連線之間的會話",
|
||||
"reopenTabsOnLogin": "登入後重新開啟標籤頁",
|
||||
"reopenTabsOnLoginDesc": "即使從其他裝置登入或重新整理頁面,也能恢復您開啟的標籤頁。",
|
||||
"confirmTabClose": "確認關閉標籤頁",
|
||||
"confirmTabCloseDesc": "關閉終端標籤頁前請先詢問。",
|
||||
"settingsSidebar": "側邊欄",
|
||||
"showHostTags": "節目主持人標籤",
|
||||
"showHostTagsDesc": "在主機清單中顯示標籤",
|
||||
"hostTrayOnClick": "點選展開主機操作",
|
||||
"hostTrayOnClickDesc": "需要點擊才能展開主機操作,而不是滑鼠懸停。",
|
||||
"pinAppRail": "Pin App Rail",
|
||||
"pinAppRailDesc": "保持左側邊欄應用導軌始終展開,而不是滑鼠懸停時才展開。",
|
||||
"settingsSnippets": "片段",
|
||||
"foldersCollapsed": "資料夾已折疊",
|
||||
"foldersCollapsedDesc": "預設折疊資料夾",
|
||||
|
||||
+97
-2
@@ -3968,7 +3968,7 @@ export async function reorderSnippets(
|
||||
updates: Array<{ id: number; order: number; folder?: string }>,
|
||||
): Promise<{ success: boolean }> {
|
||||
try {
|
||||
const response = await authApi.post("/snippets/reorder", {
|
||||
const response = await authApi.put("/snippets/reorder", {
|
||||
snippets: updates,
|
||||
});
|
||||
return response.data;
|
||||
@@ -4360,9 +4360,13 @@ export async function getGuacamoleToken(
|
||||
|
||||
export async function getGuacamoleTokenFromHost(
|
||||
hostId: number,
|
||||
protocol?: "rdp" | "vnc" | "telnet",
|
||||
): Promise<GuacamoleTokenResponse> {
|
||||
try {
|
||||
const response = await authApi.post(`/guacamole/connect-host/${hostId}`);
|
||||
const response = await authApi.post(
|
||||
`/guacamole/connect-host/${hostId}`,
|
||||
protocol ? { protocol } : {},
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "get guacamole token from host");
|
||||
@@ -4924,3 +4928,94 @@ export async function saveDashboardPreferences(
|
||||
const response = await dashboardApi.post("/dashboard/preferences", layout);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OPEN TABS API
|
||||
// ============================================================================
|
||||
|
||||
export interface OpenTabRecord {
|
||||
id: string;
|
||||
userId: string;
|
||||
tabType: string;
|
||||
hostId: number | null;
|
||||
label: string;
|
||||
tabOrder: number;
|
||||
backendSessionId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface OpenTabSyncPayload {
|
||||
id: string;
|
||||
tabType: string;
|
||||
hostId?: number | null;
|
||||
label: string;
|
||||
tabOrder: number;
|
||||
backendSessionId?: string | null;
|
||||
}
|
||||
|
||||
export interface OpenTabUpsertPayload {
|
||||
id: string;
|
||||
tabType: string;
|
||||
hostId?: number | null;
|
||||
label: string;
|
||||
tabOrder: number;
|
||||
backendSessionId?: string | null;
|
||||
}
|
||||
|
||||
export interface ActiveSessionInfo {
|
||||
sessionId: string;
|
||||
hostId: number;
|
||||
hostName: string;
|
||||
tabInstanceId: string | null;
|
||||
isConnected: boolean;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export async function getOpenTabs(): Promise<OpenTabRecord[]> {
|
||||
const response = await authApi.get("/open-tabs");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function syncOpenTabs(tabs: OpenTabSyncPayload[]): Promise<void> {
|
||||
await authApi.put("/open-tabs", { tabs });
|
||||
}
|
||||
|
||||
export async function deleteOpenTab(instanceId: string): Promise<void> {
|
||||
await authApi.delete(`/open-tabs/${instanceId}`);
|
||||
}
|
||||
|
||||
export async function patchOpenTab(
|
||||
instanceId: string,
|
||||
updates: Partial<Pick<OpenTabRecord, "label" | "tabOrder" | "backendSessionId">>,
|
||||
): Promise<void> {
|
||||
await authApi.patch(`/open-tabs/${instanceId}`, updates);
|
||||
}
|
||||
|
||||
export async function addOpenTab(tab: OpenTabUpsertPayload): Promise<void> {
|
||||
await authApi.post("/open-tabs", tab);
|
||||
}
|
||||
|
||||
export async function getActiveSessions(): Promise<ActiveSessionInfo[]> {
|
||||
const response = await authApi.get("/open-tabs/active-sessions");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// USER PREFERENCES API
|
||||
// ============================================================================
|
||||
|
||||
export interface UserPreferences {
|
||||
reopenTabsOnLogin: boolean;
|
||||
}
|
||||
|
||||
export async function getUserPreferences(): Promise<UserPreferences> {
|
||||
const response = await authApi.get("/user-preferences");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function saveUserPreferences(
|
||||
prefs: Partial<UserPreferences>,
|
||||
): Promise<void> {
|
||||
await authApi.put("/user-preferences", prefs);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
KeyRound,
|
||||
LayoutDashboard,
|
||||
Monitor,
|
||||
MousePointerClick,
|
||||
Clock,
|
||||
Folder,
|
||||
Pencil,
|
||||
@@ -48,7 +49,7 @@ const ACTIVITY_ICONS: Record<string, React.ReactNode> = {
|
||||
tunnel: <Network className="size-3.5" />,
|
||||
docker: <Box className="size-3.5" />,
|
||||
telnet: <MessagesSquare className="size-3.5" />,
|
||||
vnc: <Monitor className="size-3.5" />,
|
||||
vnc: <MousePointerClick className="size-3.5" />,
|
||||
rdp: <Monitor className="size-3.5" />,
|
||||
};
|
||||
|
||||
@@ -424,7 +425,7 @@ export function CommandPalette({
|
||||
}}
|
||||
className="flex items-center gap-1 px-2 h-6 rounded text-xs font-medium text-muted-foreground/70 hover:text-foreground hover:bg-muted-foreground/10 transition-colors border border-border/40"
|
||||
>
|
||||
<Monitor className="size-3" />
|
||||
<MousePointerClick className="size-3" />
|
||||
VNC
|
||||
</button>
|
||||
)}
|
||||
|
||||
+108
-71
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useRef, useEffect, memo, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { splitDragState, notifyDragEnd } from "@/lib/splitDragging";
|
||||
import { renderTabContent, tabIcon } from "@/shell/tabUtils";
|
||||
import type { Tab, TabType, Host, SplitMode } from "@/types/ui-types";
|
||||
import { tabIcon } from "@/shell/tabUtils";
|
||||
import type { Tab, SplitMode } from "@/types/ui-types";
|
||||
|
||||
// ─── useSplitSizes ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -267,17 +267,32 @@ function RowDivider({
|
||||
function PaneHeader({
|
||||
tab,
|
||||
paneIndex,
|
||||
isFocused,
|
||||
}: {
|
||||
tab: Tab | null;
|
||||
paneIndex: number;
|
||||
isFocused: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 px-2.5 h-7 shrink-0 bg-sidebar border-b border-border text-xs font-medium text-muted-foreground select-none">
|
||||
<div
|
||||
className={`flex items-center gap-1.5 px-2.5 h-7 shrink-0 border-b text-xs font-medium select-none transition-colors ${
|
||||
isFocused
|
||||
? "bg-accent-brand/10 border-accent-brand/40 text-accent-brand"
|
||||
: "bg-sidebar border-border text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{isFocused && (
|
||||
<span className="w-1 h-3.5 rounded-full bg-accent-brand shrink-0" />
|
||||
)}
|
||||
{tab ? (
|
||||
<>
|
||||
<span className="opacity-60">{tabIcon(tab.type)}</span>
|
||||
<span className="truncate text-foreground">
|
||||
<span className={isFocused ? "text-accent-brand" : "opacity-60"}>
|
||||
{tabIcon(tab.type)}
|
||||
</span>
|
||||
<span
|
||||
className={`truncate ${isFocused ? "text-accent-brand font-semibold" : "text-foreground"}`}
|
||||
>
|
||||
{tab.type === "dashboard" ? "Dashboard" : tab.label}
|
||||
</span>
|
||||
</>
|
||||
@@ -305,34 +320,25 @@ function EmptyPane() {
|
||||
);
|
||||
}
|
||||
|
||||
const PaneContent = memo(function PaneContent({
|
||||
tab,
|
||||
onOpenSingletonTab,
|
||||
onOpenTab,
|
||||
}: {
|
||||
tab: Tab;
|
||||
onOpenSingletonTab: (type: TabType) => void;
|
||||
onOpenTab: (host: Host, type: TabType) => void;
|
||||
}) {
|
||||
return <>{renderTabContent(tab, onOpenSingletonTab, onOpenTab)}</>;
|
||||
});
|
||||
|
||||
const Pane = memo(function Pane({
|
||||
tab,
|
||||
paneIndex,
|
||||
isDragging,
|
||||
onOpenSingletonTab,
|
||||
onOpenTab,
|
||||
isFocused,
|
||||
onPaneContentRef,
|
||||
onPaneClick,
|
||||
onAssignPane,
|
||||
}: {
|
||||
tab: Tab | null;
|
||||
paneIndex: number;
|
||||
isDragging: boolean;
|
||||
onOpenSingletonTab: (type: TabType) => void;
|
||||
onOpenTab: (host: Host, type: TabType) => void;
|
||||
isFocused: boolean;
|
||||
onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void;
|
||||
onPaneClick?: (paneIndex: number) => void;
|
||||
onAssignPane?: (paneIndex: number, tabId: string) => void;
|
||||
}) {
|
||||
const isTerminal = tab?.type === "terminal";
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
|
||||
const contentRef = useCallback(
|
||||
(el: HTMLDivElement | null) => {
|
||||
onPaneContentRef?.(paneIndex, el);
|
||||
@@ -341,24 +347,37 @@ const Pane = memo(function Pane({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col w-full h-full min-w-0 min-h-0 overflow-hidden">
|
||||
<PaneHeader tab={tab} paneIndex={paneIndex} />
|
||||
<div
|
||||
className={`relative flex flex-col w-full h-full min-w-0 min-h-0 overflow-hidden transition-colors ${
|
||||
isFocused ? "ring-1 ring-inset ring-accent-brand/30" : ""
|
||||
} ${isDragOver ? "ring-2 ring-inset ring-accent-brand" : ""}`}
|
||||
onClick={() => onPaneClick?.(paneIndex)}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(true);
|
||||
}}
|
||||
onDragLeave={() => setIsDragOver(false)}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
const tabId = e.dataTransfer.getData("text/plain");
|
||||
if (tabId) onAssignPane?.(paneIndex, tabId);
|
||||
}}
|
||||
>
|
||||
<PaneHeader tab={tab} paneIndex={paneIndex} isFocused={isFocused} />
|
||||
<div className="flex-1 min-h-0 overflow-hidden relative">
|
||||
{tab ? (
|
||||
isTerminal ? (
|
||||
// Terminal tabs are portaled in from AppShell to avoid remounting
|
||||
<div ref={contentRef} className="absolute inset-0" />
|
||||
) : (
|
||||
<PaneContent
|
||||
key={tab.id}
|
||||
tab={tab}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
/>
|
||||
)
|
||||
<div ref={contentRef} className="absolute inset-0" />
|
||||
) : (
|
||||
<EmptyPane />
|
||||
)}
|
||||
{isDragOver && (
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-accent-brand/10 border-2 border-dashed border-accent-brand pointer-events-none">
|
||||
<span className="text-xs font-medium text-accent-brand">
|
||||
Drop to assign
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isDragging && (
|
||||
<div className="absolute inset-0 z-10" style={{ cursor: "inherit" }} />
|
||||
@@ -377,11 +396,12 @@ const Row = memo(function Row({
|
||||
paneTabIds,
|
||||
tabs,
|
||||
isDragging,
|
||||
onOpenSingletonTab,
|
||||
onOpenTab,
|
||||
focusedPaneIndex,
|
||||
onColDivider,
|
||||
onColDividerTouch,
|
||||
onPaneContentRef,
|
||||
onPaneClick,
|
||||
onAssignPane,
|
||||
}: {
|
||||
rowIdx: number;
|
||||
paneIndices: number[];
|
||||
@@ -390,8 +410,7 @@ const Row = memo(function Row({
|
||||
paneTabIds: (string | null)[];
|
||||
tabs: Tab[];
|
||||
isDragging: boolean;
|
||||
onOpenSingletonTab: (type: TabType) => void;
|
||||
onOpenTab: (host: Host, type: TabType) => void;
|
||||
focusedPaneIndex: number | null;
|
||||
onColDivider: (e: React.MouseEvent, rowIdx: number, colIdx: number) => void;
|
||||
onColDividerTouch: (
|
||||
e: React.TouchEvent,
|
||||
@@ -399,6 +418,8 @@ const Row = memo(function Row({
|
||||
colIdx: number,
|
||||
) => void;
|
||||
onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void;
|
||||
onPaneClick?: (paneIndex: number) => void;
|
||||
onAssignPane?: (paneIndex: number, tabId: string) => void;
|
||||
}) {
|
||||
const widths = colWidths ?? [];
|
||||
return (
|
||||
@@ -417,9 +438,10 @@ const Row = memo(function Row({
|
||||
tab={tab}
|
||||
paneIndex={pIdx}
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
isFocused={focusedPaneIndex === pIdx}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
onPaneClick={onPaneClick}
|
||||
onAssignPane={onAssignPane}
|
||||
/>
|
||||
</div>
|
||||
{ci < paneIndices.length - 1 && (
|
||||
@@ -441,18 +463,20 @@ export const SplitView = memo(function SplitView({
|
||||
tabs,
|
||||
paneTabIds,
|
||||
splitMode,
|
||||
onOpenSingletonTab,
|
||||
onOpenTab,
|
||||
focusedPaneIndex,
|
||||
onTerminalResize,
|
||||
onPaneContentRef,
|
||||
onPaneClick,
|
||||
onAssignPane,
|
||||
}: {
|
||||
tabs: Tab[];
|
||||
paneTabIds: (string | null)[];
|
||||
splitMode: SplitMode;
|
||||
onOpenSingletonTab: (type: TabType) => void;
|
||||
onOpenTab: (host: Host, type: TabType) => void;
|
||||
focusedPaneIndex?: number | null;
|
||||
onTerminalResize?: () => void;
|
||||
onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void;
|
||||
onPaneClick?: (paneIndex: number) => void;
|
||||
onAssignPane?: (paneIndex: number, tabId: string) => void;
|
||||
}) {
|
||||
const {
|
||||
rowSizes,
|
||||
@@ -501,11 +525,12 @@ export const SplitView = memo(function SplitView({
|
||||
paneTabIds={paneTabIds}
|
||||
tabs={tabs}
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
focusedPaneIndex={focusedPaneIndex ?? null}
|
||||
onColDivider={onColDivider}
|
||||
onColDividerTouch={onColDividerTouch}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
onPaneClick={onPaneClick}
|
||||
onAssignPane={onAssignPane}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -519,9 +544,10 @@ export const SplitView = memo(function SplitView({
|
||||
tab={tab(0)}
|
||||
paneIndex={0}
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
isFocused={focusedPaneIndex === 0}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
onPaneClick={onPaneClick}
|
||||
onAssignPane={onAssignPane}
|
||||
/>
|
||||
</div>
|
||||
<ColDivider
|
||||
@@ -537,9 +563,10 @@ export const SplitView = memo(function SplitView({
|
||||
tab={tab(1)}
|
||||
paneIndex={1}
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
isFocused={focusedPaneIndex === 1}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
onPaneClick={onPaneClick}
|
||||
onAssignPane={onAssignPane}
|
||||
/>
|
||||
</div>
|
||||
<RowDivider
|
||||
@@ -554,9 +581,10 @@ export const SplitView = memo(function SplitView({
|
||||
tab={tab(2)}
|
||||
paneIndex={2}
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
isFocused={focusedPaneIndex === 2}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
onPaneClick={onPaneClick}
|
||||
onAssignPane={onAssignPane}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -577,9 +605,10 @@ export const SplitView = memo(function SplitView({
|
||||
tab={tab(0)}
|
||||
paneIndex={0}
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
isFocused={focusedPaneIndex === 0}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
onPaneClick={onPaneClick}
|
||||
onAssignPane={onAssignPane}
|
||||
/>
|
||||
</div>
|
||||
<ColDivider
|
||||
@@ -591,9 +620,10 @@ export const SplitView = memo(function SplitView({
|
||||
tab={tab(1)}
|
||||
paneIndex={1}
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
isFocused={focusedPaneIndex === 1}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
onPaneClick={onPaneClick}
|
||||
onAssignPane={onAssignPane}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -606,9 +636,10 @@ export const SplitView = memo(function SplitView({
|
||||
tab={tab(2)}
|
||||
paneIndex={2}
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
isFocused={focusedPaneIndex === 2}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
onPaneClick={onPaneClick}
|
||||
onAssignPane={onAssignPane}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -624,11 +655,12 @@ export const SplitView = memo(function SplitView({
|
||||
paneTabIds={paneTabIds}
|
||||
tabs={tabs}
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
focusedPaneIndex={focusedPaneIndex ?? null}
|
||||
onColDivider={onColDivider}
|
||||
onColDividerTouch={onColDividerTouch}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
onPaneClick={onPaneClick}
|
||||
onAssignPane={onAssignPane}
|
||||
/>
|
||||
<RowDivider
|
||||
onMouseDown={(e) => onRowDivider(e, 0)}
|
||||
@@ -642,11 +674,12 @@ export const SplitView = memo(function SplitView({
|
||||
paneTabIds={paneTabIds}
|
||||
tabs={tabs}
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
focusedPaneIndex={focusedPaneIndex ?? null}
|
||||
onColDivider={onColDivider}
|
||||
onColDividerTouch={onColDividerTouch}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
onPaneClick={onPaneClick}
|
||||
onAssignPane={onAssignPane}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -661,11 +694,12 @@ export const SplitView = memo(function SplitView({
|
||||
paneTabIds={paneTabIds}
|
||||
tabs={tabs}
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
focusedPaneIndex={focusedPaneIndex ?? null}
|
||||
onColDivider={onColDivider}
|
||||
onColDividerTouch={onColDividerTouch}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
onPaneClick={onPaneClick}
|
||||
onAssignPane={onAssignPane}
|
||||
/>
|
||||
<RowDivider
|
||||
onMouseDown={(e) => onRowDivider(e, 0)}
|
||||
@@ -679,11 +713,12 @@ export const SplitView = memo(function SplitView({
|
||||
paneTabIds={paneTabIds}
|
||||
tabs={tabs}
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
focusedPaneIndex={focusedPaneIndex ?? null}
|
||||
onColDivider={onColDivider}
|
||||
onColDividerTouch={onColDividerTouch}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
onPaneClick={onPaneClick}
|
||||
onAssignPane={onAssignPane}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -698,11 +733,12 @@ export const SplitView = memo(function SplitView({
|
||||
paneTabIds={paneTabIds}
|
||||
tabs={tabs}
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
focusedPaneIndex={focusedPaneIndex ?? null}
|
||||
onColDivider={onColDivider}
|
||||
onColDividerTouch={onColDividerTouch}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
onPaneClick={onPaneClick}
|
||||
onAssignPane={onAssignPane}
|
||||
/>
|
||||
<RowDivider
|
||||
onMouseDown={(e) => onRowDivider(e, 0)}
|
||||
@@ -716,11 +752,12 @@ export const SplitView = memo(function SplitView({
|
||||
paneTabIds={paneTabIds}
|
||||
tabs={tabs}
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
focusedPaneIndex={focusedPaneIndex ?? null}
|
||||
onColDivider={onColDivider}
|
||||
onColDividerTouch={onColDividerTouch}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
onPaneClick={onPaneClick}
|
||||
onAssignPane={onAssignPane}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+170
-4
@@ -1,4 +1,5 @@
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/button";
|
||||
import { Separator } from "@/components/separator";
|
||||
import {
|
||||
@@ -6,31 +7,57 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/dropdown-menu";
|
||||
import { ChevronDown, ChevronUp, RefreshCw, X } from "lucide-react";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
RefreshCw,
|
||||
X,
|
||||
LayoutPanelLeft,
|
||||
Plus,
|
||||
Minus,
|
||||
} from "lucide-react";
|
||||
import { tabIcon } from "@/shell/tabUtils";
|
||||
import type { Tab, TabType } from "@/types/ui-types";
|
||||
import type { Tab, TabType, SplitMode } from "@/types/ui-types";
|
||||
import { SPLIT_MODES, PANE_COUNTS } from "@/lib/theme";
|
||||
|
||||
const CONNECTION_TAB_TYPES: TabType[] = ["terminal", "rdp", "vnc", "telnet"];
|
||||
|
||||
export function TabBar({
|
||||
tabs,
|
||||
activeTabId,
|
||||
splitMode,
|
||||
paneTabIds,
|
||||
focusedPaneIndex,
|
||||
onSetActiveTab,
|
||||
onCloseTab,
|
||||
onRefreshTab,
|
||||
onReorderTabs,
|
||||
onSplitTab,
|
||||
onAddToSplit,
|
||||
onRemoveFromSplit,
|
||||
}: {
|
||||
tabs: Tab[];
|
||||
activeTabId: string;
|
||||
splitMode: SplitMode;
|
||||
paneTabIds: (string | null)[];
|
||||
focusedPaneIndex: number | null;
|
||||
onSetActiveTab: (id: string) => void;
|
||||
onCloseTab: (id: string) => void;
|
||||
onRefreshTab: (id: string) => void;
|
||||
onReorderTabs: (tabs: Tab[]) => void;
|
||||
onSplitTab: (tabId: string, mode: SplitMode) => void;
|
||||
onAddToSplit: (tabId: string) => void;
|
||||
onRemoveFromSplit: (tabId: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(true);
|
||||
const [dragTabId, setDragTabId] = useState<string | null>(null);
|
||||
const [dragTargetIndex, setDragTargetIndex] = useState<number | null>(null);
|
||||
const [dragPos, setDragPos] = useState<{ x: number; y: number } | null>(null);
|
||||
const [contextTabId, setContextTabId] = useState<string | null>(null);
|
||||
const [contextPos, setContextPos] = useState<{ x: number; y: number } | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const tabBarRef = useRef<HTMLDivElement>(null);
|
||||
const tabEls = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
@@ -49,6 +76,9 @@ export function TabBar({
|
||||
const dragTargetRef = useRef<number | null>(null);
|
||||
const didDrag = useRef(false);
|
||||
|
||||
const isSplit = splitMode !== "none";
|
||||
const paneCount = PANE_COUNTS[splitMode];
|
||||
|
||||
useEffect(() => {
|
||||
const el = tabBarRef.current;
|
||||
if (!el) return;
|
||||
@@ -123,6 +153,18 @@ export function TabBar({
|
||||
};
|
||||
}, [dragTabId, tabs, onReorderTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!contextTabId) return;
|
||||
function onDown(e: MouseEvent) {
|
||||
if (!(e.target as HTMLElement).closest("[data-context-menu]")) {
|
||||
setContextTabId(null);
|
||||
setContextPos(null);
|
||||
}
|
||||
}
|
||||
window.addEventListener("mousedown", onDown);
|
||||
return () => window.removeEventListener("mousedown", onDown);
|
||||
}, [contextTabId]);
|
||||
|
||||
const dragIdx = tabs.findIndex((t) => t.id === dragTabId);
|
||||
const target = dragTargetIndex ?? dragIdx;
|
||||
|
||||
@@ -138,6 +180,9 @@ export function TabBar({
|
||||
{tabs.map((tab, index) => {
|
||||
const active = tab.id === activeTabId;
|
||||
const isDragging = dragTabId === tab.id;
|
||||
const paneIdx = paneTabIds.indexOf(tab.id);
|
||||
const isInPane = paneIdx !== -1;
|
||||
const isFocusedPane = isInPane && paneIdx === focusedPaneIndex;
|
||||
let translateX = 0;
|
||||
if (
|
||||
dragTabId &&
|
||||
@@ -153,6 +198,10 @@ export function TabBar({
|
||||
else if (dragIdx > target && index < dragIdx && index >= target)
|
||||
translateX = draggedWidth;
|
||||
}
|
||||
|
||||
const showFocusIndicator = isFocusedPane && isSplit;
|
||||
const showInPaneIndicator = isInPane && isSplit && !isFocusedPane;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={tab.id}
|
||||
@@ -160,6 +209,12 @@ export function TabBar({
|
||||
if (el) tabEls.current.set(tab.id, el);
|
||||
else tabEls.current.delete(tab.id);
|
||||
}}
|
||||
draggable={isSplit && tab.type !== "dashboard"}
|
||||
onDragStart={(e) => {
|
||||
if (!isSplit || tab.type === "dashboard") return;
|
||||
e.dataTransfer.setData("text/plain", tab.id);
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
}}
|
||||
onClick={() =>
|
||||
!dragTabId && !didDrag.current && onSetActiveTab(tab.id)
|
||||
}
|
||||
@@ -169,6 +224,12 @@ export function TabBar({
|
||||
onCloseTab(tab.id);
|
||||
}
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
if (tab.type === "dashboard") return;
|
||||
e.preventDefault();
|
||||
setContextTabId(tab.id);
|
||||
setContextPos({ x: e.clientX, y: e.clientY });
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
if (e.button !== 0 || tab.type === "dashboard") return;
|
||||
e.preventDefault();
|
||||
@@ -210,7 +271,7 @@ export function TabBar({
|
||||
: "grab",
|
||||
userSelect: "none",
|
||||
}}
|
||||
className={`group/tab flex items-center gap-2 shrink-0 transition-colors border-r border-border text-sm
|
||||
className={`group/tab relative flex items-center gap-2 shrink-0 transition-colors border-r border-border text-sm
|
||||
${index === 0 && tab.type !== "dashboard" ? "border-l border-border" : ""}
|
||||
${
|
||||
tab.type === "dashboard"
|
||||
@@ -218,6 +279,14 @@ export function TabBar({
|
||||
: `px-2.5 md:px-4 font-medium ${active ? "border-b-2 border-b-accent-brand bg-surface text-foreground" : "text-muted-foreground hover:text-foreground hover:bg-surface"}`
|
||||
}`}
|
||||
>
|
||||
{/* Focused-pane indicator: brand accent bottom border overlay */}
|
||||
{showFocusIndicator && (
|
||||
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent-brand/70 z-10" />
|
||||
)}
|
||||
{/* In-pane (not focused) indicator: subtle dot */}
|
||||
{showInPaneIndicator && (
|
||||
<span className="absolute bottom-0.5 left-1/2 -translate-x-1/2 size-1 rounded-full bg-muted-foreground/40 z-10" />
|
||||
)}
|
||||
{tabIcon(tab.type)}
|
||||
{tab.type !== "dashboard" && tab.label}
|
||||
{tab.type !== "dashboard" && (
|
||||
@@ -312,7 +381,7 @@ export function TabBar({
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{tabIcon(tab.type)}
|
||||
<span className="truncate">
|
||||
{tab.type === "dashboard" ? "Dashboard" : tab.label}
|
||||
{tab.type === "dashboard" ? t("nav.dashboard") : tab.label}
|
||||
</span>
|
||||
</div>
|
||||
{tab.type !== "dashboard" && (
|
||||
@@ -351,6 +420,103 @@ export function TabBar({
|
||||
<ChevronDown className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Right-click context menu */}
|
||||
{contextTabId &&
|
||||
contextPos &&
|
||||
(() => {
|
||||
const ctxTab = tabs.find((t) => t.id === contextTabId);
|
||||
if (!ctxTab) return null;
|
||||
const isInPane = paneTabIds.indexOf(contextTabId) !== -1;
|
||||
const hasEmptySlot =
|
||||
isSplit && paneTabIds.slice(0, paneCount).some((p) => p === null);
|
||||
return (
|
||||
<div
|
||||
data-context-menu
|
||||
style={{
|
||||
position: "fixed",
|
||||
left: contextPos.x,
|
||||
top: contextPos.y,
|
||||
zIndex: 10000,
|
||||
}}
|
||||
className="bg-popover border border-border shadow-lg py-1 min-w-[180px]"
|
||||
>
|
||||
<div className="px-2 py-1 text-xs font-semibold text-muted-foreground truncate max-w-[200px]">
|
||||
{ctxTab.label}
|
||||
</div>
|
||||
<div className="h-px bg-border my-1" />
|
||||
{CONNECTION_TAB_TYPES.includes(ctxTab.type) && (
|
||||
<button
|
||||
className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-left hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => {
|
||||
onRefreshTab(contextTabId);
|
||||
setContextTabId(null);
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="size-3" />
|
||||
Refresh connection
|
||||
</button>
|
||||
)}
|
||||
<div className="h-px bg-border my-1" />
|
||||
{/* Split submenu */}
|
||||
<div className="px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Split
|
||||
</div>
|
||||
{SPLIT_MODES.filter((m) => m.id !== "none").map((mode) => (
|
||||
<button
|
||||
key={mode.id}
|
||||
className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-left hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => {
|
||||
onSplitTab(contextTabId, mode.id);
|
||||
setContextTabId(null);
|
||||
}}
|
||||
>
|
||||
<LayoutPanelLeft className="size-3 text-muted-foreground" />
|
||||
{mode.label}
|
||||
</button>
|
||||
))}
|
||||
{isSplit && (
|
||||
<>
|
||||
<div className="h-px bg-border my-1" />
|
||||
{isInPane ? (
|
||||
<button
|
||||
className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-left hover:bg-accent hover:text-accent-foreground text-muted-foreground"
|
||||
onClick={() => {
|
||||
onRemoveFromSplit(contextTabId);
|
||||
setContextTabId(null);
|
||||
}}
|
||||
>
|
||||
<Minus className="size-3" />
|
||||
Remove from split
|
||||
</button>
|
||||
) : hasEmptySlot ? (
|
||||
<button
|
||||
className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-left hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => {
|
||||
onAddToSplit(contextTabId);
|
||||
setContextTabId(null);
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
Add to split
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
<div className="h-px bg-border my-1" />
|
||||
<button
|
||||
className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-left hover:bg-accent hover:text-accent-foreground text-destructive"
|
||||
onClick={() => {
|
||||
onCloseTab(contextTabId);
|
||||
setContextTabId(null);
|
||||
}}
|
||||
>
|
||||
<X className="size-3" />
|
||||
Close tab
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,9 +40,6 @@ interface TabContextType {
|
||||
|
||||
const TabContext = createContext<TabContextType | undefined>(undefined);
|
||||
|
||||
type ElectronWindow = Window & {
|
||||
electronAPI?: unknown;
|
||||
};
|
||||
|
||||
export function useTabs() {
|
||||
const context = useContext(TabContext);
|
||||
@@ -79,111 +76,19 @@ interface TabProviderProps {
|
||||
export function clearTermixSessionStorage() {
|
||||
localStorage.removeItem("termix_tabs");
|
||||
localStorage.removeItem("termix_currentTab");
|
||||
const keysToRemove: string[] = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key?.startsWith("termix_session_")) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach((k) => localStorage.removeItem(k));
|
||||
}
|
||||
|
||||
export function TabProvider({ children }: TabProviderProps) {
|
||||
const { t } = useTranslation();
|
||||
const [tabs, setTabs] = useState<Tab[]>(() => {
|
||||
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
|
||||
const isElectron =
|
||||
typeof window !== "undefined" && !!(window as ElectronWindow).electronAPI;
|
||||
const persistenceEnabled =
|
||||
localStorage.getItem("enableTerminalSessionPersistence") !== "false";
|
||||
const shouldRestore = isMobile || isElectron || persistenceEnabled;
|
||||
|
||||
if (!shouldRestore) {
|
||||
return [{ id: 1, type: "home", title: "Home" }];
|
||||
}
|
||||
|
||||
try {
|
||||
const saved = localStorage.getItem("termix_tabs");
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved) as Tab[];
|
||||
const restored: Tab[] = [{ id: 1, type: "home", title: "Home" }];
|
||||
let maxId = 1;
|
||||
for (const tab of parsed) {
|
||||
if (tab.type === "home") continue;
|
||||
const restoredTab: Tab = {
|
||||
...tab,
|
||||
instanceId: tab.instanceId,
|
||||
terminalRef:
|
||||
tab.type === "terminal"
|
||||
? React.createRef<TerminalRefHandle>()
|
||||
: undefined,
|
||||
hostConfig: tab.hostConfig
|
||||
? {
|
||||
...tab.hostConfig,
|
||||
instanceId: tab.instanceId,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
restored.push(restoredTab);
|
||||
if (tab.id > maxId) maxId = tab.id;
|
||||
}
|
||||
if (restored.length > 1) return restored;
|
||||
}
|
||||
} catch {
|
||||
/* ignore corrupt data */
|
||||
}
|
||||
return [{ id: 1, type: "home", title: "Home" }];
|
||||
});
|
||||
const [currentTab, setCurrentTab] = useState<number>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem("termix_currentTab");
|
||||
if (saved) {
|
||||
const parsed = parseInt(saved, 10);
|
||||
if (parsed && tabs.some((t) => t.id === parsed)) return parsed;
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
const [tabs, setTabs] = useState<Tab[]>([{ id: 1, type: "home", title: "Home" }]);
|
||||
const [currentTab, setCurrentTab] = useState<number>(1);
|
||||
const [allSplitScreenTab, setAllSplitScreenTab] = useState<number[]>([]);
|
||||
const [previewTerminalTheme, setPreviewTerminalTheme] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [initialMaxId] = useState(() => {
|
||||
let maxId = 1;
|
||||
tabs.forEach((tab) => {
|
||||
if (tab.id > maxId) maxId = tab.id;
|
||||
});
|
||||
return maxId + 1;
|
||||
});
|
||||
const [initialMaxId] = useState(2);
|
||||
const nextTabId = useRef(initialMaxId);
|
||||
|
||||
useEffect(() => {
|
||||
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
|
||||
const isElectron =
|
||||
typeof window !== "undefined" && !!(window as ElectronWindow).electronAPI;
|
||||
const persistenceEnabled =
|
||||
localStorage.getItem("enableTerminalSessionPersistence") !== "false";
|
||||
const shouldSave = isMobile || isElectron || persistenceEnabled;
|
||||
|
||||
if (shouldSave) {
|
||||
const serializable = tabs
|
||||
.filter((t) => t.type !== "home")
|
||||
.map((tab) => {
|
||||
const rest = { ...tab };
|
||||
delete rest.terminalRef;
|
||||
return rest;
|
||||
});
|
||||
localStorage.setItem("termix_tabs", JSON.stringify(serializable));
|
||||
localStorage.setItem("termix_currentTab", String(currentTab));
|
||||
} else {
|
||||
localStorage.removeItem("termix_tabs");
|
||||
localStorage.removeItem("termix_currentTab");
|
||||
}
|
||||
}, [tabs, currentTab]);
|
||||
|
||||
// Safety net: if currentTab points to a tab that no longer exists, fall back to home
|
||||
useEffect(() => {
|
||||
if (tabs.length > 0 && !tabs.some((t) => t.id === currentTab)) {
|
||||
|
||||
@@ -132,6 +132,8 @@ function TerminalTabContent({
|
||||
{
|
||||
...hostToSSHHost(host),
|
||||
sshPort: host.sshPort ?? host.port,
|
||||
instanceId: tab.instanceId ?? tab.id,
|
||||
restoredSessionId: tab.restoredSessionId ?? null,
|
||||
} as any
|
||||
}
|
||||
isVisible={isVisible}
|
||||
@@ -229,7 +231,13 @@ export function renderTabContent(
|
||||
return (
|
||||
<EmptyState icon={Monitor} messageKey="guacamole.noHostSelected" />
|
||||
);
|
||||
return <GuacamoleApp hostId={host.id} tabId={tab.id} />;
|
||||
return (
|
||||
<GuacamoleApp
|
||||
hostId={host.id}
|
||||
tabId={tab.id}
|
||||
protocol={tab.type as "rdp" | "vnc" | "telnet"}
|
||||
/>
|
||||
);
|
||||
|
||||
case "network_graph":
|
||||
return <NetworkGraphCard embedded={false} />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Clock,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
LayoutPanelLeft,
|
||||
Network,
|
||||
Play,
|
||||
Plug,
|
||||
Server,
|
||||
Settings,
|
||||
User,
|
||||
@@ -25,6 +26,7 @@ export type RailView =
|
||||
| "credentials"
|
||||
| "quick-connect"
|
||||
| ToolsTab
|
||||
| "connections"
|
||||
| "user-profile"
|
||||
| "admin-settings";
|
||||
|
||||
@@ -42,6 +44,7 @@ type RailItem =
|
||||
function buildRailButtons(
|
||||
splitMode: SplitMode,
|
||||
t: (key: string) => string,
|
||||
connectionCount: number,
|
||||
): RailItem[] {
|
||||
return [
|
||||
{ view: "hosts", icon: <Server size={16} />, title: t("nav.hosts") },
|
||||
@@ -51,6 +54,12 @@ function buildRailButtons(
|
||||
title: t("nav.credentials"),
|
||||
},
|
||||
{ kind: "separator" },
|
||||
{
|
||||
view: "connections",
|
||||
icon: <Plug size={16} />,
|
||||
title: t("nav.connections"),
|
||||
},
|
||||
{ kind: "separator" },
|
||||
{
|
||||
view: "quick-connect",
|
||||
icon: <Zap size={16} />,
|
||||
@@ -88,6 +97,7 @@ export function AppRail({
|
||||
railView,
|
||||
sidebarOpen,
|
||||
splitMode,
|
||||
connectionCount,
|
||||
username,
|
||||
isAdmin,
|
||||
profileDropdownOpen,
|
||||
@@ -99,6 +109,7 @@ export function AppRail({
|
||||
railView: RailView;
|
||||
sidebarOpen: boolean;
|
||||
splitMode: SplitMode;
|
||||
connectionCount: number;
|
||||
username: string;
|
||||
isAdmin: boolean;
|
||||
profileDropdownOpen: boolean;
|
||||
@@ -109,8 +120,19 @@ export function AppRail({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const railExpanded = hovered || profileDropdownOpen;
|
||||
const railButtons = buildRailButtons(splitMode, t);
|
||||
const [pinned, setPinned] = useState(
|
||||
() => localStorage.getItem("pinAppRail") === "true",
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () =>
|
||||
setPinned(localStorage.getItem("pinAppRail") === "true");
|
||||
window.addEventListener("pinAppRailChanged", handler);
|
||||
return () => window.removeEventListener("pinAppRailChanged", handler);
|
||||
}, []);
|
||||
|
||||
const railExpanded = pinned || hovered || profileDropdownOpen;
|
||||
const railButtons = buildRailButtons(splitMode, t, connectionCount);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ExternalLink, Plug, Search, X } from "lucide-react";
|
||||
import { getActiveSessions, deleteOpenTab, type ActiveSessionInfo, type OpenTabRecord } from "@/main-axios";
|
||||
import { tabIcon } from "@/shell/tabUtils";
|
||||
import type { Tab, TabType } from "@/types/ui-types";
|
||||
import { Badge } from "@/components/badge";
|
||||
import { Input } from "@/components/input";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/tooltip";
|
||||
|
||||
const CONNECTION_TAB_TYPES: TabType[] = [
|
||||
"terminal",
|
||||
"rdp",
|
||||
"vnc",
|
||||
"telnet",
|
||||
"files",
|
||||
"docker",
|
||||
"stats",
|
||||
"tunnel",
|
||||
];
|
||||
|
||||
const TYPE_LABELS: Record<string, string> = {
|
||||
terminal: "SSH",
|
||||
rdp: "RDP",
|
||||
vnc: "VNC",
|
||||
telnet: "Telnet",
|
||||
files: "Files",
|
||||
docker: "Docker",
|
||||
stats: "Stats",
|
||||
tunnel: "Tunnel",
|
||||
};
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
const s = Math.max(0, Math.floor(ms / 1000));
|
||||
if (s < 60) return `${s}s`;
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m}m ${s % 60}s`;
|
||||
const h = Math.floor(m / 60);
|
||||
return `${h}h ${m % 60}m`;
|
||||
}
|
||||
|
||||
function formatExpiry(updatedAt: string): string {
|
||||
const TTL_MS = 30 * 60 * 1000;
|
||||
const elapsed = Date.now() - new Date(updatedAt).getTime();
|
||||
const remaining = TTL_MS - elapsed;
|
||||
if (remaining <= 0) return "expiring";
|
||||
return formatDuration(remaining);
|
||||
}
|
||||
|
||||
function ConnectionRow({
|
||||
isActive,
|
||||
isLive,
|
||||
tabType,
|
||||
name,
|
||||
subLabel,
|
||||
icon,
|
||||
onSwitch,
|
||||
onClose,
|
||||
switchTitle,
|
||||
faded,
|
||||
}: {
|
||||
isActive?: boolean;
|
||||
isLive: boolean;
|
||||
tabType: string;
|
||||
name: string;
|
||||
subLabel: string;
|
||||
icon: React.ReactNode;
|
||||
onSwitch?: () => void;
|
||||
onClose: () => void;
|
||||
switchTitle?: string;
|
||||
faded?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
role={onSwitch ? "button" : undefined}
|
||||
tabIndex={onSwitch ? 0 : undefined}
|
||||
onClick={onSwitch}
|
||||
onKeyDown={(e) => e.key === "Enter" && onSwitch?.()}
|
||||
className={`group flex items-center gap-2.5 px-3 py-2.5 border-b border-border/40 transition-colors last:border-b-0 ${
|
||||
faded ? "opacity-60" : ""
|
||||
} ${
|
||||
isActive
|
||||
? "bg-accent-brand/8 cursor-pointer border-l-2 border-l-accent-brand"
|
||||
: onSwitch
|
||||
? "hover:bg-muted/40 cursor-pointer"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`shrink-0 flex items-center justify-center size-7 rounded ${
|
||||
isActive
|
||||
? "bg-accent-brand/15 text-accent-brand"
|
||||
: "bg-muted/60 text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col flex-1 min-w-0 gap-0.5">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<span
|
||||
className={`shrink-0 size-1.5 rounded-full ${
|
||||
isLive ? "bg-green-500" : "bg-muted-foreground/30"
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`text-xs font-semibold truncate flex-1 ${
|
||||
isActive ? "text-accent-brand" : "text-foreground"
|
||||
}`}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[9px] px-1 py-0 h-4 font-mono shrink-0 text-muted-foreground/60 border-border/60"
|
||||
>
|
||||
{TYPE_LABELS[tabType] ?? tabType}
|
||||
</Badge>
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground/60 truncate pl-3">
|
||||
{subLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<TooltipProvider>
|
||||
<div className="shrink-0 flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{switchTitle && onSwitch && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onSwitch(); }}
|
||||
className="size-6 flex items-center justify-center text-muted-foreground/50 hover:text-foreground hover:bg-muted/60 rounded transition-colors"
|
||||
>
|
||||
<ExternalLink className="size-3" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">{switchTitle}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onClose(); }}
|
||||
className="size-6 flex items-center justify-center text-muted-foreground/50 hover:text-destructive hover:bg-destructive/10 rounded transition-colors"
|
||||
>
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionHeader({ label, count }: { label: string; count: number }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/60 bg-muted/20">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 flex-1">
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-[10px] font-semibold text-muted-foreground/40 bg-muted/60 rounded px-1.5 py-0.5">
|
||||
{count}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConnectionsPanel({
|
||||
tabs,
|
||||
activeTabId,
|
||||
allHosts,
|
||||
backgroundTabRecords,
|
||||
onSwitchToTab,
|
||||
onCloseTab,
|
||||
onReopenTab,
|
||||
onForgetBackground,
|
||||
}: {
|
||||
tabs: Tab[];
|
||||
activeTabId: string;
|
||||
allHosts: { id: string; name: string }[];
|
||||
backgroundTabRecords: OpenTabRecord[];
|
||||
onSwitchToTab: (tabId: string) => void;
|
||||
onCloseTab: (tabId: string) => void;
|
||||
onReopenTab: (record: OpenTabRecord, restoredSessionId: string | null) => void;
|
||||
onForgetBackground: (recordId: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [now, setNow] = useState(Date.now());
|
||||
const [activeSessions, setActiveSessions] = useState<ActiveSessionInfo[]>([]);
|
||||
const [search, setSearch] = useState("");
|
||||
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const openTabs = tabs.filter((tab) => CONNECTION_TAB_TYPES.includes(tab.type));
|
||||
|
||||
// Filter background records to only those not already open in the tab bar
|
||||
const openInstanceIds = new Set(tabs.map((t) => t.instanceId).filter(Boolean));
|
||||
const backgroundTabs = backgroundTabRecords.filter((r) => !openInstanceIds.has(r.id));
|
||||
|
||||
const q = search.trim().toLowerCase();
|
||||
const filteredOpenTabs = q
|
||||
? openTabs.filter((tab) => (tab.host?.name ?? tab.label).toLowerCase().includes(q))
|
||||
: openTabs;
|
||||
const filteredBackgroundTabs = q
|
||||
? backgroundTabs.filter((r) => {
|
||||
const host = allHosts.find((h) => h.id === String(r.hostId));
|
||||
return (host?.name ?? r.label).toLowerCase().includes(q);
|
||||
})
|
||||
: backgroundTabs;
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const sessions = await getActiveSessions();
|
||||
setActiveSessions((prev) => {
|
||||
const next = Array.isArray(sessions) ? sessions : [];
|
||||
if (JSON.stringify(prev) === JSON.stringify(next)) return prev;
|
||||
return next;
|
||||
});
|
||||
} catch {
|
||||
// silently ignore
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
pollTimerRef.current = setInterval(refresh, 5000);
|
||||
return () => { if (pollTimerRef.current) clearInterval(pollTimerRef.current); };
|
||||
}, [refresh]);
|
||||
|
||||
const sessionByInstanceId = new Map(activeSessions.map((s) => [s.tabInstanceId, s]));
|
||||
|
||||
const hasAnything = openTabs.length > 0 || backgroundTabs.length > 0;
|
||||
const hasResults = filteredOpenTabs.length > 0 || filteredBackgroundTabs.length > 0;
|
||||
|
||||
if (!hasAnything) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center flex-1 gap-3 p-6 text-center py-16">
|
||||
<div className="size-10 rounded-full bg-muted/40 flex items-center justify-center">
|
||||
<Plug className="size-5 text-muted-foreground/30" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm font-semibold text-muted-foreground/60">
|
||||
{t("connections.noConnections")}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground/40">
|
||||
{t("connections.noConnectionsDesc")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="relative px-3 py-2 border-b border-border/60">
|
||||
<Search className="absolute left-5.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground/50 pointer-events-none" />
|
||||
<Input
|
||||
placeholder={t("connections.search")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-8 h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!hasResults && (
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-10 text-center">
|
||||
<span className="text-xs text-muted-foreground/50">{t("connections.noSearchResults")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredOpenTabs.length > 0 && (
|
||||
<div className="flex flex-col">
|
||||
<SectionHeader label={t("connections.sectionOpen")} count={filteredOpenTabs.length} />
|
||||
{filteredOpenTabs.map((tab) => {
|
||||
const isActive = tab.id === activeTabId;
|
||||
const liveSession = tab.instanceId ? sessionByInstanceId.get(tab.instanceId) : undefined;
|
||||
const isLive = tab.type === "terminal"
|
||||
? (liveSession?.isConnected ?? false)
|
||||
: true;
|
||||
const duration = liveSession?.createdAt
|
||||
? formatDuration(now - liveSession.createdAt)
|
||||
: formatDuration(now - tab.openedAt);
|
||||
|
||||
return (
|
||||
<ConnectionRow
|
||||
key={tab.id}
|
||||
isActive={isActive}
|
||||
isLive={isLive}
|
||||
tabType={tab.type}
|
||||
name={tab.host?.name ?? tab.label}
|
||||
subLabel={
|
||||
isLive && tab.type === "terminal"
|
||||
? t("connections.connectedFor", { duration })
|
||||
: isLive
|
||||
? t("connections.connected")
|
||||
: t("connections.disconnected")
|
||||
}
|
||||
icon={tabIcon(tab.type)}
|
||||
onSwitch={() => onSwitchToTab(tab.id)}
|
||||
onClose={() => onCloseTab(tab.id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredBackgroundTabs.length > 0 && (
|
||||
<div className={`flex flex-col ${filteredOpenTabs.length > 0 ? "mt-2" : ""}`}>
|
||||
<SectionHeader label={t("connections.sectionBackground")} count={filteredBackgroundTabs.length} />
|
||||
<div className="px-3 py-1.5 border-b border-border/40">
|
||||
<span className="text-[10px] text-muted-foreground/50">
|
||||
{t("connections.backgroundDesc")}
|
||||
</span>
|
||||
</div>
|
||||
{filteredBackgroundTabs.map((record) => {
|
||||
const host = record.hostId
|
||||
? allHosts.find((h) => h.id === String(record.hostId))
|
||||
: undefined;
|
||||
const expiresIn = formatExpiry(record.updatedAt);
|
||||
|
||||
return (
|
||||
<ConnectionRow
|
||||
key={record.id}
|
||||
isLive={false}
|
||||
faded
|
||||
tabType={record.tabType}
|
||||
name={host?.name ?? record.label}
|
||||
subLabel={t("connections.expiresIn", { duration: expiresIn })}
|
||||
icon={tabIcon(record.tabType as TabType)}
|
||||
onSwitch={() => {
|
||||
const liveSession = sessionByInstanceId.get(record.id);
|
||||
onReopenTab(record, liveSession?.sessionId ?? null);
|
||||
}}
|
||||
onClose={async () => {
|
||||
await deleteOpenTab(record.id).catch(() => {});
|
||||
onForgetBackground(record.id);
|
||||
}}
|
||||
switchTitle={t("connections.reconnect")}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
Lock,
|
||||
Monitor,
|
||||
Network,
|
||||
MousePointerClick,
|
||||
Palette,
|
||||
Pencil,
|
||||
Plus,
|
||||
@@ -220,7 +221,7 @@ function makeHostTabs(t: (key: string) => string): HostTab[] {
|
||||
{
|
||||
id: "vnc",
|
||||
label: t("hosts.tabVnc"),
|
||||
icon: <Monitor className="size-3" />,
|
||||
icon: <MousePointerClick className="size-3" />,
|
||||
},
|
||||
{
|
||||
id: "telnet",
|
||||
@@ -355,7 +356,7 @@ function HostEditor({
|
||||
name: host?.name ?? "",
|
||||
ip: host?.ip ?? "",
|
||||
username: host?.username ?? "",
|
||||
sshPort: host?.sshPort ?? 22,
|
||||
sshPort: host?.sshPort ?? host?.port ?? 22,
|
||||
rdpPort: host?.rdpPort ?? 3389,
|
||||
vncPort: host?.vncPort ?? 5900,
|
||||
telnetPort: host?.telnetPort ?? 23,
|
||||
@@ -429,8 +430,8 @@ function HostEditor({
|
||||
autoTmux: host?.terminalConfig?.autoTmux ?? false,
|
||||
sudoPasswordAutoFill: host?.terminalConfig?.sudoPasswordAutoFill ?? false,
|
||||
sudoPassword: host?.terminalConfig?.sudoPassword ?? "",
|
||||
keepaliveInterval: host?.terminalConfig?.keepaliveInterval ?? 30,
|
||||
keepaliveCountMax: host?.terminalConfig?.keepaliveCountMax ?? 3,
|
||||
keepaliveInterval: host?.terminalConfig?.keepaliveInterval ?? 60,
|
||||
keepaliveCountMax: host?.terminalConfig?.keepaliveCountMax ?? 5,
|
||||
environmentVariables:
|
||||
host?.terminalConfig?.environmentVariables ??
|
||||
([] as { key: string; value: string }[]),
|
||||
@@ -687,7 +688,7 @@ function HostEditor({
|
||||
proto: "enableVnc" as const,
|
||||
label: t("hosts.tabVnc"),
|
||||
desc: t("hosts.virtualNetwork"),
|
||||
icon: <Monitor className="size-4" />,
|
||||
icon: <MousePointerClick className="size-4" />,
|
||||
portField: "vncPort" as const,
|
||||
},
|
||||
{
|
||||
@@ -4702,7 +4703,7 @@ export function HostManager({
|
||||
return false;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const reloadHosts = () => {
|
||||
getSSHHosts()
|
||||
.then((raw) => {
|
||||
const converted = raw.map(sshHostToHost);
|
||||
@@ -4710,6 +4711,10 @@ export function HostManager({
|
||||
applyPendingEdit(converted);
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reloadHosts();
|
||||
getCredentials()
|
||||
.then((res: any) => {
|
||||
const arr = Array.isArray(res) ? res : [];
|
||||
@@ -4728,6 +4733,10 @@ export function HostManager({
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setCredentialsLoading(false));
|
||||
|
||||
window.addEventListener("termix:hosts-changed", reloadHosts);
|
||||
return () =>
|
||||
window.removeEventListener("termix:hosts-changed", reloadHosts);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -4748,6 +4757,7 @@ export function HostManager({
|
||||
} else if (action === "add-credential") {
|
||||
setEditingCredential("new");
|
||||
setEditingHost(null);
|
||||
setActiveCredentialTab("general");
|
||||
}
|
||||
}
|
||||
}, [pendingEditId, pendingAction]);
|
||||
@@ -4769,6 +4779,7 @@ export function HostManager({
|
||||
const handleAddCredential = () => {
|
||||
setEditingCredential("new");
|
||||
setEditingHost(null);
|
||||
setActiveCredentialTab("general");
|
||||
};
|
||||
const handleEditHost = (e: Event) => {
|
||||
const id = (e as CustomEvent<string>).detail;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ArrowUpDown,
|
||||
Check,
|
||||
Download,
|
||||
Filter,
|
||||
ListChecks,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
@@ -10,20 +13,137 @@ import {
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { SidebarTree } from "@/sidebar/SidebarTree";
|
||||
import { SidebarTree, isFolder } from "@/sidebar/SidebarTree";
|
||||
import { HostManager } from "@/sidebar/HostManager";
|
||||
import { HostShareModal } from "@/sidebar/HostShareModal";
|
||||
import { Button } from "@/components/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/dropdown-menu";
|
||||
import { getSSHHosts, bulkImportSSHHosts } from "@/main-axios";
|
||||
import type { SSHHostWithStatus } from "@/main-axios";
|
||||
import type { Host, HostFolder, TabType } from "@/types/ui-types";
|
||||
|
||||
type SortKey =
|
||||
| "default"
|
||||
| "name-asc"
|
||||
| "name-desc"
|
||||
| "ip-asc"
|
||||
| "ip-desc"
|
||||
| "status-online"
|
||||
| "status-offline"
|
||||
| "pinned";
|
||||
|
||||
type FilterState = {
|
||||
status: ("online" | "offline" | "pinned")[];
|
||||
authType: ("password" | "key" | "credential" | "none" | "opkssh")[];
|
||||
protocol: ("ssh" | "rdp" | "vnc" | "telnet")[];
|
||||
features: ("terminal" | "fileManager" | "tunnel" | "docker")[];
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
const DEFAULT_FILTERS: FilterState = {
|
||||
status: [],
|
||||
authType: [],
|
||||
protocol: [],
|
||||
features: [],
|
||||
tags: [],
|
||||
};
|
||||
|
||||
function sortHostTree(folder: HostFolder, key: SortKey): HostFolder {
|
||||
if (key === "default") return folder;
|
||||
|
||||
const comparator = (a: Host | HostFolder, b: Host | HostFolder): number => {
|
||||
const aIsFolder = isFolder(a);
|
||||
const bIsFolder = isFolder(b);
|
||||
if (aIsFolder && !bIsFolder) return -1;
|
||||
if (!aIsFolder && bIsFolder) return 1;
|
||||
if (aIsFolder && bIsFolder)
|
||||
return (a as HostFolder).name.localeCompare((b as HostFolder).name);
|
||||
const ha = a as Host,
|
||||
hb = b as Host;
|
||||
switch (key) {
|
||||
case "name-asc":
|
||||
return ha.name.localeCompare(hb.name);
|
||||
case "name-desc":
|
||||
return hb.name.localeCompare(ha.name);
|
||||
case "ip-asc":
|
||||
return ha.ip.localeCompare(hb.ip);
|
||||
case "ip-desc":
|
||||
return hb.ip.localeCompare(ha.ip);
|
||||
case "status-online":
|
||||
return (hb.online ? 1 : 0) - (ha.online ? 1 : 0);
|
||||
case "status-offline":
|
||||
return (ha.online ? 1 : 0) - (hb.online ? 1 : 0);
|
||||
case "pinned":
|
||||
return (hb.pin ? 1 : 0) - (ha.pin ? 1 : 0);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const sortedChildren = [...folder.children]
|
||||
.sort(comparator)
|
||||
.map((child) => (isFolder(child) ? sortHostTree(child, key) : child));
|
||||
return { ...folder, children: sortedChildren };
|
||||
}
|
||||
|
||||
function hostPassesFilters(host: Host, filters: FilterState): boolean {
|
||||
if (filters.status.length > 0) {
|
||||
const ok =
|
||||
(filters.status.includes("online") && host.online) ||
|
||||
(filters.status.includes("offline") && !host.online) ||
|
||||
(filters.status.includes("pinned") && !!host.pin);
|
||||
if (!ok) return false;
|
||||
}
|
||||
if (filters.authType.length > 0) {
|
||||
if (!filters.authType.includes(host.authType as FilterState["authType"][number])) return false;
|
||||
}
|
||||
if (filters.protocol.length > 0) {
|
||||
const ok =
|
||||
(filters.protocol.includes("ssh") && host.enableSsh) ||
|
||||
(filters.protocol.includes("rdp") && host.enableRdp) ||
|
||||
(filters.protocol.includes("vnc") && host.enableVnc) ||
|
||||
(filters.protocol.includes("telnet") && host.enableTelnet);
|
||||
if (!ok) return false;
|
||||
}
|
||||
if (filters.features.length > 0) {
|
||||
const ok =
|
||||
(filters.features.includes("terminal") && host.enableTerminal) ||
|
||||
(filters.features.includes("fileManager") && host.enableFileManager) ||
|
||||
(filters.features.includes("tunnel") && host.enableTunnel) ||
|
||||
(filters.features.includes("docker") && host.enableDocker);
|
||||
if (!ok) return false;
|
||||
}
|
||||
if (filters.tags.length > 0) {
|
||||
const ok = filters.tags.some((tag) => host.tags?.includes(tag));
|
||||
if (!ok) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyFilters(folder: HostFolder, filters: FilterState): HostFolder {
|
||||
const active = Object.values(filters).some((arr) => arr.length > 0);
|
||||
if (!active) return folder;
|
||||
|
||||
const filteredChildren = folder.children
|
||||
.map((child) => {
|
||||
if (isFolder(child)) return applyFilters(child, filters);
|
||||
return hostPassesFilters(child, filters) ? child : null;
|
||||
})
|
||||
.filter((child): child is Host | HostFolder => {
|
||||
if (child === null) return false;
|
||||
if (isFolder(child)) return child.children.length > 0;
|
||||
return true;
|
||||
});
|
||||
return { ...folder, children: filteredChildren };
|
||||
}
|
||||
|
||||
export function HostsPanel({
|
||||
onOpenTab,
|
||||
onEditHost,
|
||||
@@ -46,8 +166,46 @@ export function HostsPanel({
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [rawHosts, setRawHosts] = useState<SSHHostWithStatus[]>([]);
|
||||
const [shareModalHost, setShareModalHost] = useState<Host | null>(null);
|
||||
const [sortKey, setSortKey] = useState<SortKey>(
|
||||
() => (localStorage.getItem("hostSortKey") as SortKey) ?? "default",
|
||||
);
|
||||
const [filterState, setFilterState] = useState<FilterState>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem("hostFilterState");
|
||||
return saved ? (JSON.parse(saved) as FilterState) : DEFAULT_FILTERS;
|
||||
} catch {
|
||||
return DEFAULT_FILTERS;
|
||||
}
|
||||
});
|
||||
const filterActive = Object.values(filterState).some((arr) => arr.length > 0);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const importOverwriteRef = useRef(false);
|
||||
const allTags = [...new Set(rawHosts.flatMap((h) => h.tags ?? []))];
|
||||
|
||||
function handleSortChange(key: SortKey) {
|
||||
setSortKey(key);
|
||||
localStorage.setItem("hostSortKey", key);
|
||||
}
|
||||
|
||||
function handleFilterToggle<K extends keyof FilterState>(
|
||||
group: K,
|
||||
value: FilterState[K][number],
|
||||
) {
|
||||
setFilterState((prev) => {
|
||||
const arr = prev[group] as string[];
|
||||
const next = arr.includes(value as string)
|
||||
? arr.filter((v) => v !== value)
|
||||
: [...arr, value as string];
|
||||
const updated = { ...prev, [group]: next };
|
||||
localStorage.setItem("hostFilterState", JSON.stringify(updated));
|
||||
return updated as FilterState;
|
||||
});
|
||||
}
|
||||
|
||||
function handleFilterClear() {
|
||||
setFilterState(DEFAULT_FILTERS);
|
||||
localStorage.setItem("hostFilterState", JSON.stringify(DEFAULT_FILTERS));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getSSHHosts()
|
||||
@@ -226,8 +384,8 @@ export function HostsPanel({
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-0.5 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<div className="flex items-center gap-0.5 shrink-0 flex-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -294,6 +452,195 @@ export function HostsPanel({
|
||||
>
|
||||
<ListChecks className="size-3.5" />
|
||||
</button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={`size-7 ${sortKey !== "default" ? "text-accent-brand" : "text-muted-foreground hover:text-foreground"}`}
|
||||
title={t("hosts.sortHosts")}
|
||||
>
|
||||
<ArrowUpDown className="size-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="text-xs min-w-[160px]"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleSortChange("default")}
|
||||
className="flex items-center gap-1.5"
|
||||
>
|
||||
{sortKey === "default" ? (
|
||||
<Check className="size-3 shrink-0 text-accent-brand" />
|
||||
) : (
|
||||
<span className="size-3 shrink-0 inline-block" />
|
||||
)}
|
||||
{t("hosts.sortDefault")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{(["name-asc", "name-desc"] as const).map((key) => (
|
||||
<DropdownMenuItem
|
||||
key={key}
|
||||
onClick={() => handleSortChange(key)}
|
||||
className="flex items-center gap-1.5"
|
||||
>
|
||||
{sortKey === key ? (
|
||||
<Check className="size-3 shrink-0 text-accent-brand" />
|
||||
) : (
|
||||
<span className="size-3 shrink-0 inline-block" />
|
||||
)}
|
||||
{t(
|
||||
`hosts.sort${key === "name-asc" ? "NameAsc" : "NameDesc"}`,
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
{(["ip-asc", "ip-desc"] as const).map((key) => (
|
||||
<DropdownMenuItem
|
||||
key={key}
|
||||
onClick={() => handleSortChange(key)}
|
||||
className="flex items-center gap-1.5"
|
||||
>
|
||||
{sortKey === key ? (
|
||||
<Check className="size-3 shrink-0 text-accent-brand" />
|
||||
) : (
|
||||
<span className="size-3 shrink-0 inline-block" />
|
||||
)}
|
||||
{t(`hosts.sort${key === "ip-asc" ? "IpAsc" : "IpDesc"}`)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
{(["status-online", "status-offline", "pinned"] as const).map(
|
||||
(key) => (
|
||||
<DropdownMenuItem
|
||||
key={key}
|
||||
onClick={() => handleSortChange(key)}
|
||||
className="flex items-center gap-1.5"
|
||||
>
|
||||
{sortKey === key ? (
|
||||
<Check className="size-3 shrink-0 text-accent-brand" />
|
||||
) : (
|
||||
<span className="size-3 shrink-0 inline-block" />
|
||||
)}
|
||||
{t(
|
||||
key === "status-online"
|
||||
? "hosts.sortOnlineFirst"
|
||||
: key === "status-offline"
|
||||
? "hosts.sortOfflineFirst"
|
||||
: "hosts.sortPinnedFirst",
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
),
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={`size-7 ${filterActive ? "text-accent-brand" : "text-muted-foreground hover:text-foreground"}`}
|
||||
title={t("hosts.filterHosts")}
|
||||
>
|
||||
<Filter className="size-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="text-xs min-w-[180px]"
|
||||
>
|
||||
{filterActive && (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onClick={handleFilterClear}
|
||||
className="flex items-center gap-1.5 text-accent-brand"
|
||||
>
|
||||
<X className="size-3 shrink-0" />
|
||||
{t("hosts.filterClearAll")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuLabel>{t("hosts.filterStatusGroup")}</DropdownMenuLabel>
|
||||
{(["online", "offline", "pinned"] as const).map((val) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={val}
|
||||
checked={filterState.status.includes(val)}
|
||||
onCheckedChange={() => handleFilterToggle("status", val)}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
{t(`hosts.filter${val.charAt(0).toUpperCase() + val.slice(1)}`)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>{t("hosts.filterAuthGroup")}</DropdownMenuLabel>
|
||||
{(["password", "key", "credential", "none", "opkssh"] as const).map((val) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={val}
|
||||
checked={filterState.authType.includes(val)}
|
||||
onCheckedChange={() => handleFilterToggle("authType", val)}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
{t(`hosts.filterAuth${val.charAt(0).toUpperCase() + val.slice(1)}`)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>{t("hosts.filterProtocolGroup")}</DropdownMenuLabel>
|
||||
{(
|
||||
[
|
||||
["ssh", "Ssh"],
|
||||
["rdp", "Rdp"],
|
||||
["vnc", "Vnc"],
|
||||
["telnet", "Telnet"],
|
||||
] as const
|
||||
).map(([val, key]) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={val}
|
||||
checked={filterState.protocol.includes(val)}
|
||||
onCheckedChange={() => handleFilterToggle("protocol", val)}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
{t(`hosts.filterProtocol${key}`)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>{t("hosts.filterFeaturesGroup")}</DropdownMenuLabel>
|
||||
{(
|
||||
[
|
||||
["terminal", "Terminal"],
|
||||
["fileManager", "FileManager"],
|
||||
["tunnel", "Tunnel"],
|
||||
["docker", "Docker"],
|
||||
] as const
|
||||
).map(([val, key]) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={val}
|
||||
checked={filterState.features.includes(val)}
|
||||
onCheckedChange={() => handleFilterToggle("features", val)}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
{t(`hosts.filterFeature${key}`)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
{allTags.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>{t("hosts.filterTagsGroup")}</DropdownMenuLabel>
|
||||
{allTags.map((tag) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={tag}
|
||||
checked={filterState.tags.includes(tag)}
|
||||
onCheckedChange={() => handleFilterToggle("tags", tag)}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
{tag}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<button
|
||||
onClick={() =>
|
||||
@@ -313,7 +660,7 @@ export function HostsPanel({
|
||||
className={`flex flex-col flex-1 min-h-0 ${managerEditing ? "hidden" : ""}`}
|
||||
>
|
||||
<SidebarTree
|
||||
children={hostTree?.children ?? []}
|
||||
children={hostTree ? applyFilters(sortHostTree(hostTree, sortKey), filterState).children : []}
|
||||
onOpenTab={onOpenTab}
|
||||
onEditHost={onEditHost}
|
||||
onShareHost={(host) => setShareModalHost(host)}
|
||||
|
||||
+154
-23
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Box,
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
MessagesSquare,
|
||||
Monitor,
|
||||
MoreHorizontal,
|
||||
MousePointerClick,
|
||||
Network,
|
||||
Pencil,
|
||||
Pin,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
Share2,
|
||||
Terminal,
|
||||
Trash2,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -35,7 +37,12 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/dropdown-menu";
|
||||
import { toast } from "sonner";
|
||||
import { bulkUpdateSSHHosts, createSSHHost, deleteSSHHost } from "@/main-axios";
|
||||
import {
|
||||
bulkUpdateSSHHosts,
|
||||
createSSHHost,
|
||||
deleteSSHHost,
|
||||
wakeOnLan,
|
||||
} from "@/main-axios";
|
||||
import type { Host, HostFolder, TabType } from "@/types/ui-types";
|
||||
|
||||
export function isFolder(item: Host | HostFolder): item is HostFolder {
|
||||
@@ -185,6 +192,8 @@ export function HostItem({
|
||||
onToggleSelect,
|
||||
isMenuOpen = false,
|
||||
onMenuOpenChange,
|
||||
isTrayOpen = false,
|
||||
onTrayOpenChange,
|
||||
}: {
|
||||
host: Host;
|
||||
onOpenTab: (type: TabType) => void;
|
||||
@@ -199,10 +208,43 @@ export function HostItem({
|
||||
onToggleSelect?: () => void;
|
||||
isMenuOpen?: boolean;
|
||||
onMenuOpenChange?: (open: boolean) => void;
|
||||
isTrayOpen?: boolean;
|
||||
onTrayOpenChange?: (open: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const metricsEnabled =
|
||||
host.enableSsh && host.statsConfig?.metricsEnabled !== false;
|
||||
const [trayOnClick, setTrayOnClick] = useState(
|
||||
() => localStorage.getItem("hostTrayOnClick") === "true",
|
||||
);
|
||||
const [showHostTags, setShowHostTags] = useState<boolean>(() => {
|
||||
const v = localStorage.getItem("showHostTags");
|
||||
return v !== null ? v === "true" : true;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () =>
|
||||
setTrayOnClick(localStorage.getItem("hostTrayOnClick") === "true");
|
||||
window.addEventListener("storage", handler);
|
||||
window.addEventListener("hostTrayOnClickChanged", handler);
|
||||
return () => {
|
||||
window.removeEventListener("storage", handler);
|
||||
window.removeEventListener("hostTrayOnClickChanged", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
const v = localStorage.getItem("showHostTags");
|
||||
setShowHostTags(v !== null ? v === "true" : true);
|
||||
};
|
||||
window.addEventListener("storage", handler);
|
||||
window.addEventListener("showHostTagsChanged", handler);
|
||||
return () => {
|
||||
window.removeEventListener("storage", handler);
|
||||
window.removeEventListener("showHostTagsChanged", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (query && !hostMatchesQuery(host, query)) return null;
|
||||
|
||||
@@ -215,11 +257,21 @@ export function HostItem({
|
||||
? "bg-muted/20"
|
||||
: ""
|
||||
} ${isMenuOpen ? "bg-muted/40" : ""}`}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
if (selectionMode) {
|
||||
onToggleSelect?.();
|
||||
return;
|
||||
}
|
||||
// On touch devices open the action tray instead of immediately launching a tab
|
||||
if (window.matchMedia("(hover: none)").matches) {
|
||||
e.stopPropagation();
|
||||
onTrayOpenChange?.(!isTrayOpen);
|
||||
return;
|
||||
}
|
||||
if (trayOnClick) {
|
||||
onTrayOpenChange?.(!isTrayOpen);
|
||||
return;
|
||||
}
|
||||
if (host.enableSsh) onOpenTab("terminal");
|
||||
else if (host.enableRdp) onOpenTab("rdp");
|
||||
else if (host.enableVnc) onOpenTab("vnc");
|
||||
@@ -253,15 +305,15 @@ export function HostItem({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Address — only visible on hover or while menu is open */}
|
||||
{/* Address — only visible on hover (or click when trayOnClick) or while menu is open */}
|
||||
<span
|
||||
className={`text-[11px] text-muted-foreground/55 truncate leading-none pl-3 transition-opacity duration-100 group-hover:opacity-100 group-hover:h-auto ${isMenuOpen ? "opacity-100 h-auto" : "opacity-0 h-0 overflow-hidden"}`}
|
||||
className={`text-[11px] text-muted-foreground/55 truncate leading-none pl-3 transition-opacity duration-100 ${!trayOnClick ? "group-hover:opacity-100 group-hover:h-auto" : ""} ${isMenuOpen || (trayOnClick && isTrayOpen) ? "opacity-100 h-auto" : "opacity-0 h-0 overflow-hidden"}`}
|
||||
>
|
||||
{host.username}@{host.ip}
|
||||
</span>
|
||||
|
||||
{/* Tag pills */}
|
||||
{host.tags && host.tags.length > 0 && (
|
||||
{showHostTags && host.tags && host.tags.length > 0 && (
|
||||
<div className="flex items-center gap-1 min-w-0 overflow-hidden pl-3">
|
||||
{host.tags.slice(0, 4).map((tag) => (
|
||||
<span
|
||||
@@ -279,9 +331,9 @@ export function HostItem({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action tray — slides open on CSS hover or while menu is open */}
|
||||
{/* Action tray — slides open on CSS hover, on click (when trayOnClick), or while menu is open */}
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-150 ease-out max-h-0 opacity-0 group-hover:max-h-[300px] group-hover:opacity-100 ${selectionMode ? "!max-h-0 !opacity-0" : ""} ${isMenuOpen && !selectionMode ? "!max-h-[300px] !opacity-100" : ""}`}
|
||||
className={`overflow-hidden transition-all duration-150 ease-out max-h-0 opacity-0 ${!trayOnClick ? "group-hover:max-h-[300px] group-hover:opacity-100" : ""} ${selectionMode ? "!max-h-0 !opacity-0" : ""} ${(isMenuOpen || (trayOnClick && isTrayOpen)) && !selectionMode ? "!max-h-[300px] !opacity-100" : ""}`}
|
||||
>
|
||||
{host.online &&
|
||||
((host.cpu != null && host.cpu > 0) ||
|
||||
@@ -360,7 +412,7 @@ export function HostItem({
|
||||
}}
|
||||
className="flex items-center justify-center size-7 rounded text-muted-foreground/50 hover:text-foreground hover:bg-muted-foreground/10 transition-colors"
|
||||
>
|
||||
<Monitor className="size-3.5" />
|
||||
<MousePointerClick className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{host.enableTelnet && (
|
||||
@@ -375,6 +427,25 @@ export function HostItem({
|
||||
<MessagesSquare className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{host.macAddress && (
|
||||
<button
|
||||
title={t("hosts.wakeOnLanAction")}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await wakeOnLan(host.id);
|
||||
toast.success(
|
||||
t("hosts.wakeOnLanSuccess", { name: host.name }),
|
||||
);
|
||||
} catch {
|
||||
toast.error(t("hosts.wakeOnLanError"));
|
||||
}
|
||||
}}
|
||||
className="flex items-center justify-center size-7 rounded text-muted-foreground/50 hover:text-foreground hover:bg-muted-foreground/10 transition-colors"
|
||||
>
|
||||
<Zap className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Separator + management buttons row — always fixed position */}
|
||||
@@ -526,7 +597,7 @@ export function HostItem({
|
||||
toast.success(t("hosts.vncUrlCopied"));
|
||||
}}
|
||||
>
|
||||
<Monitor className="size-3.5 mr-2" />
|
||||
<MousePointerClick className="size-3.5 mr-2" />
|
||||
{t("hosts.copyVncUrlAction")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
@@ -594,6 +665,8 @@ export function FolderItem({
|
||||
onToggleSelect,
|
||||
openMenuHostId,
|
||||
onMenuOpenChange,
|
||||
openTrayHostId,
|
||||
onTrayOpenChange,
|
||||
}: {
|
||||
folder: HostFolder;
|
||||
depth?: number;
|
||||
@@ -611,6 +684,8 @@ export function FolderItem({
|
||||
onToggleSelect: (id: string) => void;
|
||||
openMenuHostId: string | null;
|
||||
onMenuOpenChange: (hostId: string | null) => void;
|
||||
openTrayHostId: string | null;
|
||||
onTrayOpenChange: (hostId: string | null) => void;
|
||||
}) {
|
||||
const { total, online } = folderHostCount(folder);
|
||||
|
||||
@@ -663,6 +738,8 @@ export function FolderItem({
|
||||
onToggleSelect={onToggleSelect}
|
||||
openMenuHostId={openMenuHostId}
|
||||
onMenuOpenChange={onMenuOpenChange}
|
||||
openTrayHostId={openTrayHostId}
|
||||
onTrayOpenChange={onTrayOpenChange}
|
||||
/>
|
||||
) : (
|
||||
<HostItem
|
||||
@@ -682,6 +759,10 @@ export function FolderItem({
|
||||
onMenuOpenChange={(open) =>
|
||||
onMenuOpenChange(open ? child.id : null)
|
||||
}
|
||||
isTrayOpen={openTrayHostId === child.id}
|
||||
onTrayOpenChange={(open) =>
|
||||
onTrayOpenChange(open ? child.id : null)
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
@@ -716,6 +797,7 @@ export function SidebarTree({
|
||||
new Set(),
|
||||
);
|
||||
const [openMenuHostId, setOpenMenuHostId] = useState<string | null>(null);
|
||||
const [openTrayHostId, setOpenTrayHostId] = useState<string | null>(null);
|
||||
const [confirmDialog, setConfirmDialog] = useState<{
|
||||
message: string;
|
||||
onConfirm: () => Promise<void> | void;
|
||||
@@ -754,21 +836,64 @@ export function SidebarTree({
|
||||
|
||||
async function handleDuplicateHost(host: Host) {
|
||||
try {
|
||||
const {
|
||||
id: _id,
|
||||
online: _online,
|
||||
cpu: _cpu,
|
||||
ram: _ram,
|
||||
lastAccess: _la,
|
||||
hasKey: _hk,
|
||||
hasKeyPassword: _hkp,
|
||||
...rest
|
||||
} = host as any;
|
||||
await createSSHHost({
|
||||
...rest,
|
||||
name: `${host.name} (copy)`,
|
||||
key: undefined,
|
||||
});
|
||||
ip: host.ip,
|
||||
port: host.port,
|
||||
username: host.username,
|
||||
folder: host.folder,
|
||||
tags: host.tags ?? [],
|
||||
pin: host.pin ?? false,
|
||||
notes: host.notes,
|
||||
macAddress: host.macAddress,
|
||||
authType: host.authType,
|
||||
password: host.password ?? null,
|
||||
keyPassword: host.keyPassword ?? null,
|
||||
keyType: host.keyType ?? null,
|
||||
credentialId: host.credentialId ? Number(host.credentialId) : null,
|
||||
overrideCredentialUsername: host.overrideCredentialUsername ?? false,
|
||||
enableSsh: host.enableSsh,
|
||||
enableRdp: host.enableRdp,
|
||||
enableVnc: host.enableVnc,
|
||||
enableTelnet: host.enableTelnet,
|
||||
enableTerminal: host.enableTerminal,
|
||||
enableTunnel: host.enableTunnel,
|
||||
enableFileManager: host.enableFileManager,
|
||||
enableDocker: host.enableDocker,
|
||||
sshPort: host.sshPort,
|
||||
rdpPort: host.rdpPort,
|
||||
vncPort: host.vncPort,
|
||||
telnetPort: host.telnetPort,
|
||||
rdpUser: host.rdpUser ?? null,
|
||||
rdpPassword: host.rdpPassword ?? null,
|
||||
rdpDomain: host.domain ?? null,
|
||||
rdpSecurity: host.security ?? null,
|
||||
rdpIgnoreCert: host.ignoreCert ?? false,
|
||||
vncPassword: host.vncPassword ?? null,
|
||||
vncUser: host.vncUser ?? null,
|
||||
telnetUser: host.telnetUser ?? null,
|
||||
telnetPassword: host.telnetPassword ?? null,
|
||||
defaultPath: host.defaultPath ?? "/",
|
||||
forceKeyboardInteractive: host.forceKeyboardInteractive ?? false,
|
||||
useSocks5: host.useSocks5,
|
||||
socks5Host: host.socks5Host ?? null,
|
||||
socks5Port: host.socks5Port ?? null,
|
||||
socks5Username: host.socks5Username ?? null,
|
||||
socks5Password: host.socks5Password ?? null,
|
||||
socks5ProxyChain: host.socks5ProxyChain ?? null,
|
||||
jumpHosts: (host.jumpHosts ?? []).map((j) => ({
|
||||
hostId: Number(j.hostId),
|
||||
})),
|
||||
portKnockSequence: host.portKnockSequence ?? [],
|
||||
tunnelConnections: host.serverTunnels ?? [],
|
||||
quickActions: (host.quickActions ?? []).map((a) => ({
|
||||
name: a.name,
|
||||
snippetId: Number(a.snippetId),
|
||||
})),
|
||||
statsConfig: host.statsConfig,
|
||||
guacamoleConfig: host.guacamoleConfig ?? null,
|
||||
terminalConfig: host.terminalConfig ?? null,
|
||||
} as any);
|
||||
window.dispatchEvent(new CustomEvent("termix:hosts-changed"));
|
||||
toast.success(t("hosts.duplicatedHost", { name: host.name }));
|
||||
} catch {
|
||||
@@ -839,6 +964,8 @@ export function SidebarTree({
|
||||
onToggleSelect={toggleSelect}
|
||||
openMenuHostId={openMenuHostId}
|
||||
onMenuOpenChange={setOpenMenuHostId}
|
||||
openTrayHostId={openTrayHostId}
|
||||
onTrayOpenChange={setOpenTrayHostId}
|
||||
/>
|
||||
) : (
|
||||
<HostItem
|
||||
@@ -858,6 +985,10 @@ export function SidebarTree({
|
||||
onMenuOpenChange={(open) =>
|
||||
setOpenMenuHostId(open ? child.id : null)
|
||||
}
|
||||
isTrayOpen={openTrayHostId === child.id}
|
||||
onTrayOpenChange={(open) =>
|
||||
setOpenTrayHostId(open ? child.id : null)
|
||||
}
|
||||
/>
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useConfirmation } from "@/hooks/use-confirmation.ts";
|
||||
import {
|
||||
getSnippets,
|
||||
createSnippet as apiCreateSnippet,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
revokeSnippetAccess,
|
||||
getUserList,
|
||||
getRoles,
|
||||
reorderSnippets,
|
||||
} from "@/main-axios";
|
||||
import { Button } from "@/components/button";
|
||||
import { Input } from "@/components/input";
|
||||
@@ -34,6 +36,7 @@ import {
|
||||
Database,
|
||||
Folder,
|
||||
Globe,
|
||||
GripVertical,
|
||||
Network,
|
||||
Pencil,
|
||||
Play,
|
||||
@@ -122,7 +125,7 @@ function SnippetFormDialog({
|
||||
onOpenChange: (v: boolean) => void;
|
||||
folders: SnippetFolder[];
|
||||
snippet: Snippet | null;
|
||||
onSave: (data: Omit<Snippet, "id">, id?: number) => void;
|
||||
onSave: (data: Omit<Snippet, "id" | "order">, id?: number) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState("");
|
||||
@@ -685,6 +688,12 @@ function SnippetCard({
|
||||
onDelete,
|
||||
onEdit,
|
||||
onShare,
|
||||
onConfirmRun,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
onDragOver,
|
||||
dropIndicator,
|
||||
isDragging,
|
||||
t,
|
||||
}: {
|
||||
snippet: Snippet;
|
||||
@@ -693,9 +702,15 @@ function SnippetCard({
|
||||
onDelete: (id: number) => void;
|
||||
onEdit: (snippet: Snippet) => void;
|
||||
onShare: (snippet: Snippet) => void;
|
||||
onConfirmRun: (snippet: Snippet, execute: () => void) => void;
|
||||
onDragStart: () => void;
|
||||
onDragEnd: () => void;
|
||||
onDragOver: (e: React.DragEvent) => void;
|
||||
dropIndicator: "above" | "below" | null;
|
||||
isDragging: boolean;
|
||||
t: (key: string, opts?: Record<string, unknown>) => string;
|
||||
}) {
|
||||
function handleRun() {
|
||||
function executeRun() {
|
||||
const targets = terminalTabs.filter((tab) => selectedTabIds.has(tab.id));
|
||||
if (targets.length > 0) {
|
||||
targets.forEach((tab) => {
|
||||
@@ -723,6 +738,10 @@ function SnippetCard({
|
||||
}
|
||||
}
|
||||
|
||||
function handleRun() {
|
||||
onConfirmRun(snippet, executeRun);
|
||||
}
|
||||
|
||||
function handleCopy() {
|
||||
navigator.clipboard.writeText(snippet.content);
|
||||
toast.success(
|
||||
@@ -731,69 +750,80 @@ function SnippetCard({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border border-border bg-background p-2.5 flex flex-col gap-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="grid grid-cols-2 gap-px mt-0.5 shrink-0 opacity-30">
|
||||
<div className="size-1 bg-muted-foreground rounded-full" />
|
||||
<div className="size-1 bg-muted-foreground rounded-full" />
|
||||
<div className="size-1 bg-muted-foreground rounded-full" />
|
||||
<div className="size-1 bg-muted-foreground rounded-full" />
|
||||
<div className="relative" onDragOver={onDragOver}>
|
||||
{dropIndicator === "above" && (
|
||||
<div className="absolute -top-1 left-0 right-0 h-0.5 bg-accent-brand z-10 pointer-events-none" />
|
||||
)}
|
||||
<div
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
onDragStart();
|
||||
}}
|
||||
onDragEnd={onDragEnd}
|
||||
className={`border bg-background p-2.5 flex flex-col gap-2 group/card transition-opacity ${isDragging ? "opacity-40" : "opacity-100"} border-border`}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<GripVertical className="size-3.5 mt-0.5 shrink-0 text-muted-foreground/30 group-hover/card:text-muted-foreground/60 cursor-grab active:cursor-grabbing transition-colors" />
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-xs font-semibold">{snippet.name}</span>
|
||||
{snippet.description && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{snippet.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-xs font-semibold">{snippet.name}</span>
|
||||
{snippet.description && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{snippet.description}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground font-mono px-1">
|
||||
{snippet.content}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 text-xs h-7 gap-1.5"
|
||||
onClick={handleRun}
|
||||
>
|
||||
<Play className="size-3" />
|
||||
{t("newUi.sidebar.snippets.run")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-muted-foreground hover:text-foreground shrink-0"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<Copy className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-muted-foreground hover:text-foreground shrink-0"
|
||||
onClick={() => onEdit(snippet)}
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-muted-foreground hover:text-destructive shrink-0"
|
||||
onClick={() => onDelete(snippet.id)}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-muted-foreground hover:text-foreground shrink-0"
|
||||
onClick={() => onShare(snippet)}
|
||||
>
|
||||
<Share2 className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground font-mono px-1">
|
||||
{snippet.content}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 text-xs h-7 gap-1.5"
|
||||
onClick={handleRun}
|
||||
>
|
||||
<Play className="size-3" />
|
||||
{t("newUi.sidebar.snippets.run")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-muted-foreground hover:text-foreground shrink-0"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<Copy className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-muted-foreground hover:text-foreground shrink-0"
|
||||
onClick={() => onEdit(snippet)}
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-muted-foreground hover:text-destructive shrink-0"
|
||||
onClick={() => onDelete(snippet.id)}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-muted-foreground hover:text-foreground shrink-0"
|
||||
onClick={() => onShare(snippet)}
|
||||
>
|
||||
<Share2 className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
{dropIndicator === "below" && (
|
||||
<div className="absolute -bottom-1 left-0 right-0 h-0.5 bg-accent-brand z-10 pointer-events-none" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -806,9 +836,11 @@ export function SnippetsPanel({
|
||||
activeTabId: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { confirmWithToast } = useConfirmation();
|
||||
const [snippetSearch, setSnippetSearch] = useState("");
|
||||
const [folders, setFolders] = useState<SnippetFolder[]>([]);
|
||||
const [snippets, setSnippets] = useState<Snippet[]>([]);
|
||||
const snippetsRef = useRef<Snippet[]>([]);
|
||||
const [snippetFormOpen, setSnippetFormOpen] = useState(false);
|
||||
const [editingSnippet, setEditingSnippet] = useState<Snippet | null>(null);
|
||||
const [createFolderOpen, setCreateFolderOpen] = useState(false);
|
||||
@@ -823,9 +855,123 @@ export function SnippetsPanel({
|
||||
: [],
|
||||
),
|
||||
);
|
||||
const [uncategorizedOpen, setUncategorizedOpen] = useState(true);
|
||||
|
||||
function updateSnippets(next: Snippet[] | ((prev: Snippet[]) => Snippet[])) {
|
||||
setSnippets((prev) => {
|
||||
const resolved = typeof next === "function" ? next(prev) : next;
|
||||
snippetsRef.current = resolved;
|
||||
return resolved;
|
||||
});
|
||||
}
|
||||
|
||||
const getFoldersCollapsed = () =>
|
||||
localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false";
|
||||
|
||||
const [uncategorizedOpen, setUncategorizedOpen] = useState(
|
||||
() => !getFoldersCollapsed(),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
const collapsed = getFoldersCollapsed();
|
||||
setUncategorizedOpen(!collapsed);
|
||||
setFolders((prev) => prev.map((f) => ({ ...f, open: !collapsed })));
|
||||
};
|
||||
window.addEventListener("defaultSnippetFoldersCollapsedChanged", handler);
|
||||
return () =>
|
||||
window.removeEventListener(
|
||||
"defaultSnippetFoldersCollapsedChanged",
|
||||
handler,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const [draggedSnippet, setDraggedSnippet] = useState<Snippet | null>(null);
|
||||
const [dropTarget, setDropTarget] = useState<{
|
||||
id: number;
|
||||
position: "above" | "below";
|
||||
} | null>(null);
|
||||
|
||||
function handleDragStart(snippet: Snippet) {
|
||||
setDraggedSnippet(snippet);
|
||||
}
|
||||
|
||||
function handleDragOver(e: React.DragEvent, snippetId: number) {
|
||||
e.preventDefault();
|
||||
if (!draggedSnippet || draggedSnippet.id === snippetId) return;
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
const position = e.clientY < rect.top + rect.height / 2 ? "above" : "below";
|
||||
setDropTarget((prev) =>
|
||||
prev?.id === snippetId && prev?.position === position
|
||||
? prev
|
||||
: { id: snippetId, position },
|
||||
);
|
||||
}
|
||||
|
||||
async function handleDrop(folder: string | null) {
|
||||
const dragged = draggedSnippet;
|
||||
const target = dropTarget;
|
||||
setDraggedSnippet(null);
|
||||
setDropTarget(null);
|
||||
|
||||
if (!dragged || !target || dragged.id === target.id) return;
|
||||
|
||||
const group = snippetsRef.current.filter((s) => s.folder === folder);
|
||||
const draggedIdx = group.findIndex((s) => s.id === dragged.id);
|
||||
const targetIdx = group.findIndex((s) => s.id === target.id);
|
||||
if (draggedIdx === -1 || targetIdx === -1) return;
|
||||
|
||||
const reordered = [...group];
|
||||
reordered.splice(draggedIdx, 1);
|
||||
const insertAt =
|
||||
target.position === "below"
|
||||
? reordered.findIndex((s) => s.id === target.id) + 1
|
||||
: reordered.findIndex((s) => s.id === target.id);
|
||||
reordered.splice(insertAt, 0, dragged);
|
||||
|
||||
const rest = snippetsRef.current.filter((s) => s.folder !== folder);
|
||||
const next = [...rest, ...reordered];
|
||||
snippetsRef.current = next;
|
||||
setSnippets(next);
|
||||
|
||||
const payload = reordered.map((s, idx) => ({
|
||||
id: s.id,
|
||||
order: idx,
|
||||
folder: folder ?? undefined,
|
||||
}));
|
||||
try {
|
||||
await reorderSnippets(payload);
|
||||
} catch {
|
||||
toast.error(t("newUi.sidebar.snippets.reorderFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragEnd() {
|
||||
setDraggedSnippet(null);
|
||||
setDropTarget(null);
|
||||
}
|
||||
|
||||
const handleConfirmRun = useCallback(
|
||||
(snippet: Snippet, execute: () => void) => {
|
||||
const shouldConfirm =
|
||||
localStorage.getItem("confirmSnippetExecution") === "true";
|
||||
if (!shouldConfirm) {
|
||||
execute();
|
||||
return;
|
||||
}
|
||||
confirmWithToast(
|
||||
t("newUi.sidebar.snippets.confirmRunMessage", { name: snippet.name }),
|
||||
execute,
|
||||
t("newUi.sidebar.snippets.confirmRunButton"),
|
||||
t("newUi.sidebar.snippets.cancel"),
|
||||
{ confirmOnEnter: true, duration: 6000 },
|
||||
);
|
||||
},
|
||||
[confirmWithToast, t],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const collapsed = getFoldersCollapsed();
|
||||
|
||||
getSnippets()
|
||||
.then((data) => {
|
||||
const arr = Array.isArray(data) ? data : [];
|
||||
@@ -836,8 +982,9 @@ export function SnippetsPanel({
|
||||
description: s.description,
|
||||
content: s.content,
|
||||
folder: s.folder ?? null,
|
||||
order: s.order ?? 0,
|
||||
}));
|
||||
setSnippets(mapped);
|
||||
updateSnippets(mapped);
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
@@ -850,7 +997,7 @@ export function SnippetsPanel({
|
||||
name: f.name,
|
||||
color: f.color ?? FOLDER_COLORS[0],
|
||||
icon: (f.icon as FolderIconId) ?? "folder",
|
||||
open: true,
|
||||
open: !collapsed,
|
||||
}));
|
||||
setFolders(mapped);
|
||||
})
|
||||
@@ -865,23 +1012,27 @@ export function SnippetsPanel({
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSaveSnippet(data: Omit<Snippet, "id">, id?: number) {
|
||||
async function handleSaveSnippet(
|
||||
data: Omit<Snippet, "id" | "order">,
|
||||
id?: number,
|
||||
) {
|
||||
try {
|
||||
if (id !== undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await apiUpdateSnippet(id, data as any);
|
||||
setSnippets((prev) =>
|
||||
updateSnippets((prev) =>
|
||||
prev.map((s) => (s.id === id ? { ...s, ...data } : s)),
|
||||
);
|
||||
toast.success(t("newUi.sidebar.snippets.updateSuccess"));
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const created = (await apiCreateSnippet(data as any)) as any;
|
||||
setSnippets((prev) => [
|
||||
updateSnippets((prev) => [
|
||||
...prev,
|
||||
{
|
||||
...data,
|
||||
id: created.id ?? Math.max(0, ...prev.map((x) => x.id)) + 1,
|
||||
order: created.order ?? prev.length,
|
||||
},
|
||||
]);
|
||||
toast.success(t("newUi.sidebar.snippets.createSuccess"));
|
||||
@@ -919,7 +1070,7 @@ export function SnippetsPanel({
|
||||
try {
|
||||
await apiDeleteSnippetFolder(folder.name);
|
||||
setFolders((prev) => prev.filter((f) => f.id !== folder.id));
|
||||
setSnippets((prev) =>
|
||||
updateSnippets((prev) =>
|
||||
prev.map((s) =>
|
||||
s.folder === folder.name ? { ...s, folder: null } : s,
|
||||
),
|
||||
@@ -940,7 +1091,7 @@ export function SnippetsPanel({
|
||||
const nameChanged = data.name !== oldName;
|
||||
if (nameChanged) {
|
||||
await apiRenameSnippetFolder(oldName, data.name);
|
||||
setSnippets((prev) =>
|
||||
updateSnippets((prev) =>
|
||||
prev.map((s) =>
|
||||
s.folder === oldName ? { ...s, folder: data.name } : s,
|
||||
),
|
||||
@@ -966,7 +1117,7 @@ export function SnippetsPanel({
|
||||
async function handleDeleteSnippet(id: number) {
|
||||
try {
|
||||
await apiDeleteSnippet(id);
|
||||
setSnippets((prev) => prev.filter((s) => s.id !== id));
|
||||
updateSnippets((prev) => prev.filter((s) => s.id !== id));
|
||||
} catch {
|
||||
toast.error(t("newUi.sidebar.snippets.deleteFailed"));
|
||||
}
|
||||
@@ -1109,7 +1260,11 @@ export function SnippetsPanel({
|
||||
</span>
|
||||
</button>
|
||||
{uncategorizedOpen && (
|
||||
<div className="flex flex-col gap-2 ml-1">
|
||||
<div
|
||||
className="flex flex-col gap-2 ml-1"
|
||||
onDrop={() => handleDrop(null)}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
>
|
||||
{uncategorizedSnippets.map((snippet) => (
|
||||
<SnippetCard
|
||||
key={snippet.id}
|
||||
@@ -1119,6 +1274,16 @@ export function SnippetsPanel({
|
||||
onDelete={handleDeleteSnippet}
|
||||
onEdit={handleEditSnippet}
|
||||
onShare={setShareSnippet}
|
||||
onConfirmRun={handleConfirmRun}
|
||||
onDragStart={() => handleDragStart(snippet)}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={(e) => handleDragOver(e, snippet.id)}
|
||||
dropIndicator={
|
||||
dropTarget?.id === snippet.id
|
||||
? dropTarget.position
|
||||
: null
|
||||
}
|
||||
isDragging={draggedSnippet?.id === snippet.id}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
@@ -1188,7 +1353,11 @@ export function SnippetsPanel({
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
{folder.open && (
|
||||
<div className="flex flex-col gap-2 ml-1">
|
||||
<div
|
||||
className="flex flex-col gap-2 ml-1"
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={() => handleDrop(folder.name)}
|
||||
>
|
||||
{folderSnippets.map((snippet) => (
|
||||
<SnippetCard
|
||||
key={snippet.id}
|
||||
@@ -1198,6 +1367,16 @@ export function SnippetsPanel({
|
||||
onDelete={handleDeleteSnippet}
|
||||
onEdit={handleEditSnippet}
|
||||
onShare={setShareSnippet}
|
||||
onConfirmRun={handleConfirmRun}
|
||||
onDragStart={() => handleDragStart(snippet)}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={(e) => handleDragOver(e, snippet.id)}
|
||||
dropIndicator={
|
||||
dropTarget?.id === snippet.id
|
||||
? dropTarget.position
|
||||
: null
|
||||
}
|
||||
isDragging={draggedSnippet?.id === snippet.id}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/button";
|
||||
import { Separator } from "@/components/separator";
|
||||
import { LayoutPanelLeft, X } from "lucide-react";
|
||||
import { LayoutPanelLeft, X, ChevronDown } from "lucide-react";
|
||||
import { PANE_COUNTS, SPLIT_MODES } from "@/lib/theme";
|
||||
import { tabIcon } from "@/shell/tabUtils";
|
||||
import type { Tab, SplitMode } from "@/types/ui-types";
|
||||
@@ -72,23 +72,24 @@ export function SplitScreenPanel({
|
||||
setSplitMode,
|
||||
paneTabIds,
|
||||
setPaneTabIds,
|
||||
onAssignPane,
|
||||
}: {
|
||||
tabs: Tab[];
|
||||
splitMode: SplitMode;
|
||||
setSplitMode: (m: SplitMode) => void;
|
||||
paneTabIds: (string | null)[];
|
||||
setPaneTabIds: (ids: (string | null)[]) => void;
|
||||
onAssignPane: (paneIndex: number, tabId: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const paneCount = PANE_COUNTS[splitMode];
|
||||
const [draggingTabId, setDraggingTabId] = useState<string | null>(null);
|
||||
const [dragOverPane, setDragOverPane] = useState<number | null>(null);
|
||||
const [quickAssignPane, setQuickAssignPane] = useState<number | null>(null);
|
||||
|
||||
function handleDrop(paneIndex: number) {
|
||||
if (draggingTabId === null) return;
|
||||
const next = [...paneTabIds];
|
||||
next[paneIndex] = draggingTabId;
|
||||
setPaneTabIds(next);
|
||||
onAssignPane(paneIndex, draggingTabId);
|
||||
setDraggingTabId(null);
|
||||
setDragOverPane(null);
|
||||
}
|
||||
@@ -172,51 +173,104 @@ export function SplitScreenPanel({
|
||||
? tabs.find((t) => t.id === assignedId)
|
||||
: null;
|
||||
const isOver = dragOverPane === i;
|
||||
const isQuickAssignOpen = quickAssignPane === i;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setDragOverPane(i);
|
||||
}}
|
||||
onDragLeave={() => setDragOverPane(null)}
|
||||
onDrop={() => handleDrop(i)}
|
||||
className={`relative flex flex-col items-center justify-center gap-1 p-2 min-h-[52px] border transition-colors ${
|
||||
isOver
|
||||
? "border-accent-brand bg-accent-brand/10"
|
||||
: assignedTab
|
||||
? "border-border bg-muted/20"
|
||||
: "border-dashed border-border/60 bg-muted/5 hover:border-border hover:bg-muted/10"
|
||||
}`}
|
||||
>
|
||||
<span className="absolute top-1 left-1.5 text-[10px] text-muted-foreground/40 font-mono leading-none">
|
||||
{i + 1}
|
||||
</span>
|
||||
{assignedTab ? (
|
||||
<>
|
||||
<div className="flex items-center gap-1 px-1 w-full justify-center">
|
||||
<span className="text-muted-foreground shrink-0">
|
||||
{tabIcon(assignedTab.type)}
|
||||
</span>
|
||||
<span className="text-xs font-semibold truncate max-w-[70px]">
|
||||
{assignedTab.type === "dashboard"
|
||||
? t("newUi.sidebar.splitScreen.dashboard")
|
||||
: assignedTab.label}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => clearPane(i)}
|
||||
className="absolute top-0.5 right-0.5 size-4 flex items-center justify-center text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="size-2.5" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground/40">
|
||||
{isOver
|
||||
? t("newUi.sidebar.splitScreen.dropHere")
|
||||
: t("newUi.sidebar.splitScreen.emptyPane")}
|
||||
<div key={i} className="flex flex-col gap-0.5">
|
||||
<div
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setDragOverPane(i);
|
||||
}}
|
||||
onDragLeave={() => setDragOverPane(null)}
|
||||
onDrop={() => handleDrop(i)}
|
||||
className={`relative flex flex-col items-center justify-center gap-1 p-2 min-h-[52px] border transition-colors ${
|
||||
isOver
|
||||
? "border-accent-brand bg-accent-brand/10"
|
||||
: assignedTab
|
||||
? "border-accent-brand/30 bg-accent-brand/5"
|
||||
: "border-dashed border-border/60 bg-muted/5 hover:border-border hover:bg-muted/10"
|
||||
}`}
|
||||
>
|
||||
<span className="absolute top-1 left-1.5 text-[10px] text-muted-foreground/40 font-mono leading-none">
|
||||
{i + 1}
|
||||
</span>
|
||||
{assignedTab ? (
|
||||
<>
|
||||
<div className="flex items-center gap-1 px-1 w-full justify-center">
|
||||
<span className="text-accent-brand shrink-0">
|
||||
{tabIcon(assignedTab.type)}
|
||||
</span>
|
||||
<span className="text-xs font-semibold truncate max-w-[70px] text-foreground">
|
||||
{assignedTab.type === "dashboard"
|
||||
? t("newUi.sidebar.splitScreen.dashboard")
|
||||
: assignedTab.label}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => clearPane(i)}
|
||||
className="absolute top-0.5 right-0.5 size-4 flex items-center justify-center text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="size-2.5" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground/40">
|
||||
{isOver
|
||||
? t("newUi.sidebar.splitScreen.dropHere")
|
||||
: t("newUi.sidebar.splitScreen.emptyPane")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick assign button */}
|
||||
{!assignedTab && (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() =>
|
||||
setQuickAssignPane(isQuickAssignOpen ? null : i)
|
||||
}
|
||||
className="flex items-center justify-center gap-1 w-full py-0.5 text-[10px] text-muted-foreground/60 hover:text-accent-brand hover:bg-accent-brand/5 border border-dashed border-border/40 hover:border-accent-brand/30 transition-colors"
|
||||
>
|
||||
{t("newUi.sidebar.splitScreen.quickAssign")}
|
||||
<ChevronDown
|
||||
className={`size-2.5 transition-transform ${isQuickAssignOpen ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
{isQuickAssignOpen && (
|
||||
<div className="absolute top-full left-0 right-0 z-20 bg-popover border border-border shadow-md mt-0.5 max-h-40 overflow-y-auto">
|
||||
{tabs
|
||||
.filter(
|
||||
(tab) =>
|
||||
tab.type !== "dashboard" &&
|
||||
!paneTabIds.includes(tab.id),
|
||||
)
|
||||
.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => {
|
||||
onAssignPane(i, tab.id);
|
||||
setQuickAssignPane(null);
|
||||
}}
|
||||
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs text-left hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<span className="text-muted-foreground shrink-0">
|
||||
{tabIcon(tab.type)}
|
||||
</span>
|
||||
<span className="truncate">{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
{tabs.filter(
|
||||
(tab) =>
|
||||
tab.type !== "dashboard" &&
|
||||
!paneTabIds.includes(tab.id),
|
||||
).length === 0 && (
|
||||
<div className="px-2 py-2 text-xs text-muted-foreground/60">
|
||||
All tabs assigned
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -234,37 +288,51 @@ export function SplitScreenPanel({
|
||||
{t("newUi.sidebar.splitScreen.dragTabsHint")}
|
||||
</span>
|
||||
<div className="flex flex-col gap-1 mt-0.5">
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
draggable
|
||||
onDragStart={() => setDraggingTabId(tab.id)}
|
||||
onDragEnd={() => {
|
||||
setDraggingTabId(null);
|
||||
setDragOverPane(null);
|
||||
}}
|
||||
className={`flex items-center gap-2 px-2.5 py-2 border cursor-grab active:cursor-grabbing select-none transition-colors ${
|
||||
draggingTabId === tab.id
|
||||
? "border-accent-brand/40 bg-accent-brand/10 text-accent-brand"
|
||||
: "border-border hover:border-muted-foreground/40 hover:bg-muted/30"
|
||||
}`}
|
||||
>
|
||||
<span className="text-muted-foreground shrink-0">
|
||||
{tabIcon(tab.type)}
|
||||
</span>
|
||||
<span className="text-xs font-medium flex-1 truncate">
|
||||
{tab.type === "dashboard"
|
||||
? t("newUi.sidebar.splitScreen.dashboard")
|
||||
: tab.label}
|
||||
</span>
|
||||
<div className="grid grid-cols-2 gap-px opacity-30 shrink-0">
|
||||
<div className="size-1 bg-muted-foreground rounded-full" />
|
||||
<div className="size-1 bg-muted-foreground rounded-full" />
|
||||
<div className="size-1 bg-muted-foreground rounded-full" />
|
||||
<div className="size-1 bg-muted-foreground rounded-full" />
|
||||
{tabs.map((tab) => {
|
||||
const paneIdx = paneTabIds.indexOf(tab.id);
|
||||
const isAssigned = paneIdx !== -1;
|
||||
return (
|
||||
<div
|
||||
key={tab.id}
|
||||
draggable={!isAssigned}
|
||||
onDragStart={() => !isAssigned && setDraggingTabId(tab.id)}
|
||||
onDragEnd={() => {
|
||||
setDraggingTabId(null);
|
||||
setDragOverPane(null);
|
||||
}}
|
||||
className={`flex items-center gap-2 px-2.5 py-2 border select-none transition-colors ${
|
||||
isAssigned
|
||||
? "border-accent-brand/20 bg-accent-brand/5 opacity-60 cursor-default"
|
||||
: draggingTabId === tab.id
|
||||
? "border-accent-brand/40 bg-accent-brand/10 text-accent-brand cursor-grabbing"
|
||||
: "border-border hover:border-muted-foreground/40 hover:bg-muted/30 cursor-grab active:cursor-grabbing"
|
||||
}`}
|
||||
>
|
||||
<span className="text-muted-foreground shrink-0">
|
||||
{tabIcon(tab.type)}
|
||||
</span>
|
||||
<span className="text-xs font-medium flex-1 truncate">
|
||||
{tab.type === "dashboard"
|
||||
? t("newUi.sidebar.splitScreen.dashboard")
|
||||
: tab.label}
|
||||
</span>
|
||||
{isAssigned ? (
|
||||
<span className="text-[10px] text-accent-brand/60 font-mono shrink-0">
|
||||
{t("newUi.sidebar.splitScreen.alreadyAssigned", {
|
||||
index: paneIdx + 1,
|
||||
})}
|
||||
</span>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-px opacity-30 shrink-0">
|
||||
<div className="size-1 bg-muted-foreground rounded-full" />
|
||||
<div className="size-1 bg-muted-foreground rounded-full" />
|
||||
<div className="size-1 bg-muted-foreground rounded-full" />
|
||||
<div className="size-1 bg-muted-foreground rounded-full" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -377,9 +377,13 @@ function PasswordChangeSection({
|
||||
export function UserProfilePanel({
|
||||
username,
|
||||
onLogout,
|
||||
userPrefs,
|
||||
onPrefsChange,
|
||||
}: {
|
||||
username?: string;
|
||||
onLogout?: () => void;
|
||||
userPrefs?: { reopenTabsOnLogin: boolean };
|
||||
onPrefsChange?: (prefs: { reopenTabsOnLogin: boolean }) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const themeLabel: Record<ThemeId, string> = {
|
||||
@@ -448,9 +452,6 @@ export function UserProfilePanel({
|
||||
);
|
||||
|
||||
// Settings toggles — all backed by localStorage
|
||||
const [fileColorCoding, setFileColorCoding] = useState(
|
||||
() => localStorage.getItem("fileColorCoding") !== "false",
|
||||
);
|
||||
const [commandAutocomplete, setCommandAutocomplete] = useState(
|
||||
() => localStorage.getItem("commandAutocomplete") === "true",
|
||||
);
|
||||
@@ -464,13 +465,16 @@ export function UserProfilePanel({
|
||||
const v = localStorage.getItem("commandPaletteShortcutEnabled");
|
||||
return v !== null ? v === "true" : true;
|
||||
});
|
||||
const [sessionPersistence, setSessionPersistence] = useState(
|
||||
() => localStorage.getItem("enableTerminalSessionPersistence") !== "false",
|
||||
);
|
||||
const [showHostTags, setShowHostTags] = useState(() => {
|
||||
const v = localStorage.getItem("showHostTags");
|
||||
return v !== null ? v === "true" : true;
|
||||
});
|
||||
const [hostTrayOnClick, setHostTrayOnClick] = useState(
|
||||
() => localStorage.getItem("hostTrayOnClick") === "true",
|
||||
);
|
||||
const [pinAppRail, setPinAppRail] = useState(
|
||||
() => localStorage.getItem("pinAppRail") === "true",
|
||||
);
|
||||
const [foldersCollapsed, setFoldersCollapsed] = useState(
|
||||
() => localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false",
|
||||
);
|
||||
@@ -905,25 +909,6 @@ export function UserProfilePanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 border-t border-border pt-3">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1">
|
||||
{t("newUi.sidebar.userProfile.settingsFileManager")}
|
||||
</span>
|
||||
<SettingRow
|
||||
label={t("newUi.sidebar.userProfile.fileColorCoding")}
|
||||
description={t("newUi.sidebar.userProfile.fileColorCodingDesc")}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={fileColorCoding}
|
||||
onChange={(v) => {
|
||||
setFileColorCoding(v);
|
||||
localStorage.setItem("fileColorCoding", v.toString());
|
||||
window.dispatchEvent(new Event("fileColorCodingChanged"));
|
||||
}}
|
||||
/>
|
||||
</SettingRow>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 border-t border-border pt-3">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1">
|
||||
{t("newUi.sidebar.userProfile.settingsTerminal")}
|
||||
@@ -990,24 +975,23 @@ export function UserProfilePanel({
|
||||
"commandPaletteShortcutEnabled",
|
||||
v.toString(),
|
||||
);
|
||||
window.dispatchEvent(
|
||||
new Event("commandPaletteShortcutEnabledChanged"),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("newUi.sidebar.userProfile.sessionPersistence")}
|
||||
description={t(
|
||||
"newUi.sidebar.userProfile.sessionPersistenceDesc",
|
||||
)}
|
||||
badge="BETA"
|
||||
label={t("newUi.sidebar.userProfile.reopenTabsOnLogin")}
|
||||
description={t("newUi.sidebar.userProfile.reopenTabsOnLoginDesc")}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={sessionPersistence}
|
||||
checked={userPrefs?.reopenTabsOnLogin ?? false}
|
||||
onChange={(v) => {
|
||||
setSessionPersistence(v);
|
||||
localStorage.setItem(
|
||||
"enableTerminalSessionPersistence",
|
||||
v.toString(),
|
||||
);
|
||||
onPrefsChange?.({ reopenTabsOnLogin: v });
|
||||
import("@/main-axios").then(({ saveUserPreferences }) => {
|
||||
saveUserPreferences({ reopenTabsOnLogin: v }).catch(() => {});
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</SettingRow>
|
||||
@@ -1042,6 +1026,32 @@ export function UserProfilePanel({
|
||||
}}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("newUi.sidebar.userProfile.hostTrayOnClick")}
|
||||
description={t("newUi.sidebar.userProfile.hostTrayOnClickDesc")}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={hostTrayOnClick}
|
||||
onChange={(v) => {
|
||||
setHostTrayOnClick(v);
|
||||
localStorage.setItem("hostTrayOnClick", v.toString());
|
||||
window.dispatchEvent(new Event("hostTrayOnClickChanged"));
|
||||
}}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("newUi.sidebar.userProfile.pinAppRail")}
|
||||
description={t("newUi.sidebar.userProfile.pinAppRailDesc")}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={pinAppRail}
|
||||
onChange={(v) => {
|
||||
setPinAppRail(v);
|
||||
localStorage.setItem("pinAppRail", v.toString());
|
||||
window.dispatchEvent(new Event("pinAppRailChanged"));
|
||||
}}
|
||||
/>
|
||||
</SettingRow>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 border-t border-border pt-3">
|
||||
|
||||
@@ -102,8 +102,8 @@ export function ElectronVersionCheck({ onContinue }: VersionCheckModalProps) {
|
||||
if (versionChecking && !versionInfo) {
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-background p-6 z-50">
|
||||
<div className="flex flex-col gap-5 p-6 border border-border bg-card max-w-md w-full items-center">
|
||||
<div className="w-5 h-5 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
<div className="flex flex-col gap-5 p-6 border border-border bg-background max-w-md w-full items-center">
|
||||
<div className="w-5 h-5 border-2 border-accent-brand border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("versionCheck.checkingUpdates")}
|
||||
</p>
|
||||
@@ -115,7 +115,7 @@ export function ElectronVersionCheck({ onContinue }: VersionCheckModalProps) {
|
||||
if (!versionInfo || versionDismissed) {
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-background p-6 z-50">
|
||||
<div className="flex flex-col gap-5 p-6 border border-border bg-card max-w-md w-full">
|
||||
<div className="flex flex-col gap-5 p-6 border border-border bg-background max-w-md w-full">
|
||||
<p className="font-bold">{t("versionCheck.checkUpdates")}</p>
|
||||
{versionInfo && !versionDismissed && (
|
||||
<VersionAlert
|
||||
@@ -125,7 +125,7 @@ export function ElectronVersionCheck({ onContinue }: VersionCheckModalProps) {
|
||||
)}
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
className="w-full bg-accent-brand hover:bg-accent-brand/90 text-background font-bold"
|
||||
className="w-full bg-accent-brand hover:bg-accent-brand/90 text-background font-bold rounded-none"
|
||||
>
|
||||
{t("common.continue")}
|
||||
</Button>
|
||||
@@ -136,7 +136,7 @@ export function ElectronVersionCheck({ onContinue }: VersionCheckModalProps) {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-background p-6 z-50">
|
||||
<div className="flex flex-col gap-5 p-6 border border-border bg-card max-w-md w-full">
|
||||
<div className="flex flex-col gap-5 p-6 border border-border bg-background max-w-md w-full">
|
||||
<p className="font-bold">{versionModalTitle}</p>
|
||||
<VersionAlert
|
||||
updateInfo={versionInfo}
|
||||
@@ -144,7 +144,7 @@ export function ElectronVersionCheck({ onContinue }: VersionCheckModalProps) {
|
||||
/>
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
className="w-full bg-accent-brand hover:bg-accent-brand/90 text-background font-bold"
|
||||
className="w-full bg-accent-brand hover:bg-accent-brand/90 text-background font-bold rounded-none"
|
||||
>
|
||||
{t("common.continue")}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user