This commit is contained in:
@@ -11,19 +11,14 @@ on:
|
||||
tags: ["v*"]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
HTTP_PROXY: "http://10.31.88.88:7891"
|
||||
HTTPS_PROXY: "http://10.31.88.88:7891"
|
||||
NO_PROXY: "localhost,127.0.0.1,.local,*.local"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-windows-gnu:
|
||||
name: windows-x86_64-gnu
|
||||
runs-on: Ubuntu_docker_act
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
TARGET: x86_64-pc-windows-gnu
|
||||
BIN: meatshell.exe
|
||||
@@ -36,72 +31,24 @@ jobs:
|
||||
PKG_CONFIG_ALLOW_CROSS: "1"
|
||||
|
||||
steps:
|
||||
- name: 显示代理配置
|
||||
shell: bash
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
echo "=== Proxy Configuration ==="
|
||||
echo "HTTP_PROXY: ${HTTP_PROXY}"
|
||||
echo "HTTPS_PROXY: ${HTTPS_PROXY}"
|
||||
echo "NO_PROXY: ${NO_PROXY}"
|
||||
echo "=========================="
|
||||
|
||||
- name: 测试代理连通性
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Testing proxy connectivity..."
|
||||
if curl -x "${HTTP_PROXY}" -I --connect-timeout 10 https://google.com 2>&1 | grep -q "HTTP"; then
|
||||
echo "✅ Proxy is working!"
|
||||
else
|
||||
echo "⚠️ Proxy test failed, but continuing..."
|
||||
fi
|
||||
|
||||
- name: 配置 Git 全局代理
|
||||
shell: bash
|
||||
run: |
|
||||
git config --global http.proxy ${HTTP_PROXY}
|
||||
git config --global https.proxy ${HTTPS_PROXY}
|
||||
git config --global http.sslVerify false
|
||||
git config --global https.sslVerify false
|
||||
echo "✅ Git proxy configured"
|
||||
|
||||
- name: 配置 Cargo 代理
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p ~/.cargo
|
||||
printf '[http]\n' > ~/.cargo/config.toml
|
||||
printf 'proxy = "%s"\n' "${HTTP_PROXY}" >> ~/.cargo/config.toml
|
||||
printf 'check-revoke = false\n\n' >> ~/.cargo/config.toml
|
||||
printf '[https]\n' >> ~/.cargo/config.toml
|
||||
printf 'proxy = "%s"\n' "${HTTPS_PROXY}" >> ~/.cargo/config.toml
|
||||
printf 'check-revoke = false\n\n' >> ~/.cargo/config.toml
|
||||
printf '[net]\n' >> ~/.cargo/config.toml
|
||||
printf 'git-fetch-with-cli = true\n' >> ~/.cargo/config.toml
|
||||
printf 'retry = 10\n' >> ~/.cargo/config.toml
|
||||
echo "✅ Cargo proxy configured"
|
||||
cat ~/.cargo/config.toml
|
||||
|
||||
- name: 配置 Cargo 环境变量
|
||||
shell: bash
|
||||
run: |
|
||||
echo "CARGO_HTTP_PROXY=${HTTP_PROXY}" >> $GITHUB_ENV
|
||||
echo "CARGO_HTTPS_PROXY=${HTTPS_PROXY}" >> $GITHUB_ENV
|
||||
echo "CARGO_HTTP_SSL_VERIFY=false" >> $GITHUB_ENV
|
||||
echo "CARGO_HTTPS_SSL_VERIFY=false" >> $GITHUB_ENV
|
||||
|
||||
- name: 配置 curl 默认代理
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p ~/.curl
|
||||
printf 'proxy = %s\n' "${HTTP_PROXY}" > ~/.curlrc
|
||||
printf 'insecure\n' >> ~/.curlrc
|
||||
echo "✅ curl proxy configured"
|
||||
|
||||
sudo dpkg --add-architecture i386
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
gcc-mingw-w64-x86-64 \
|
||||
binutils-mingw-w64-x86-64 \
|
||||
wine64 wine32 wine \
|
||||
xvfb \
|
||||
zip
|
||||
sudo apt-get clean
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
|
||||
- name: Cache Cargo registry and target
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
@@ -112,51 +59,17 @@ jobs:
|
||||
${{ runner.os }}-${{ env.TARGET }}-cargo-
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- name: Configure local Cargo cache
|
||||
shell: bash
|
||||
- name: Install Rust toolchain
|
||||
run: |
|
||||
set -eu
|
||||
CACHE_ROOT="${MEATSHELL_CI_CACHE:-$PWD/.cache/ci}"
|
||||
case "$CACHE_ROOT" in
|
||||
/*) ;;
|
||||
*) CACHE_ROOT="$PWD/$CACHE_ROOT" ;;
|
||||
esac
|
||||
mkdir -p "$CACHE_ROOT/cargo" "$CACHE_ROOT/target"
|
||||
echo "CARGO_HOME=$CACHE_ROOT/cargo" >> "$GITHUB_ENV"
|
||||
echo "CARGO_TARGET_DIR=$CACHE_ROOT/target" >> "$GITHUB_ENV"
|
||||
echo "CARGO_NET_RETRY=10" >> "$GITHUB_ENV"
|
||||
echo "Using Cargo cache root: $CACHE_ROOT"
|
||||
|
||||
if [ -d "$CACHE_ROOT/target/$TARGET/release" ]; then
|
||||
echo "✅ Cache hit! Previous build artifacts found."
|
||||
echo "Release binary size: $(du -sh "$CACHE_ROOT/target/$TARGET/release/$BIN" 2>/dev/null | cut -f1 || echo 'not built yet')"
|
||||
else
|
||||
echo "❌ Cache miss or first run. Will perform full build."
|
||||
fi
|
||||
|
||||
- name: 清理可能损坏的缓存
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -d ~/.cargo/registry/index/github.com-* ]; then
|
||||
echo "Cleaning corrupted cargo registry cache..."
|
||||
rm -rf ~/.cargo/registry/index/github.com-*
|
||||
fi
|
||||
rustup target add "$TARGET"
|
||||
|
||||
- name: Show toolchain
|
||||
shell: bash
|
||||
run: |
|
||||
set -eu
|
||||
rustc --version
|
||||
cargo --version
|
||||
x86_64-w64-mingw32-gcc --version | head -n 1
|
||||
x86_64-w64-mingw32-windres --version | head -n 1
|
||||
|
||||
- name: Ensure Rust target
|
||||
shell: bash
|
||||
run: rustup target add "$TARGET"
|
||||
|
||||
- name: Update crates.io index with retry
|
||||
shell: bash
|
||||
- name: Fetch dependencies
|
||||
run: |
|
||||
for i in 1 2 3 4 5; do
|
||||
echo "Attempt $i to fetch dependencies..."
|
||||
@@ -172,45 +85,11 @@ jobs:
|
||||
sleep 10
|
||||
done
|
||||
|
||||
- name: UI terminal smoke test
|
||||
shell: bash
|
||||
run: |
|
||||
set -eu
|
||||
BUILD_DIR="${CARGO_TARGET_DIR:-target}"
|
||||
cargo build
|
||||
rm -f "$BUILD_DIR/debug/meatshell-debug.log" meatshell-debug.log
|
||||
SMOKE_OUTPUT="$BUILD_DIR/debug/meatshell-smoke.out"
|
||||
set +e
|
||||
MEATSHELL_SMOKE_TERMINAL=1 timeout 8s xvfb-run -a "$BUILD_DIR/debug/meatshell" >"$SMOKE_OUTPUT" 2>&1
|
||||
code=$?
|
||||
set -e
|
||||
if [ "$code" -ne 0 ]; then
|
||||
cat "$SMOKE_OUTPUT"
|
||||
if [ -f "$BUILD_DIR/debug/meatshell-debug.log" ]; then
|
||||
cat "$BUILD_DIR/debug/meatshell-debug.log"
|
||||
fi
|
||||
exit "$code"
|
||||
fi
|
||||
if [ -f "$BUILD_DIR/debug/meatshell-debug.log" ]; then
|
||||
if grep -E "ERROR|panic|Recursion detected" "$BUILD_DIR/debug/meatshell-debug.log"; then
|
||||
cat "$SMOKE_OUTPUT"
|
||||
cat "$BUILD_DIR/debug/meatshell-debug.log"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Build Windows GNU release
|
||||
shell: bash
|
||||
run: |
|
||||
set -eu
|
||||
if command -v build-win-gnu >/dev/null 2>&1; then
|
||||
build-win-gnu
|
||||
else
|
||||
cargo build --release --target "$TARGET"
|
||||
fi
|
||||
cargo build --release --target "$TARGET"
|
||||
|
||||
- name: Package Windows zip
|
||||
shell: bash
|
||||
env:
|
||||
EVENT_NAME: ${{ gitea.event_name }}
|
||||
REF: ${{ gitea.ref }}
|
||||
@@ -218,9 +97,9 @@ jobs:
|
||||
run: |
|
||||
set -eu
|
||||
BUILD_DIR="${CARGO_TARGET_DIR:-target}"
|
||||
event_name="${EVENT_NAME:-${GITHUB_EVENT_NAME:-}}"
|
||||
ref="${REF:-${GITHUB_REF:-}}"
|
||||
ref_name="${REF_NAME:-${GITHUB_REF_NAME:-dev}}"
|
||||
event_name="${EVENT_NAME:-}"
|
||||
ref="${REF:-}"
|
||||
ref_name="${REF_NAME:-dev}"
|
||||
short_sha="$(git rev-parse --short=12 HEAD)"
|
||||
if [ "$event_name" = "push" ] && [ "$ref" = "refs/tags/${ref_name}" ]; then
|
||||
VERSION="${ref_name}-${short_sha}"
|
||||
@@ -238,263 +117,18 @@ jobs:
|
||||
mkdir -p "$STAGE"
|
||||
cp "${BUILD_DIR}/${TARGET}/release/${BIN}" "$STAGE/"
|
||||
cp README.md README.en.md CHANGELOG.md "$STAGE/" 2>/dev/null || true
|
||||
if command -v 7z >/dev/null 2>&1; then
|
||||
7z a "${STAGE}.zip" "$STAGE" >/dev/null
|
||||
else
|
||||
zip -r "${STAGE}.zip" "$STAGE"
|
||||
fi
|
||||
zip -r "${STAGE}.zip" "$STAGE"
|
||||
echo "ASSET=${STAGE}.zip" >> "$GITHUB_ENV"
|
||||
echo "RELEASE_TAG=${RELEASE_TAG}" >> "$GITHUB_ENV"
|
||||
echo "RELEASE_NAME=${RELEASE_NAME}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Select release token
|
||||
shell: bash
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
WORKFLOW_TOKEN: ${{ gitea.token }}
|
||||
run: |
|
||||
set -eu
|
||||
token="${RELEASE_TOKEN:-${GITEA_TOKEN:-${WORKFLOW_TOKEN:-}}}"
|
||||
if [ -z "$token" ]; then
|
||||
echo "No release token available; set RELEASE_TOKEN or GITEA_TOKEN." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "::add-mask::$token"
|
||||
echo "RELEASE_API_KEY=$token" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Publish Gitea release
|
||||
shell: bash
|
||||
env:
|
||||
API_TOKEN: ${{ env.RELEASE_API_KEY }}
|
||||
RELEASE_TAG_ENV: ${{ env.RELEASE_TAG }}
|
||||
RELEASE_NAME_ENV: ${{ env.RELEASE_NAME }}
|
||||
EVENT_NAME: ${{ gitea.event_name }}
|
||||
REF: ${{ gitea.ref }}
|
||||
REF_NAME: ${{ gitea.ref_name }}
|
||||
RUN_ID: ${{ gitea.run_id }}
|
||||
RUN_NUMBER: ${{ gitea.run_number }}
|
||||
GITEA_SERVER_URL_CTX: ${{ gitea.server_url }}
|
||||
GITEA_REPOSITORY_CTX: ${{ gitea.repository }}
|
||||
GITHUB_SERVER_URL_CTX: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY_CTX: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
asset="${ASSET:?ASSET is not set}"
|
||||
event_name="${EVENT_NAME:-${GITHUB_EVENT_NAME:-}}"
|
||||
ref="${REF:-${GITHUB_REF:-}}"
|
||||
if [ -n "${RELEASE_TAG_ENV:-}" ]; then
|
||||
tag="$RELEASE_TAG_ENV"
|
||||
release_name="${RELEASE_NAME_ENV:-$tag}"
|
||||
release_body="Automated Windows GNU build for ${tag}."
|
||||
target_commitish="$(git rev-parse HEAD)"
|
||||
elif [ "$event_name" = "push" ] && [ "$ref" = "refs/heads/main" ]; then
|
||||
short_sha="$(git rev-parse --short=12 HEAD)"
|
||||
stamp="$(date -u +%Y%m%d-%H%M%S)"
|
||||
tag="build-${stamp}-${short_sha}"
|
||||
release_name="$tag"
|
||||
release_body="Automated Windows GNU build for main commit ${short_sha}."
|
||||
target_commitish="$(git rev-parse HEAD)"
|
||||
elif [ "$event_name" = "workflow_dispatch" ]; then
|
||||
short_sha="$(git rev-parse --short=12 HEAD)"
|
||||
run_id="${RUN_ID:-${GITHUB_RUN_ID:-${RUN_NUMBER:-${GITHUB_RUN_NUMBER:-$(date +%Y%m%d%H%M%S)}}}}"
|
||||
tag="manual-${run_id}"
|
||||
release_name="$tag"
|
||||
release_body="Manual Windows GNU build for commit ${short_sha}."
|
||||
target_commitish="$(git rev-parse HEAD)"
|
||||
else
|
||||
tag="${REF_NAME:-${GITHUB_REF_NAME:-}}"
|
||||
release_name="$tag"
|
||||
release_body="Automated Windows GNU build for ${tag}."
|
||||
target_commitish="$(git rev-parse HEAD)"
|
||||
fi
|
||||
token="${API_TOKEN:-${RELEASE_API_KEY:-}}"
|
||||
server="${GITEA_SERVER_URL_CTX:-${GITHUB_SERVER_URL_CTX:-${GITHUB_SERVER_URL:-}}}"
|
||||
repo="${GITEA_REPOSITORY_CTX:-${GITHUB_REPOSITORY_CTX:-${GITHUB_REPOSITORY:-}}}"
|
||||
|
||||
if [ -z "$token" ]; then
|
||||
echo "RELEASE_API_KEY is not set." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$tag" ]; then
|
||||
echo "Tag name is empty." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$asset" ]; then
|
||||
echo "Release asset not found: $asset" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$server" ] || [ -z "$repo" ]; then
|
||||
read -r server repo < <(python3 - <<'PY'
|
||||
import re
|
||||
import subprocess
|
||||
from urllib.parse import urlparse
|
||||
|
||||
remote = subprocess.check_output(
|
||||
["git", "remote", "get-url", "origin"], text=True
|
||||
).strip()
|
||||
if remote.startswith(("http://", "https://")):
|
||||
parsed = urlparse(remote)
|
||||
path = parsed.path.strip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
print(f"{parsed.scheme}://{parsed.netloc} {path}")
|
||||
else:
|
||||
match = re.match(r"[^@]+@([^:]+):(.+)$", remote)
|
||||
if not match:
|
||||
raise SystemExit(f"cannot parse git remote: {remote}")
|
||||
path = match.group(2)
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
print(f"https://{match.group(1)} {path}")
|
||||
PY
|
||||
)
|
||||
fi
|
||||
|
||||
server="${server%/}"
|
||||
api="$server/api/v1/repos/$repo"
|
||||
auth_header="Authorization: token $token"
|
||||
|
||||
curl_api() {
|
||||
attempt=1
|
||||
while true; do
|
||||
if curl --proxy "${HTTP_PROXY:-}" \
|
||||
--insecure \
|
||||
--fail --silent --show-error --location \
|
||||
--connect-timeout 20 --max-time 180 \
|
||||
-H "$auth_header" "$@"; then
|
||||
return 0
|
||||
fi
|
||||
if [ "$attempt" -ge 8 ]; then
|
||||
return 1
|
||||
fi
|
||||
sleep $((attempt * 3))
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
}
|
||||
|
||||
curl_status() {
|
||||
out="$1"
|
||||
shift
|
||||
attempt=1
|
||||
while true; do
|
||||
status="$(
|
||||
curl --proxy "${HTTP_PROXY:-}" \
|
||||
--insecure \
|
||||
--silent --show-error --location \
|
||||
--connect-timeout 20 --max-time 180 \
|
||||
-H "$auth_header" \
|
||||
-o "$out" -w "%{http_code}" \
|
||||
"$@" || true
|
||||
)"
|
||||
if [ -n "$status" ] && [ "$status" != "000" ]; then
|
||||
printf '%s' "$status"
|
||||
return 0
|
||||
fi
|
||||
if [ "$attempt" -ge 8 ]; then
|
||||
printf '%s' "${status:-000}"
|
||||
return 1
|
||||
fi
|
||||
sleep $((attempt * 3))
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
}
|
||||
|
||||
echo "Checking Gitea API: $server/api/v1/version"
|
||||
curl_api "$server/api/v1/version" >/dev/null
|
||||
|
||||
release_json="$(mktemp)"
|
||||
status="$(curl_status "$release_json" "$api/releases/tags/$tag" || true)"
|
||||
if [ "$status" = "404" ]; then
|
||||
payload="$(
|
||||
TAG="$tag" RELEASE_NAME="$release_name" RELEASE_BODY="$release_body" TARGET_COMMITISH="$target_commitish" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
tag = os.environ["TAG"]
|
||||
print(json.dumps({
|
||||
"tag_name": tag,
|
||||
"target_commitish": os.environ["TARGET_COMMITISH"],
|
||||
"name": os.environ["RELEASE_NAME"],
|
||||
"body": os.environ["RELEASE_BODY"],
|
||||
"draft": False,
|
||||
"prerelease": False,
|
||||
}))
|
||||
PY
|
||||
)"
|
||||
status="$(
|
||||
curl_status "$release_json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST -d "$payload" \
|
||||
"$api/releases" || true
|
||||
)"
|
||||
fi
|
||||
|
||||
if [ "$status" != "200" ] && [ "$status" != "201" ]; then
|
||||
echo "Release lookup/create failed with HTTP $status" >&2
|
||||
cat "$release_json" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
release_id="$(python3 - "$release_json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
with open(sys.argv[1], encoding="utf-8") as fh:
|
||||
print(json.load(fh)["id"])
|
||||
PY
|
||||
)"
|
||||
|
||||
update_payload="$(
|
||||
RELEASE_NAME="$release_name" RELEASE_BODY="$release_body" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
print(json.dumps({
|
||||
"name": os.environ["RELEASE_NAME"],
|
||||
"body": os.environ["RELEASE_BODY"],
|
||||
"draft": False,
|
||||
"prerelease": False,
|
||||
}))
|
||||
PY
|
||||
)"
|
||||
curl_api -H "Content-Type: application/json" \
|
||||
-X PATCH -d "$update_payload" \
|
||||
"$api/releases/$release_id" >/dev/null
|
||||
|
||||
asset_name="$(basename "$asset")"
|
||||
asset_id="$(
|
||||
ASSET_NAME="$asset_name" python3 - "$release_json" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
with open(sys.argv[1], encoding="utf-8") as fh:
|
||||
release = json.load(fh)
|
||||
assets = release.get("assets") or release.get("attachments") or []
|
||||
for item in assets:
|
||||
if item.get("name") == os.environ["ASSET_NAME"]:
|
||||
print(item["id"])
|
||||
break
|
||||
PY
|
||||
)"
|
||||
if [ -n "$asset_id" ]; then
|
||||
echo "Deleting existing release asset: $asset_name"
|
||||
curl_api -X DELETE "$api/releases/$release_id/assets/$asset_id" >/dev/null
|
||||
fi
|
||||
|
||||
asset_query="$(
|
||||
ASSET_NAME="$asset_name" python3 - <<'PY'
|
||||
import os
|
||||
import urllib.parse
|
||||
|
||||
print(urllib.parse.quote(os.environ["ASSET_NAME"]))
|
||||
PY
|
||||
)"
|
||||
echo "Uploading release asset: $asset_name"
|
||||
curl_api -X POST \
|
||||
-F "attachment=@${asset}" \
|
||||
"$api/releases/$release_id/assets?name=$asset_query" >/dev/null
|
||||
|
||||
echo "✅ Release published successfully through proxy"
|
||||
if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main'
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
files: ${{ env.ASSET }}
|
||||
tag_name: ${{ env.RELEASE_TAG }}
|
||||
name: ${{ env.RELEASE_NAME }}
|
||||
api_key: ${{ secrets.GITHUB_TOKEN }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
@@ -54,6 +54,8 @@ dark-light = "1"
|
||||
# Runtime background image decoding/blur. Used for png/jpg/jpeg/webp user
|
||||
# backgrounds before handing pixels to Slint.
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] }
|
||||
# HTTP client for WebDAV sync.
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
|
||||
|
||||
# Decode the embedded PNG icon to set the window icon at runtime (Linux only).
|
||||
# On Windows the icon comes from the .ico embedded by winresource at link time;
|
||||
|
||||
@@ -353,3 +353,24 @@ msgstr "Off"
|
||||
|
||||
msgid "Shows stable ids on UI parts for bug reports"
|
||||
msgstr "Shows stable ids on UI parts for bug reports"
|
||||
|
||||
msgid "WebDAV sync"
|
||||
msgstr "WebDAV sync"
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr "WebDAV URL"
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr "WebDAV username"
|
||||
|
||||
msgid "Password"
|
||||
msgstr "Password"
|
||||
|
||||
msgid "Test"
|
||||
msgstr "Test"
|
||||
|
||||
msgid "Upload"
|
||||
msgstr "Upload"
|
||||
|
||||
msgid "Download"
|
||||
msgstr "Download"
|
||||
|
||||
@@ -353,3 +353,24 @@ msgstr "关闭"
|
||||
|
||||
msgid "Shows stable ids on UI parts for bug reports"
|
||||
msgstr "在界面部件上显示固定 ID,便于反馈问题"
|
||||
|
||||
msgid "WebDAV sync"
|
||||
msgstr "WebDAV 同步"
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr "WebDAV 地址"
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr "WebDAV 用户名"
|
||||
|
||||
msgid "Password"
|
||||
msgstr "密码"
|
||||
|
||||
msgid "Test"
|
||||
msgstr "测试"
|
||||
|
||||
msgid "Upload"
|
||||
msgstr "上传"
|
||||
|
||||
msgid "Download"
|
||||
msgstr "下载"
|
||||
|
||||
214
src/app.rs
214
src/app.rs
@@ -444,6 +444,14 @@ pub fn run() -> Result<()> {
|
||||
.to_string()
|
||||
.into(),
|
||||
);
|
||||
// Load WebDAV settings into the UI.
|
||||
{
|
||||
let s = store.borrow();
|
||||
window.set_webdav_enabled(s.webdav_enabled());
|
||||
window.set_webdav_url_text(s.webdav_url().to_string().into());
|
||||
window.set_webdav_username_text(s.webdav_username().to_string().into());
|
||||
window.set_webdav_password_text(s.webdav_password().as_str().to_string().into());
|
||||
}
|
||||
window.on_debug_click(move |parent, button| {
|
||||
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
||||
tracing::info!("[{now}]点击[{button}/{parent}]");
|
||||
@@ -648,6 +656,212 @@ pub fn run() -> Result<()> {
|
||||
},
|
||||
);
|
||||
}
|
||||
// --- WebDAV sync callbacks -------------------------------------------
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let store = store.clone();
|
||||
window.on_webdav_save_settings(move |enabled, url, username, password| {
|
||||
{
|
||||
let mut s = store.borrow_mut();
|
||||
s.set_webdav_enabled(enabled);
|
||||
s.set_webdav_url(url.to_string());
|
||||
s.set_webdav_username(username.to_string());
|
||||
s.set_webdav_password(crate::config::Secret::new(password.to_string()));
|
||||
if let Err(err) = s.save() {
|
||||
tracing::warn!("failed to save WebDAV settings: {err:#}");
|
||||
}
|
||||
}
|
||||
if let Some(w) = weak.upgrade() {
|
||||
w.set_webdav_enabled(enabled);
|
||||
w.set_webdav_url_text(url);
|
||||
w.set_webdav_username_text(username);
|
||||
w.set_webdav_password_text(password);
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let store = store.clone();
|
||||
let rt = runtime.clone();
|
||||
window.on_webdav_test_connection(move || {
|
||||
let url: SharedString = store.borrow().webdav_url().to_string().into();
|
||||
let username: SharedString = store.borrow().webdav_username().to_string().into();
|
||||
let password: SharedString = store.borrow().webdav_password().as_str().to_string().into();
|
||||
if let Some(w) = weak.upgrade() {
|
||||
w.set_webdav_status_text(t("测试连接中...", "Testing...").into());
|
||||
}
|
||||
let weak2 = weak.clone();
|
||||
rt.spawn(async move {
|
||||
let result = crate::webdav::test_connection(
|
||||
url.as_str(),
|
||||
username.as_str(),
|
||||
password.as_str(),
|
||||
)
|
||||
.await;
|
||||
let test_result = match result {
|
||||
Ok(()) => Ok(t("连接成功", "Connection OK").to_string()),
|
||||
Err(err) => Err(format!("{}: {err:#}", t("连接失败", "Connection failed"))),
|
||||
};
|
||||
let _ = slint::invoke_from_event_loop(move || {
|
||||
if let Some(w) = weak2.upgrade() {
|
||||
let msg = match test_result {
|
||||
Ok(m) => m,
|
||||
Err(e) => e,
|
||||
};
|
||||
w.set_webdav_status_text(msg.into());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let store = store.clone();
|
||||
let rt = runtime.clone();
|
||||
window.on_webdav_push(move || {
|
||||
let url: SharedString = store.borrow().webdav_url().to_string().into();
|
||||
let username: SharedString = store.borrow().webdav_username().to_string().into();
|
||||
let password: SharedString = store.borrow().webdav_password().as_str().to_string().into();
|
||||
{
|
||||
let s = store.borrow();
|
||||
if let Err(err) = s.save() {
|
||||
tracing::warn!("failed to save config before WebDAV push: {err:#}");
|
||||
}
|
||||
}
|
||||
let config_path = {
|
||||
let s = store.borrow();
|
||||
s.config_dir().join("sessions.json")
|
||||
};
|
||||
let config_json = match std::fs::read_to_string(&config_path) {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
if let Some(w) = weak.upgrade() {
|
||||
w.set_webdav_status_text(
|
||||
format!("{}: {err:#}", t("读取配置失败", "Failed to read config"))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(w) = weak.upgrade() {
|
||||
w.set_webdav_status_text(t("正在上传...", "Uploading...").into());
|
||||
}
|
||||
let weak2 = weak.clone();
|
||||
rt.spawn(async move {
|
||||
let result =
|
||||
crate::webdav::push(url.as_str(), username.as_str(), password.as_str(), &config_json)
|
||||
.await;
|
||||
let push_result = match result {
|
||||
Ok(()) => Ok(t("上传成功", "Upload OK").to_string()),
|
||||
Err(err) => Err(format!("{}: {err:#}", t("上传失败", "Upload failed"))),
|
||||
};
|
||||
let _ = slint::invoke_from_event_loop(move || {
|
||||
if let Some(w) = weak2.upgrade() {
|
||||
let msg = match push_result {
|
||||
Ok(m) => m,
|
||||
Err(e) => e,
|
||||
};
|
||||
w.set_webdav_status_text(msg.into());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let store = store.clone();
|
||||
let rt = runtime.clone();
|
||||
let sessions_model = sessions_model.clone();
|
||||
let folders_names_model = connection_folder_names_model.clone();
|
||||
let pull_result: Arc<Mutex<Option<std::result::Result<String, String>>>> =
|
||||
Arc::new(Mutex::new(None));
|
||||
{
|
||||
let pull_result = pull_result.clone();
|
||||
let weak = weak.clone();
|
||||
let store = store.clone();
|
||||
let sessions_model = sessions_model.clone();
|
||||
let folders_model = connection_folders_model.clone();
|
||||
let folders_names_model = folders_names_model.clone();
|
||||
let timer = slint::Timer::default();
|
||||
timer.start(
|
||||
slint::TimerMode::Repeated,
|
||||
std::time::Duration::from_millis(200),
|
||||
move || {
|
||||
let mut guard = pull_result.lock().unwrap();
|
||||
if let Some(result) = guard.take() {
|
||||
drop(guard);
|
||||
match result {
|
||||
Ok(json) => {
|
||||
match serde_json::from_str::<crate::config::ConfigFile>(&json) {
|
||||
Ok(new_cfg) => {
|
||||
{
|
||||
let mut s = store.borrow_mut();
|
||||
s.replace_config(new_cfg);
|
||||
if let Err(err) = s.save() {
|
||||
tracing::warn!(
|
||||
"failed to save downloaded config: {err:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
let s = store.borrow();
|
||||
sync_sessions_to_model(&s, &sessions_model);
|
||||
sync_connection_folders_to_model(
|
||||
&s,
|
||||
&folders_model,
|
||||
&folders_names_model,
|
||||
);
|
||||
if let Some(w) = weak.upgrade() {
|
||||
w.set_webdav_status_text(
|
||||
t("下载成功", "Download OK").to_string().into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
if let Some(w) = weak.upgrade() {
|
||||
w.set_webdav_status_text(
|
||||
format!(
|
||||
"{}: {err:#}",
|
||||
t("解析配置失败", "Failed to parse config")
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
if let Some(w) = weak.upgrade() {
|
||||
w.set_webdav_status_text(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
// Keep the timer alive for the app's lifetime by leaking it.
|
||||
// A more refined approach would store it in a global.
|
||||
std::mem::forget(timer);
|
||||
}
|
||||
window.on_webdav_pull(move || {
|
||||
let url: SharedString = store.borrow().webdav_url().to_string().into();
|
||||
let username: SharedString = store.borrow().webdav_username().to_string().into();
|
||||
let password: SharedString = store.borrow().webdav_password().as_str().to_string().into();
|
||||
if let Some(w) = weak.upgrade() {
|
||||
w.set_webdav_status_text(t("正在下载...", "Downloading...").into());
|
||||
}
|
||||
let pull_result = pull_result.clone();
|
||||
rt.spawn(async move {
|
||||
let result =
|
||||
crate::webdav::pull(url.as_str(), username.as_str(), password.as_str()).await;
|
||||
let mapped = match result {
|
||||
Ok(json) => Ok(json),
|
||||
Err(err) => Err(format!("{}: {err:#}", t("下载失败", "Download failed"))),
|
||||
};
|
||||
*pull_result.lock().unwrap() = Some(mapped);
|
||||
});
|
||||
});
|
||||
}
|
||||
{
|
||||
let store = store.clone();
|
||||
window.on_sftp_name_column_width_changed(move |width_px: f32| {
|
||||
|
||||
114
src/config.rs
114
src/config.rs
@@ -383,6 +383,23 @@ pub struct ConfigFile {
|
||||
/// FinalShell-style custom commands.
|
||||
#[serde(default)]
|
||||
pub commands: Vec<StoredCommand>,
|
||||
|
||||
// --- WebDAV sync settings ------------------------------------------------
|
||||
/// WebDAV server URL, e.g. "https://dav.example.com/meatshell/".
|
||||
#[serde(default)]
|
||||
pub webdav_url: String,
|
||||
/// WebDAV username for authentication.
|
||||
#[serde(default)]
|
||||
pub webdav_username: String,
|
||||
/// WebDAV password (encrypted at rest, decrypted in memory).
|
||||
#[serde(default, skip_serializing_if = "Secret::is_empty")]
|
||||
pub webdav_password: Secret,
|
||||
/// WebDAV keychain reference for the password.
|
||||
#[serde(default)]
|
||||
pub webdav_password_ref: String,
|
||||
/// Whether WebDAV sync is enabled.
|
||||
#[serde(default)]
|
||||
pub webdav_enabled: bool,
|
||||
}
|
||||
|
||||
pub struct ConfigStore {
|
||||
@@ -653,6 +670,28 @@ impl ConfigStore {
|
||||
session.password = Secret::new(plain);
|
||||
}
|
||||
}
|
||||
// Load WebDAV password from keychain or encrypted fallback.
|
||||
if !cfg.webdav_password_ref.is_empty() {
|
||||
match crate::keychain::get_password(&cfg.webdav_password_ref) {
|
||||
Ok(plain) => {
|
||||
cfg.webdav_password = Secret::new(plain);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"failed to read WebDAV keychain secret: {err:#}"
|
||||
);
|
||||
if let Some(plain) =
|
||||
Self::try_decrypt(&key, cfg.webdav_password.as_str())
|
||||
{
|
||||
cfg.webdav_password = Secret::new(plain);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(plain) =
|
||||
Self::try_decrypt(&key, cfg.webdav_password.as_str())
|
||||
{
|
||||
cfg.webdav_password = Secret::new(plain);
|
||||
}
|
||||
Self::normalise_config(&mut cfg);
|
||||
cfg
|
||||
}
|
||||
@@ -1214,6 +1253,58 @@ impl ConfigStore {
|
||||
}
|
||||
}
|
||||
|
||||
// ── WebDAV sync settings ─────────────────────────────────────────────
|
||||
|
||||
pub fn webdav_enabled(&self) -> bool {
|
||||
self.cache.webdav_enabled
|
||||
}
|
||||
|
||||
pub fn set_webdav_enabled(&mut self, enabled: bool) {
|
||||
self.cache.webdav_enabled = enabled;
|
||||
}
|
||||
|
||||
pub fn webdav_url(&self) -> &str {
|
||||
&self.cache.webdav_url
|
||||
}
|
||||
|
||||
pub fn set_webdav_url(&mut self, url: String) {
|
||||
self.cache.webdav_url = url;
|
||||
}
|
||||
|
||||
pub fn webdav_username(&self) -> &str {
|
||||
&self.cache.webdav_username
|
||||
}
|
||||
|
||||
pub fn set_webdav_username(&mut self, username: String) {
|
||||
self.cache.webdav_username = username;
|
||||
}
|
||||
|
||||
pub fn webdav_password(&self) -> &Secret {
|
||||
&self.cache.webdav_password
|
||||
}
|
||||
|
||||
pub fn set_webdav_password(&mut self, password: Secret) {
|
||||
self.cache.webdav_password = password;
|
||||
}
|
||||
|
||||
const WEBDAV_KEYCHAIN_PREFIX: &'static str = "webdav:";
|
||||
|
||||
fn webdav_keychain_ref() -> String {
|
||||
format!("{}{}", Self::WEBDAV_KEYCHAIN_PREFIX, "default")
|
||||
}
|
||||
|
||||
/// Replace the in-memory config with a new one (used after WebDAV pull).
|
||||
pub fn replace_config(&mut self, new_cfg: ConfigFile) {
|
||||
// Preserve the WebDAV password from the current config so we don't
|
||||
// overwrite it with whatever was in the remote file.
|
||||
let old_webdav_password = self.cache.webdav_password.clone();
|
||||
let old_webdav_password_ref = self.cache.webdav_password_ref.clone();
|
||||
self.cache = new_cfg;
|
||||
self.cache.webdav_password = old_webdav_password;
|
||||
self.cache.webdav_password_ref = old_webdav_password_ref;
|
||||
Self::normalise_config(&mut self.cache);
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<()> {
|
||||
// Build a disk copy where every non-empty password is either migrated
|
||||
// into the OS keychain (preferred) or encrypted as a fallback.
|
||||
@@ -1242,6 +1333,29 @@ impl ConfigStore {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Save WebDAV password to keychain or encrypted fallback.
|
||||
if !disk.webdav_password.is_empty()
|
||||
&& !disk.webdav_password.as_str().starts_with(Self::ENC_PREFIX)
|
||||
{
|
||||
let password_ref = if disk.webdav_password_ref.is_empty() {
|
||||
Self::webdav_keychain_ref()
|
||||
} else {
|
||||
disk.webdav_password_ref.clone()
|
||||
};
|
||||
match crate::keychain::set_password(&password_ref, disk.webdav_password.as_str()) {
|
||||
Ok(()) => {
|
||||
disk.webdav_password_ref = password_ref;
|
||||
disk.webdav_password = Secret::default();
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"failed to write WebDAV password to OS keychain; using encrypted config fallback: {err:#}"
|
||||
);
|
||||
let enc = Self::encrypt(&self.key, disk.webdav_password.as_str())?;
|
||||
disk.webdav_password = Secret::new(enc);
|
||||
}
|
||||
}
|
||||
}
|
||||
let raw = serde_json::to_string_pretty(&disk)?;
|
||||
// Write to a sibling temp file then rename — cheap atomicity.
|
||||
let tmp = self.path.with_extension("json.tmp");
|
||||
|
||||
@@ -16,6 +16,7 @@ mod ssh;
|
||||
mod ssh_config;
|
||||
mod system;
|
||||
mod telnet;
|
||||
mod webdav;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let log_path = debug_log::init();
|
||||
|
||||
112
src/webdav.rs
Normal file
112
src/webdav.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
//! WebDAV sync support for configuration backup/restore.
|
||||
//!
|
||||
//! Provides functions to push (upload) and pull (download) the sessions.json
|
||||
//! config file to/from a WebDAV server, enabling multi-device sync.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use reqwest::Client;
|
||||
|
||||
/// Test connection to a WebDAV server with the given credentials.
|
||||
///
|
||||
/// Sends a PROPFIND request to verify the URL and credentials are valid.
|
||||
pub async fn test_connection(url: &str, username: &str, password: &str) -> Result<()> {
|
||||
let client = build_client()?;
|
||||
let url = normalize_url(url);
|
||||
|
||||
let response = client
|
||||
.request(reqwest::Method::from_bytes(b"PROPFIND")?, &url)
|
||||
.basic_auth(username, Some(password))
|
||||
.header("Depth", "0")
|
||||
.send()
|
||||
.await
|
||||
.context("failed to connect to WebDAV server")?;
|
||||
|
||||
if response.status().is_success() || response.status().as_u16() == 207 {
|
||||
Ok(())
|
||||
} else if response.status().as_u16() == 401 {
|
||||
anyhow::bail!("authentication failed: invalid username or password")
|
||||
} else if response.status().as_u16() == 404 {
|
||||
anyhow::bail!("WebDAV path not found: {}", url)
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"WebDAV server returned HTTP {}",
|
||||
response.status().as_u16()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload (push) config JSON to the WebDAV server.
|
||||
///
|
||||
/// The `config_json` should be the raw content of sessions.json.
|
||||
/// The file is uploaded as `{url}/sessions.json`.
|
||||
pub async fn push(url: &str, username: &str, password: &str, config_json: &str) -> Result<()> {
|
||||
let client = build_client()?;
|
||||
let url = format!("{}sessions.json", normalize_url(url));
|
||||
|
||||
let response = client
|
||||
.put(&url)
|
||||
.basic_auth(username, Some(password))
|
||||
.header("Content-Type", "application/json")
|
||||
.body(config_json.to_string())
|
||||
.send()
|
||||
.await
|
||||
.context("failed to upload config to WebDAV server")?;
|
||||
|
||||
if response.status().is_success() || response.status().as_u16() == 201 {
|
||||
Ok(())
|
||||
} else if response.status().as_u16() == 401 {
|
||||
anyhow::bail!("authentication failed: invalid username or password")
|
||||
} else {
|
||||
let status = response.status().as_u16();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
anyhow::bail!("WebDAV upload failed (HTTP {status}): {body}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Download (pull) config JSON from the WebDAV server.
|
||||
///
|
||||
/// Returns the raw JSON content of sessions.json.
|
||||
pub async fn pull(url: &str, username: &str, password: &str) -> Result<String> {
|
||||
let client = build_client()?;
|
||||
let url = format!("{}sessions.json", normalize_url(url));
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.basic_auth(username, Some(password))
|
||||
.send()
|
||||
.await
|
||||
.context("failed to download config from WebDAV server")?;
|
||||
|
||||
if response.status().is_success() {
|
||||
response
|
||||
.text()
|
||||
.await
|
||||
.context("failed to read WebDAV response body")
|
||||
} else if response.status().as_u16() == 401 {
|
||||
anyhow::bail!("authentication failed: invalid username or password")
|
||||
} else if response.status().as_u16() == 404 {
|
||||
anyhow::bail!("no config found on WebDAV server (file not found)")
|
||||
} else {
|
||||
let status = response.status().as_u16();
|
||||
anyhow::bail!("WebDAV download failed (HTTP {status})")
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an HTTP client with sensible defaults for WebDAV operations.
|
||||
fn build_client() -> Result<Client> {
|
||||
Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.redirect(reqwest::redirect::Policy::limited(5))
|
||||
.build()
|
||||
.context("failed to create HTTP client")
|
||||
}
|
||||
|
||||
/// Ensure the URL ends with a single trailing slash.
|
||||
fn normalize_url(url: &str) -> String {
|
||||
let url = url.trim();
|
||||
if url.ends_with('/') {
|
||||
url.to_string()
|
||||
} else {
|
||||
format!("{url}/")
|
||||
}
|
||||
}
|
||||
123
ui/app.slint
123
ui/app.slint
@@ -222,6 +222,16 @@ export component AppWindow inherits Window {
|
||||
in-out property <bool> ui-debug-ids: false;
|
||||
in property <image> background-image;
|
||||
in-out property <string> config-dir-text;
|
||||
// WebDAV sync settings
|
||||
in-out property <bool> webdav-enabled: false;
|
||||
in-out property <string> webdav-url-text;
|
||||
in-out property <string> webdav-username-text;
|
||||
in-out property <string> webdav-password-text;
|
||||
in-out property <string> webdav-status-text;
|
||||
callback webdav-test-connection();
|
||||
callback webdav-push();
|
||||
callback webdav-pull();
|
||||
callback webdav-save-settings(bool, string, string, string);
|
||||
callback app-config-save(string, string, string, int, string, string, string, bool, string);
|
||||
callback app-config-pick-dir();
|
||||
callback app-background-pick();
|
||||
@@ -1012,7 +1022,7 @@ export component AppWindow inherits Window {
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: min(640px, parent.width - 48px);
|
||||
height: min(500px, parent.height - 48px);
|
||||
height: min(620px, parent.height - 48px);
|
||||
background: Theme.bg-panel;
|
||||
border-radius: Theme.radius-md;
|
||||
border-width: 1px;
|
||||
@@ -1301,6 +1311,112 @@ export component AppWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
// --- WebDAV Sync Settings ---
|
||||
Rectangle { height: 1px; background: Theme.border-subtle; }
|
||||
|
||||
HorizontalLayout {
|
||||
height: 30px;
|
||||
spacing: 10px;
|
||||
Text {
|
||||
width: 148px;
|
||||
height: parent.height;
|
||||
text: @tr("WebDAV sync");
|
||||
color: Theme.text-secondary;
|
||||
font-size: Theme.fs-sm;
|
||||
font-weight: 600;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
ConfigButton {
|
||||
label: root.webdav-enabled ? @tr("On") : @tr("Off");
|
||||
button-width: 76px;
|
||||
selected: root.webdav-enabled;
|
||||
clicked => {
|
||||
root.webdav-enabled = !root.webdav-enabled;
|
||||
}
|
||||
}
|
||||
Rectangle { horizontal-stretch: 1; }
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
height: 30px;
|
||||
spacing: 10px;
|
||||
Text {
|
||||
width: 148px;
|
||||
height: parent.height;
|
||||
text: @tr("WebDAV URL");
|
||||
color: Theme.text-secondary;
|
||||
font-size: Theme.fs-sm;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
ConfigTextField {
|
||||
value <=> root.webdav-url-text;
|
||||
field-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
height: 30px;
|
||||
spacing: 10px;
|
||||
Text {
|
||||
width: 148px;
|
||||
height: parent.height;
|
||||
text: @tr("WebDAV username");
|
||||
color: Theme.text-secondary;
|
||||
font-size: Theme.fs-sm;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
ConfigTextField {
|
||||
value <=> root.webdav-username-text;
|
||||
field-width: 160px;
|
||||
}
|
||||
Text {
|
||||
width: 80px;
|
||||
height: parent.height;
|
||||
text: @tr("Password");
|
||||
color: Theme.text-secondary;
|
||||
font-size: Theme.fs-sm;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
ConfigTextField {
|
||||
value <=> root.webdav-password-text;
|
||||
field-width: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
height: 30px;
|
||||
spacing: 10px;
|
||||
Text {
|
||||
width: 148px;
|
||||
height: parent.height;
|
||||
text: root.webdav-status-text;
|
||||
color: root.webdav-status-text == @tr("Connection OK") ? Theme.success : Theme.text-muted;
|
||||
font-size: Theme.fs-xs;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
ConfigButton {
|
||||
label: @tr("Test");
|
||||
button-width: 72px;
|
||||
clicked => { root.webdav-test-connection(); }
|
||||
}
|
||||
ConfigButton {
|
||||
label: @tr("Upload");
|
||||
button-width: 82px;
|
||||
clicked => { root.webdav-push(); }
|
||||
}
|
||||
ConfigButton {
|
||||
label: @tr("Download");
|
||||
button-width: 82px;
|
||||
clicked => { root.webdav-pull(); }
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
height: 30px;
|
||||
spacing: 10px;
|
||||
@@ -1319,6 +1435,11 @@ export component AppWindow inherits Window {
|
||||
primary: true;
|
||||
clicked => {
|
||||
if root.ui-debug-ids { root.debug-click("configuration", "save"); }
|
||||
root.webdav-save-settings(
|
||||
root.webdav-enabled,
|
||||
root.webdav-url-text,
|
||||
root.webdav-username-text,
|
||||
root.webdav-password-text);
|
||||
root.app-config-save(
|
||||
root.max-tabs-text,
|
||||
root.terminal-font-size-text,
|
||||
|
||||
Reference in New Issue
Block a user