Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ docker compose exec rustdesk cat /data/id_ed25519.pub

**RustDesk client:** ID Server `your-server:21116`, Relay `your-server:21117`, API `http://your-server:21114`, Key — paste the public-key contents returned by `docker compose exec rustdesk cat /data/id_ed25519.pub`; the file is at `/data/id_ed25519.pub` inside the container.

## Upgrading an existing deployment

The Compose file bind-mounts `../data/uploads` to `/app/resources/public/upload`.
On an existing deployment the mount shadows the uploads already stored inside the
old container, so branding assets referenced before the mount (`GET /upload/...`)
return 404. Copy them out of the old container before the first `docker compose
up -d` with the new mount:

```bash
mkdir -p ../data/uploads
docker cp <old-rustdesk-container>:/app/resources/public/upload/. ../data/uploads/
docker compose up -d
```

This migration applies only to the upload bind mount; other data (`/data`, the
database) stays on its existing volumes.

---

## Ports
Expand Down
72 changes: 67 additions & 5 deletions admin-ui/src/components/ui/ImageAttachField.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
<template>
<div class="image-attach-field">
<div class="image-attach-field__preview" :aria-label="modelValue && !previewFailed ? undefined : previewEmptyLabel">
<div class="image-attach-field__preview" :aria-label="previewAriaLabel">
<img
v-if="modelValue && !previewFailed"
:src="modelValue"
alt=""
class="image-attach-field__image"
@error="previewFailed = true"
>
<span
v-else-if="previewFailed"
class="image-attach-field__unavailable"
tabindex="0"
:aria-label="unavailableLabel"
>
<el-tooltip :content="unavailableDetails" placement="top" popper-class="image-attach-field-hint-popper">
<span class="image-attach-field__unavailable-chip">
<el-icon aria-hidden="true"><WarningFilled /></el-icon>
<span aria-hidden="true">{{ unavailableLabel }}</span>
</span>
</el-tooltip>
</span>
<span v-else class="image-attach-field__placeholder">
<el-icon aria-hidden="true"><Picture /></el-icon>
</span>
Expand Down Expand Up @@ -50,8 +63,8 @@
</template>

<script setup>
import { ref, watch } from 'vue'
import { Close, InfoFilled, Picture } from '@element-plus/icons-vue'
import { computed, ref, watch } from 'vue'
import { Close, InfoFilled, Picture, WarningFilled } from '@element-plus/icons-vue'

