This commit is contained in:
@@ -142,11 +142,37 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish Gitea release
|
- name: Publish Gitea release
|
||||||
if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main'
|
if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main'
|
||||||
uses: https://gitea.com/actions/release-action@main
|
env:
|
||||||
with:
|
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: ${{ env.ASSET }}
|
run: |
|
||||||
tag_name: ${{ env.RELEASE_TAG }}
|
set -eu
|
||||||
name: ${{ env.RELEASE_NAME }}
|
TAG="${{ env.RELEASE_TAG }}"
|
||||||
api_key: ${{ secrets.GITHUB_TOKEN }}
|
NAME="${{ env.RELEASE_NAME }}"
|
||||||
draft: false
|
FILE="${{ env.ASSET }}"
|
||||||
prerelease: false
|
# Try GITHUB_SERVER_URL first, fall back to GITEA_URL or localhost
|
||||||
|
BASE_URL="${GITHUB_SERVER_URL:-${GITEA_URL:-http://localhost:3000}}"
|
||||||
|
API="${BASE_URL}/api/v1"
|
||||||
|
REPO="${GITHUB_REPOSITORY:-$(git remote get-url origin | sed 's#.*[:/]\([^/]*/[^/]*\)\.git#\1#')}"
|
||||||
|
echo "Creating release ${TAG} on ${API}/repos/${REPO}..."
|
||||||
|
RELEASE_ID=$(curl -s -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/repos/${REPO}/releases" \
|
||||||
|
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${NAME}\",\"draft\":false,\"prerelease\":false}" \
|
||||||
|
-o /tmp/release.json | tail -c3)
|
||||||
|
if [ "$RELEASE_ID" = "409" ]; then
|
||||||
|
echo "Release ${TAG} already exists, updating..."
|
||||||
|
curl -s -X PATCH \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/repos/${REPO}/releases/tags/${TAG}" \
|
||||||
|
-d "{\"name\":\"${NAME}\"}" > /dev/null
|
||||||
|
fi
|
||||||
|
ATTACH_URL="${API}/repos/${REPO}/releases/tags/${TAG}/assets"
|
||||||
|
echo "Uploading ${FILE} to ${ATTACH_URL}..."
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/zip" \
|
||||||
|
--data-binary "@${FILE}" \
|
||||||
|
"${ATTACH_URL}?filename=$(basename ${FILE})"
|
||||||
|
echo "✅ Release ${TAG} published"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ zeroize = "1"
|
|||||||
# (HTTP CONNECT proxies are handled in src/proxy.rs without an extra crate.)
|
# (HTTP CONNECT proxies are handled in src/proxy.rs without an extra crate.)
|
||||||
tokio-socks = "0.5"
|
tokio-socks = "0.5"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
urlencoding = "2"
|
||||||
# Cross-platform serial port access for the Serial session type (#14, #17).
|
# Cross-platform serial port access for the Serial session type (#14, #17).
|
||||||
# Opens COM3 / /dev/ttyUSB0 etc.; on Linux the default `libudev` feature is
|
# Opens COM3 / /dev/ttyUSB0 etc.; on Linux the default `libudev` feature is
|
||||||
# used for port enumeration (CI installs libudev-dev).
|
# used for port enumeration (CI installs libudev-dev).
|
||||||
|
|||||||
171
src/app.rs
171
src/app.rs
@@ -451,6 +451,7 @@ pub fn run() -> Result<()> {
|
|||||||
window.set_webdav_url_text(s.webdav_url().to_string().into());
|
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_username_text(s.webdav_username().to_string().into());
|
||||||
window.set_webdav_password_text(s.webdav_password().as_str().to_string().into());
|
window.set_webdav_password_text(s.webdav_password().as_str().to_string().into());
|
||||||
|
window.set_webdav_auto_sync_interval(s.webdav_auto_sync_interval().to_string().into());
|
||||||
}
|
}
|
||||||
window.on_debug_click(move |parent, button| {
|
window.on_debug_click(move |parent, button| {
|
||||||
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
||||||
@@ -660,13 +661,15 @@ pub fn run() -> Result<()> {
|
|||||||
{
|
{
|
||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let store = store.clone();
|
let store = store.clone();
|
||||||
window.on_webdav_save_settings(move |enabled, url, username, password| {
|
window.on_webdav_save_settings(move |enabled, url, username, password, auto_sync_interval| {
|
||||||
|
let interval: u32 = auto_sync_interval.to_string().trim().parse().unwrap_or(0);
|
||||||
{
|
{
|
||||||
let mut s = store.borrow_mut();
|
let mut s = store.borrow_mut();
|
||||||
s.set_webdav_enabled(enabled);
|
s.set_webdav_enabled(enabled);
|
||||||
s.set_webdav_url(url.to_string());
|
s.set_webdav_url(url.to_string());
|
||||||
s.set_webdav_username(username.to_string());
|
s.set_webdav_username(username.to_string());
|
||||||
s.set_webdav_password(crate::config::Secret::new(password.to_string()));
|
s.set_webdav_password(crate::config::Secret::new(password.to_string()));
|
||||||
|
s.set_webdav_auto_sync_interval(interval);
|
||||||
if let Err(err) = s.save() {
|
if let Err(err) = s.save() {
|
||||||
tracing::warn!("failed to save WebDAV settings: {err:#}");
|
tracing::warn!("failed to save WebDAV settings: {err:#}");
|
||||||
}
|
}
|
||||||
@@ -676,6 +679,7 @@ pub fn run() -> Result<()> {
|
|||||||
w.set_webdav_url_text(url);
|
w.set_webdav_url_text(url);
|
||||||
w.set_webdav_username_text(username);
|
w.set_webdav_username_text(username);
|
||||||
w.set_webdav_password_text(password);
|
w.set_webdav_password_text(password);
|
||||||
|
w.set_webdav_auto_sync_interval(auto_sync_interval);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -749,19 +753,74 @@ pub fn run() -> Result<()> {
|
|||||||
}
|
}
|
||||||
let weak2 = weak.clone();
|
let weak2 = weak.clone();
|
||||||
rt.spawn(async move {
|
rt.spawn(async move {
|
||||||
let result =
|
// Generate timestamped backup filename
|
||||||
crate::webdav::push(url.as_str(), username.as_str(), password.as_str(), &config_json)
|
let now = chrono::Local::now();
|
||||||
|
let timestamp = now.format("%Y-%m-%d_%H-%M-%S").to_string();
|
||||||
|
let backup_filename = format!("sessions_{}.json", timestamp);
|
||||||
|
|
||||||
|
// Push timestamped backup
|
||||||
|
let result = crate::webdav::push_backup(
|
||||||
|
url.as_str(),
|
||||||
|
username.as_str(),
|
||||||
|
password.as_str(),
|
||||||
|
&config_json,
|
||||||
|
&backup_filename,
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
let push_result = match result {
|
|
||||||
Ok(()) => Ok(t("上传成功", "Upload OK").to_string()),
|
if let Err(err) = result {
|
||||||
Err(err) => Err(format!("{}: {err:#}", t("上传失败", "Upload failed"))),
|
let msg = format!("{}: {err:#}", t("备份上传失败", "Backup upload failed"));
|
||||||
};
|
|
||||||
let _ = slint::invoke_from_event_loop(move || {
|
let _ = slint::invoke_from_event_loop(move || {
|
||||||
if let Some(w) = weak2.upgrade() {
|
if let Some(w) = weak2.upgrade() {
|
||||||
let msg = match push_result {
|
w.set_webdav_status_text(msg.into());
|
||||||
Ok(m) => m,
|
}
|
||||||
Err(e) => e,
|
});
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up old backups: keep only 10 most recent
|
||||||
|
if let Ok(files) = crate::webdav::list_files(
|
||||||
|
url.as_str(),
|
||||||
|
username.as_str(),
|
||||||
|
password.as_str(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
// Filter for timestamped backup files
|
||||||
|
let mut backups: Vec<String> = files
|
||||||
|
.iter()
|
||||||
|
.filter(|f| {
|
||||||
|
f.starts_with("sessions_") && f.ends_with(".json") && *f != "sessions.json"
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Sort by timestamp (newest first) — timestamp format is lexicographically sortable
|
||||||
|
backups.sort_by(|a, b| b.cmp(a));
|
||||||
|
|
||||||
|
// Delete oldest backups beyond 10
|
||||||
|
let max_backups = 10;
|
||||||
|
if backups.len() > max_backups {
|
||||||
|
let to_delete = &backups[max_backups..];
|
||||||
|
for file in to_delete {
|
||||||
|
let _ = crate::webdav::delete_file(
|
||||||
|
url.as_str(),
|
||||||
|
username.as_str(),
|
||||||
|
password.as_str(),
|
||||||
|
file,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = slint::invoke_from_event_loop(move || {
|
||||||
|
if let Some(w) = weak2.upgrade() {
|
||||||
|
let msg = format!(
|
||||||
|
"{} ({})",
|
||||||
|
t("备份上传成功", "Backup OK"),
|
||||||
|
backup_filename
|
||||||
|
);
|
||||||
w.set_webdav_status_text(msg.into());
|
w.set_webdav_status_text(msg.into());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -862,6 +921,96 @@ pub fn run() -> Result<()> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- WebDAV auto-sync timer -------------------------------------------
|
||||||
|
// Periodically push the config to the WebDAV server when auto-sync is
|
||||||
|
// enabled. The timer fires every minute; the actual push only happens
|
||||||
|
// when the configured interval has elapsed since the last push.
|
||||||
|
{
|
||||||
|
let store = store.clone();
|
||||||
|
let rt = runtime.clone();
|
||||||
|
let auto_sync_last: Arc<Mutex<Option<std::time::Instant>>> =
|
||||||
|
Arc::new(Mutex::new(None));
|
||||||
|
let auto_sync_timer = slint::Timer::default();
|
||||||
|
auto_sync_timer.start(
|
||||||
|
slint::TimerMode::Repeated,
|
||||||
|
std::time::Duration::from_secs(60),
|
||||||
|
move || {
|
||||||
|
let (enabled, url, username, password, interval) = {
|
||||||
|
let s = store.borrow();
|
||||||
|
(
|
||||||
|
s.webdav_enabled(),
|
||||||
|
s.webdav_url().to_string(),
|
||||||
|
s.webdav_username().to_string(),
|
||||||
|
s.webdav_password().as_str().to_string(),
|
||||||
|
s.webdav_auto_sync_interval(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if !enabled || url.is_empty() || interval == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let should_sync = {
|
||||||
|
let mut last = auto_sync_last.lock().unwrap();
|
||||||
|
match *last {
|
||||||
|
Some(t) => {
|
||||||
|
if t.elapsed().as_secs() >= (interval as u64) * 60 {
|
||||||
|
*last = Some(std::time::Instant::now());
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
*last = Some(std::time::Instant::now());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !should_sync {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Save config then read the file.
|
||||||
|
{
|
||||||
|
let s = store.borrow();
|
||||||
|
if let Err(err) = s.save() {
|
||||||
|
tracing::warn!("auto-sync: failed to save config: {err:#}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) => {
|
||||||
|
tracing::warn!("auto-sync: failed to read config: {err:#}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tracing::info!("WebDAV auto-sync: pushing config (interval={}min)", interval);
|
||||||
|
let url_clone = url.clone();
|
||||||
|
let username_clone = username.clone();
|
||||||
|
let password_clone = password.clone();
|
||||||
|
rt.spawn(async move {
|
||||||
|
let result = crate::webdav::push(
|
||||||
|
&url_clone,
|
||||||
|
&username_clone,
|
||||||
|
&password_clone,
|
||||||
|
&config_json,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
match result {
|
||||||
|
Ok(()) => tracing::info!("WebDAV auto-sync: push OK"),
|
||||||
|
Err(err) => tracing::warn!("WebDAV auto-sync: push failed: {err:#}"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Keep the auto-sync timer alive by leaking it.
|
||||||
|
std::mem::forget(auto_sync_timer);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let store = store.clone();
|
let store = store.clone();
|
||||||
window.on_sftp_name_column_width_changed(move |width_px: f32| {
|
window.on_sftp_name_column_width_changed(move |width_px: f32| {
|
||||||
|
|||||||
@@ -400,6 +400,9 @@ pub struct ConfigFile {
|
|||||||
/// Whether WebDAV sync is enabled.
|
/// Whether WebDAV sync is enabled.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub webdav_enabled: bool,
|
pub webdav_enabled: bool,
|
||||||
|
/// Auto-sync interval in minutes (0 = disabled).
|
||||||
|
#[serde(default)]
|
||||||
|
pub webdav_auto_sync_interval: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ConfigStore {
|
pub struct ConfigStore {
|
||||||
@@ -1287,6 +1290,14 @@ impl ConfigStore {
|
|||||||
self.cache.webdav_password = password;
|
self.cache.webdav_password = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn webdav_auto_sync_interval(&self) -> u32 {
|
||||||
|
self.cache.webdav_auto_sync_interval
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_webdav_auto_sync_interval(&mut self, minutes: u32) {
|
||||||
|
self.cache.webdav_auto_sync_interval = minutes;
|
||||||
|
}
|
||||||
|
|
||||||
const WEBDAV_KEYCHAIN_PREFIX: &'static str = "webdav:";
|
const WEBDAV_KEYCHAIN_PREFIX: &'static str = "webdav:";
|
||||||
|
|
||||||
fn webdav_keychain_ref() -> String {
|
fn webdav_keychain_ref() -> String {
|
||||||
|
|||||||
104
src/webdav.rs
104
src/webdav.rs
@@ -92,6 +92,110 @@ pub async fn pull(url: &str, username: &str, password: &str) -> Result<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Upload (push) config JSON to the WebDAV server with a custom filename.
|
||||||
|
///
|
||||||
|
/// Used for timestamped manual backups, e.g. `sessions_2024-01-15_10-30-00.json`.
|
||||||
|
pub async fn push_backup(
|
||||||
|
url: &str,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
config_json: &str,
|
||||||
|
filename: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let client = build_client()?;
|
||||||
|
let url = format!("{}{}", normalize_url(url), filename);
|
||||||
|
|
||||||
|
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 backup 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 backup upload failed (HTTP {status}): {body}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List files in the WebDAV directory (depth=1).
|
||||||
|
///
|
||||||
|
/// Returns a list of filenames (not full paths) found at the given URL.
|
||||||
|
pub async fn list_files(url: &str, username: &str, password: &str) -> Result<Vec<String>> {
|
||||||
|
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", "1")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("failed to list WebDAV directory")?;
|
||||||
|
|
||||||
|
if !response.status().is_success() && response.status().as_u16() != 207 {
|
||||||
|
let status = response.status().as_u16();
|
||||||
|
anyhow::bail!("WebDAV PROPFIND failed (HTTP {status})");
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.context("failed to read WebDAV PROPFIND body")?;
|
||||||
|
|
||||||
|
// Simple XML parsing: extract all <d:href> or <D:href> or <href> values
|
||||||
|
// and convert them to filenames.
|
||||||
|
let base = normalize_url(&url);
|
||||||
|
let mut files = Vec::new();
|
||||||
|
// Match various namespace prefixes for href
|
||||||
|
for pattern in ["<D:href>", "<d:href>", "<href>"] {
|
||||||
|
for href in body.split(pattern).skip(1) {
|
||||||
|
if let Some(end) = href.find("</") {
|
||||||
|
let full_path = &href[..end];
|
||||||
|
// Convert URL-encoded path to plain filename
|
||||||
|
if let Some(name) = full_path.strip_prefix(&base) {
|
||||||
|
let name = name.trim_end_matches('/');
|
||||||
|
if !name.is_empty() {
|
||||||
|
let decoded = urlencoding::decode(name)
|
||||||
|
.unwrap_or(std::borrow::Cow::Borrowed(name));
|
||||||
|
files.push(decoded.into_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a file from the WebDAV server.
|
||||||
|
pub async fn delete_file(url: &str, username: &str, password: &str, filename: &str) -> Result<()> {
|
||||||
|
let client = build_client()?;
|
||||||
|
let url = format!("{}{}", normalize_url(url), filename);
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.delete(&url)
|
||||||
|
.basic_auth(username, Some(password))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("failed to delete file from WebDAV server")?;
|
||||||
|
|
||||||
|
if response.status().is_success() || response.status().as_u16() == 204 {
|
||||||
|
Ok(())
|
||||||
|
} else if response.status().as_u16() == 401 {
|
||||||
|
anyhow::bail!("authentication failed: invalid username or password")
|
||||||
|
} else {
|
||||||
|
let status = response.status().as_u16();
|
||||||
|
anyhow::bail!("WebDAV delete failed (HTTP {status})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Build an HTTP client with sensible defaults for WebDAV operations.
|
/// Build an HTTP client with sensible defaults for WebDAV operations.
|
||||||
fn build_client() -> Result<Client> {
|
fn build_client() -> Result<Client> {
|
||||||
Client::builder()
|
Client::builder()
|
||||||
|
|||||||
32
ui/app.slint
32
ui/app.slint
@@ -228,10 +228,11 @@ export component AppWindow inherits Window {
|
|||||||
in-out property <string> webdav-username-text;
|
in-out property <string> webdav-username-text;
|
||||||
in-out property <string> webdav-password-text;
|
in-out property <string> webdav-password-text;
|
||||||
in-out property <string> webdav-status-text;
|
in-out property <string> webdav-status-text;
|
||||||
|
in-out property <string> webdav-auto-sync-interval: "0";
|
||||||
callback webdav-test-connection();
|
callback webdav-test-connection();
|
||||||
callback webdav-push();
|
callback webdav-push();
|
||||||
callback webdav-pull();
|
callback webdav-pull();
|
||||||
callback webdav-save-settings(bool, string, string, string);
|
callback webdav-save-settings(bool, string, string, string, string);
|
||||||
callback app-config-save(string, string, string, int, string, string, string, bool, string);
|
callback app-config-save(string, string, string, int, string, string, string, bool, string);
|
||||||
callback app-config-pick-dir();
|
callback app-config-pick-dir();
|
||||||
callback app-background-pick();
|
callback app-background-pick();
|
||||||
@@ -1387,6 +1388,32 @@ export component AppWindow inherits Window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HorizontalLayout {
|
||||||
|
height: 30px;
|
||||||
|
spacing: 10px;
|
||||||
|
Text {
|
||||||
|
width: 148px;
|
||||||
|
height: parent.height;
|
||||||
|
text: @tr("Auto-sync interval (min)");
|
||||||
|
color: Theme.text-secondary;
|
||||||
|
font-size: Theme.fs-sm;
|
||||||
|
vertical-alignment: center;
|
||||||
|
overflow: elide;
|
||||||
|
}
|
||||||
|
ConfigTextField {
|
||||||
|
value <=> root.webdav-auto-sync-interval;
|
||||||
|
field-width: 80px;
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
height: parent.height;
|
||||||
|
text: @tr("0 = off");
|
||||||
|
color: Theme.text-muted;
|
||||||
|
font-size: Theme.fs-xs;
|
||||||
|
vertical-alignment: center;
|
||||||
|
overflow: elide;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HorizontalLayout {
|
HorizontalLayout {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
spacing: 10px;
|
spacing: 10px;
|
||||||
@@ -1439,7 +1466,8 @@ export component AppWindow inherits Window {
|
|||||||
root.webdav-enabled,
|
root.webdav-enabled,
|
||||||
root.webdav-url-text,
|
root.webdav-url-text,
|
||||||
root.webdav-username-text,
|
root.webdav-username-text,
|
||||||
root.webdav-password-text);
|
root.webdav-password-text,
|
||||||
|
root.webdav-auto-sync-interval);
|
||||||
root.app-config-save(
|
root.app-config-save(
|
||||||
root.max-tabs-text,
|
root.max-tabs-text,
|
||||||
root.terminal-font-size-text,
|
root.terminal-font-size-text,
|
||||||
|
|||||||
Reference in New Issue
Block a user