const props = defineProps({
modelValue: { type: String, default: '' },
Expand All @@ -64,6 +77,11 @@ const props = defineProps({
linkPlaceholder: { type: String, default: '' },
clearLabel: { type: String, default: '' },
previewEmptyLabel: { type: String, default: '' },
// Explicit unavailable state for a preview URL that failed to load (404 or
// otherwise unreachable): a warning message, never a validation error —
// the field is optional and the file may only be temporarily unreachable.
unavailableLabel: { type: String, default: '' },
unavailableDetails: { type: String, default: '' },
// Allowed-range hover text (variants/limits). Required format lives in the
// permanent note below the input; mechanics live in the section note.
rangeHint: { type: String, default: '' },
Expand All @@ -74,11 +92,20 @@ const props = defineProps({

const emit = defineEmits(['update:modelValue', 'input'])

// A broken preview URL degrades to the neutral placeholder; a new value
// (typed or uploaded) retries the preview automatically.
// Preview failed-state flag, declared before the computed below that reads it:
// it is a local ref, not a prop (props.previewFailed was always undefined),
// and the computed must read `previewFailed.value`. A new value (typed or
// uploaded) resets the flag, retrying the preview automatically.
const previewFailed = ref(false)
watch(() => props.modelValue, () => { previewFailed.value = false })

// Preview state labels: visible image needs no label; a failed load names the
// unavailable state; an empty preview keeps the neutral no-preview label.
const previewAriaLabel = computed(() => {
if (!props.modelValue) return props.previewEmptyLabel
return previewFailed.value ? props.unavailableLabel : undefined
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const clear = () => {
if (!props.modelValue) return
previewFailed.value = false
Expand Down Expand Up @@ -138,6 +165,41 @@ const onInputEvent = (value) => {
font-size: 24px;
}

// Explicit warning state for a preview URL that failed to load — visually a
// warning, semantically still a plain state message (not a validation error).
.image-attach-field__unavailable {
display: flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
padding: 4px;
border: 1px solid var(--el-color-warning-light-5);
border-radius: 4px;
background: var(--el-color-warning-light-9);
color: var(--el-color-warning);
text-align: center;
cursor: help;

&:focus-visible {
outline: 2px solid var(--el-color-primary);
outline-offset: 2px;
}
}

.image-attach-field__unavailable-chip {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
font-size: 10px;
line-height: 1.2;

.el-icon {
font-size: 16px;
}
}

.image-attach-field__clear {
display: inline-flex;
align-items: center;
Expand Down
30 changes: 21 additions & 9 deletions admin-ui/src/utils/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1139,19 +1139,31 @@
"One": "App Icon (PNG)"
},
"ImageAttachFileButton": {
"One": "Upload file (≤5 MB)"
"One": "Upload file"
},
"ImageAttachLinkPlaceholder": {
"One": "or paste http(s)…"
"One": "or paste an http(s) link…"
},
"ImageAttachClear": {
"One": "Clear"
},
"ImageAttachPreviewEmpty": {
"One": "No preview"
},
"ImageUnavailable": {
"One": "File unavailable"
},
"ImageUnavailableDetails": {
"One": "Could not load — the file may be missing on the server; building with this link will fail at the branding step."
},
"BrandingImagesMechanicsNote": {
"One": "Files are stored on the server; at build time the link is automatically resolved against the API server address. A pasted link must be a publicly reachable http(s) URL."
"One": "Uploaded files are stored on the server and resolved at build time; paste a publicly reachable http(s) link."
},
"BrandingPublicOriginMissingTitle": {
"One": "Images won't attach to the build"
},
"BrandingPublicOriginMissing": {
"One": "The public API server address is not set. Uploaded images will not be usable in builds: /upload/… values cannot be resolved to a public link. Set the RUSTDESK_API_RUSTDESK_PUBLIC_API_SERVER environment variable in the API environment."
},
"AppLogo": {
"One": "App Logo (PNG)"
Expand All @@ -1160,22 +1172,22 @@
"One": "Privacy screen (PNG)"
},
"ImageFieldNoteIcon": {
"One": "Required: PNG, square; 256×256 ideal (minimum 16×16)"
"One": "PNG · square · 256×256 ideal · file ≤5 MB · link ≤10 MB"
},
"ImageFieldNoteLogo": {
"One": "Required: PNG; up to 300×60 ideal (render stops at 300×60)"
"One": "PNG · up to 300×60 · file ≤5 MB · link ≤10 MB"
},
"ImageFieldNoteScreen": {
"One": "Required: PNG (shown fullscreen)"
"One": "PNG · fullscreen · file ≤5 MB · link ≤20 MB"
},
"ImageRangeIcon": {
"One": "Accepted: square 16×16–4096×4096; larger than 256×256 is downscaled to 256 for Windows icons; file ≤5 MB, URL ≤10 MB"
"One": "Accepted: square 16×16–4096×4096; squares above 256 are downscaled to 256 for Windows icons"
},
"ImageRangeLogo": {
"One": "Accepted: PNG up to 4096×4096; file ≤5 MB, URL ≤10 MB"
"One": "Accepted source: PNG up to 4096×4096"
},
"ImageRangeScreen": {
"One": "Accepted: PNG up to 4096×4096; file ≤5 MB, URL ≤20 MB"
"One": "Accepted source: PNG up to 4096×4096"
},
"ImageFieldPngOnlyError": {
"One": "Only PNG images are allowed"
Expand Down
30 changes: 21 additions & 9 deletions admin-ui/src/utils/i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -830,19 +830,31 @@
"One": "Иконка приложения (PNG)"
},
"ImageAttachFileButton": {
"One": "Загрузить файл (≤5 МБ)"
"One": "Загрузить файл"
},
"ImageAttachLinkPlaceholder": {
"One": "или вставьте http(s)…"
"One": "или вставьте ссылку http(s)…"
},
"ImageAttachClear": {
"One": "Очистить"
},
"ImageAttachPreviewEmpty": {
"One": "Нет предпросмотра"
},
"ImageUnavailable": {
"One": "Файл недоступен"
},
"ImageUnavailableDetails": {
"One": "Не удалось загрузить — файл может отсутствовать на сервере; сборка с такой ссылкой упадёт на брендинге."
},
"BrandingImagesMechanicsNote": {
"One": "Файл сохраняется на сервере; при сборке ссылка автоматически подставляется из адреса API-сервера. Вставленная ссылка должна быть публично доступной http(s)."
"One": "Загруженный файл хранится на сервере и подставляется при сборке; вставьте публичную http(s)-ссылку."
},
"BrandingPublicOriginMissingTitle": {
"One": "Картинки не подключатся к сборке"
},
"BrandingPublicOriginMissing": {
"One": "Публичный API-адрес (public API server) не задан. Загруженные картинки не будут использоваться в сборках: значения /upload/… невозможно преобразовать в публичную ссылку. Задайте переменную RUSTDESK_API_RUSTDESK_PUBLIC_API_SERVER в окружении API."
},
"AppLogo": {
"One": "Логотип приложения (PNG)"
Expand All @@ -851,22 +863,22 @@
"One": "Экран приватности (PNG)"
},
"ImageFieldNoteIcon": {
"One": "Требуется: PNG, квадрат; оптимально 256×256 (минимум 16×16)"
"One": "PNG · квадрат · 256×256 лучше всего · файл ≤5 МБ · ссылка ≤10 МБ"
},
"ImageFieldNoteLogo": {
"One": "Требуется: PNG; оптимально ≤300×60 — рендер не шире"
"One": "PNG · ≤300×60 · файл ≤5 МБ · ссылка ≤10 МБ"
},
"ImageFieldNoteScreen": {
"One": "Требуется: PNG (показ на весь экран)"
"One": "PNG · по всему экрану · файл ≤5 МБ · ссылка ≤20 МБ"
},
"ImageRangeIcon": {
"One": "Допустимо: квадрат 16×16–4096×4096; больше 256×256 ужимается до 256 для иконок Windows; файл ≤5 МБ, ссылка ≤10 МБ"
"One": "Допустимо: квадрат 16×16–4096×4096; квадрат больше 256 ужимается до 256 для значков Windows"
},
"ImageRangeLogo": {
"One": "Допустимо: PNG до 4096×4096; файл ≤5 МБ, ссылка ≤10 МБ"
"One": "Допустимый источник: PNG до 4096×4096"
},
"ImageRangeScreen": {
"One": "Допустимо: PNG до 4096×4096; файл ≤5 МБ, ссылка ≤20 МБ"
"One": "Допустимый источник: PNG до 4096×4096"
},
"ImageFieldPngOnlyError": {
"One": "Допускаются только изображения PNG"
Expand Down
30 changes: 21 additions & 9 deletions admin-ui/src/utils/i18n/zh_CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -843,19 +843,31 @@
"One": "应用图标 (PNG)"
},
"ImageAttachFileButton": {
"One": "上传文件(≤5 MB)"
"One": "上传文件"
},
"ImageAttachLinkPlaceholder": {
"One": "或粘贴 http(s)…"
"One": "或粘贴 http(s) 链接…"
},
"ImageAttachClear": {
"One": "清除"
},
"ImageAttachPreviewEmpty": {
"One": "无预览"
},
"ImageUnavailable": {
"One": "文件不可用"
},
"ImageUnavailableDetails": {
"One": "无法加载——文件可能已不在服务器上;带此链接的构建将在 branding 阶段失败。"
},
"BrandingImagesMechanicsNote": {
"One": "文件保存到服务器;构建时会根据 API 服务器地址自动解析链接。粘贴的链接必须是公网可访问的 http(s) 地址。"
"One": "上传的文件存储在服务器上,并在构建时解析;请粘贴可公网访问的 http(s) 链接。"
},
"BrandingPublicOriginMissingTitle": {
"One": "图片无法接入构建"
},
"BrandingPublicOriginMissing": {
"One": "未设置公开 API 服务器地址。上传的图片将无法在构建中使用:/upload/… 值无法转换为公开链接。请在 API 环境中设置 RUSTDESK_API_RUSTDESK_PUBLIC_API_SERVER 变量。"
},
"AppLogo": {
"One": "应用 Logo (PNG)"
Expand All @@ -864,22 +876,22 @@
"One": "隐私屏幕 (PNG)"
},
"ImageFieldNoteIcon": {
"One": "要求:PNG,正方形;建议 256×256(最小 16×16)"
"One": "PNG · 正方形 · 建议 256×256 · 文件 ≤5 MB · 链接 ≤10 MB"
},
"ImageFieldNoteLogo": {
"One": "要求:PNG;建议 ≤300×60(渲染不超过此宽度)"
"One": "PNG · ≤300×60 · 文件 ≤5 MB · 链接 ≤10 MB"
},
"ImageFieldNoteScreen": {
"One": "要求:PNG(全屏显示)"
"One": "PNG · 全屏 · 文件 ≤5 MB · 链接 ≤20 MB"
},
"ImageRangeIcon": {
"One": "允许:正方形 16×16–4096×4096;大于 256×256 会缩至 256 用作 Windows 图标;文件 ≤5 MB,链接 ≤10 MB"
"One": "允许:正方形 16×16–4096×4096;大于 256 的正方形会缩至 256 用作 Windows 图标"
},
"ImageRangeLogo": {
"One": "允许:PNG 最大 4096×4096;文件 ≤5 MB,链接 ≤10 MB"
"One": "允许来源:不超过 4096×4096 的 PNG"
},
"ImageRangeScreen": {
"One": "允许:PNG 最大 4096×4096;文件 ≤5 MB,链接 ≤20 MB"
"One": "允许来源:不超过 4096×4096 的 PNG"
},
"ImageFieldPngOnlyError": {
"One": "仅支持 PNG 图片"
Expand Down
Loading
Loading