diff --git a/phone-connect/panel.luau b/phone-connect/panel.luau index bf42beb..8d053b6 100644 --- a/phone-connect/panel.luau +++ b/phone-connect/panel.luau @@ -1,5 +1,5 @@ -- panel.luau --- Phone Connect - details panel (layered info-style UI, 720x600). +-- Phone Connect - details panel (layered info-style UI, 500x600). -- -- Layout: header + device hero + info grid span the top (full width); below -- them a two-column row: left (flex, scrollable) holds media control + SMS @@ -27,6 +27,12 @@ end local smsDestination = "" local smsMessage = "" +-- ── Media slider drag state (freeze value while dragging) ────────────────── +-- slider is controlled: value is reset every render. While the user drags, +-- we freeze value to their position so the 1s service refresh doesn't yank +-- the slider back. Keyed by device id so multi-device switching is clean. +local dragState = {} -- [deviceId] = { dragging = true, position = } + -- ── Helpers ───────────────────────────────────────────────────────────────── local function glyphForType(t) if t == "tablet" then return "device-tablet" end @@ -61,7 +67,7 @@ local function heroCard(d) local reachable = d.isReachable == true local paired = d.isPaired == true local aliasMap = noctalia.state.get("pc.aliasMap") or {} - local name = aliasMap[d.id] or "Your-Phone" + local name = aliasMap[d.id] or d.name or "Your-Phone" local glyph = glyphForType(d.type) local imgMap = noctalia.state.get("pc.imageMap") or {} local imgPath = imgMap[d.id] @@ -92,7 +98,7 @@ local function heroCard(d) }), ui.column({ gap = 4, flexGrow = 1 }, { ui.label({ text = name, fontSize = 17, fontWeight = "bold", color = "on_surface" }), - ui.box({ radius = 10, fill = statusColor .. "/0.15", padding = 4 }, { + ui.row({ radius = 10, fill = statusColor .. "/0.15", padding = 4 }, { ui.label({ text = statusText, fontSize = 11, color = statusColor }), }), }), @@ -228,18 +234,22 @@ local function mediaSection(d) local progress if canSeek and length > 0 then - local seekPos = position + local st = dragState[d.id] + local sliderValue = (st and st.dragging and st.position) or position progress = ui.column({ gap = 2 }, { - ui.slider({ min = 0, max = length, step = 1000, value = position, + ui.slider({ min = 0, max = length, step = 1000, value = sliderValue, controlSize = "sm", onChange = function(v) - seekPos = tonumber(v) or position + dragState[d.id] = { dragging = true, position = tonumber(v) or position } end, onDragEnd = function() - sendCmd({ op = "media_seek", device = d.id, offset = tostring(seekPos) }) + local st2 = dragState[d.id] + local target = (st2 and st2.position) or position + dragState[d.id] = nil + sendCmd({ op = "media_seek", device = d.id, offset = tostring(target) }) end }), ui.row({ justify = "space_between" }, { - ui.label({ text = fmtTime(position), fontSize = 9, color = "on_surface_variant" }), + ui.label({ text = fmtTime(sliderValue), fontSize = 9, color = "on_surface_variant" }), ui.label({ text = fmtTime(length), fontSize = 9, color = "on_surface_variant" }), }), }) @@ -295,6 +305,7 @@ end -- ── Device switcher (bottom, full width, only if >1 device) ───────────────── local function switcher(devices, order, sel) + local aliasMap = noctalia.state.get("pc.aliasMap") or {} if #order <= 1 then return nil end local items = {} for _, id in ipairs(order) do @@ -308,7 +319,7 @@ local function switcher(devices, order, sel) }, { ui.glyph({ name = glyphForType(d.type), size = 14, color = d.isReachable and "primary" or "on_surface_variant" }), - ui.label({ text = aliasMap[id] or "Your-Phone", fontSize = 12, flexGrow = 1, color = "on_surface" }), + ui.label({ text = aliasMap[id] or d.name or "Your-Phone", fontSize = 12, flexGrow = 1, color = "on_surface" }), ui.label({ text = (type(d.batteryCharge) == "number" and d.batteryCharge >= 0) and (tostring(d.batteryCharge) .. "%") or "", fontSize = 10, color = "on_surface_variant" }), diff --git a/phone-connect/plugin.toml b/phone-connect/plugin.toml index c48cd34..1575077 100644 --- a/phone-connect/plugin.toml +++ b/phone-connect/plugin.toml @@ -12,7 +12,7 @@ id = "icefish/phone-connect" name = "Phone Connect" -version = "0.1.0" +version = "0.1.1" plugin_api = 16 author = "icefish" license = "MIT" @@ -66,7 +66,7 @@ description_key = "settings.language.description" default = "en" options = [ { value = "en", label_key = "settings.language.en" }, - { value = "zh_hans", label_key = "settings.language.zh_hans" }, + { value = "zh-Hans", label_key = "settings.language.zh-hans" }, ] [[setting]] @@ -76,36 +76,6 @@ label_key = "settings.enable_clipboard_action.label" description_key = "settings.enable_clipboard_action.description" default = true -[[setting]] -key = "show_ongoing_media" -type = "bool" -label_key = "settings.show_ongoing_media.label" -description_key = "settings.show_ongoing_media.description" -default = true - -[[setting]] -key = "show_device_placeholder" -type = "bool" -label_key = "settings.show_device_placeholder.label" -description_key = "settings.show_device_placeholder.description" -default = true - -[[setting]] -key = "scan_subdirectories" -type = "bool" -label_key = "settings.scan_subdirectories.label" -description_key = "settings.scan_subdirectories.description" -default = false - -[[setting]] -key = "max_recent_images" -type = "int" -label_key = "settings.max_recent_images.label" -description_key = "settings.max_recent_images.description" -default = 4 -min = 1 -max = 12 - [[setting]] key = "panel_placement" type = "select" @@ -130,10 +100,10 @@ entry = "service.luau" id = "bar" entry = "widget.luau" -# Right-click opens the details panel via onRightClick in the script -# (noctalia.togglePanel). We deliberately do not bind [widget.actions] here: -# the panel-toggle action grammar for opening a *plugin* panel by id is not -# documented, so we drive it from the script instead. +# Right-click opens settings via onRightClick in the script +# (noctalia.openSettings). Left-click toggles the details panel. We deliberately +# do not bind [widget.actions] here: the panel-toggle action grammar for opening +# a *plugin* panel by id is not documented, so we drive it from the script instead. [[shortcut]] id = "tile" diff --git a/phone-connect/service.luau b/phone-connect/service.luau index 0d45a42..3588e43 100644 --- a/phone-connect/service.luau +++ b/phone-connect/service.luau @@ -15,6 +15,13 @@ local function t(key) return table[key] or key end +-- Track last-applied config values so onConfigChanged only re-applies +-- custom_image / device_alias when they actually change, not on every +-- unrelated config edit (e.g. language switch). Prevents wiping a +-- device's image/alias that was set via the global setting. +local lastCustomImage = nil +local lastDeviceAlias = nil + -- ── State contract (read by UI entries) ────────────────────────────────────── -- noctalia.state "pc.backend" : { available=bool, name="KDE Connect"|"None", -- announcedName="", selfId="" } @@ -484,7 +491,7 @@ end local function runCli(args, cb) noctalia.runAsync("kdeconnect-cli " .. args, function(res) if res and res.error then - noctalia.notifyError(t("error.no_backend"), res.error) + noctalia.notifyError(t("error.cli_failed"), res.error) end if cb then cb(res) end end) @@ -596,7 +603,7 @@ local function handleCommand(cmd) local first = (res.stdout or ""):match("'([^']+)'") if first then dirPath = first end end - noctalia.runInTerminal("xdg-open " .. shellQuote(dirPath)) + noctalia.runAsync("xdg-open " .. shellQuote(dirPath), function() end) noctalia.notify(t("notify.opening_browser"), dirPath) end) end) @@ -664,7 +671,7 @@ local function handleCommand(cmd) .. " --object-path " .. shellQuote(path) .. " --method " .. shellQuote(PROPS_IFACE .. ".Set") .. " " .. shellQuote(MPRIS_IFACE) .. " volume " - .. "", + .. shellQuote(""), function() refreshDevices() end) else noctalia.log("[phone-connect] unknown cmd op: " .. tostring(op)) @@ -682,7 +689,7 @@ end -- ── Lifecycle ──────────────────────────────────────────────────────────────── local function intervalMs() local secs = tonumber(noctalia.getConfig("state_update_interval")) or 30 - if secs <= 0 then return 60000 end + if secs <= 0 then return 3600000 end -- disabled: large interval to minimize wakeups -- if media is playing, poll faster for live position updates for _, d in pairs(devices) do if d.mediaisPlaying == true then return 1000 end @@ -691,16 +698,19 @@ local function intervalMs() end function update() + local secs = tonumber(noctalia.getConfig("state_update_interval")) or 30 noctalia.setUpdateInterval(intervalMs()) + if secs <= 0 then return end -- disabled: skip auto-refresh detectBackend() refreshDevices() end function onConfigChanged() noctalia.setUpdateInterval(intervalMs()) - -- apply custom_image to selected device + -- apply custom_image to selected device (only when actually changed) local img = noctalia.getConfig("custom_image") - if img ~= nil then + if img ~= nil and img ~= lastCustomImage then + lastCustomImage = img local sel = noctalia.state.get("pc.selected") if sel and sel ~= "" then local map = noctalia.state.get("pc.imageMap") or {} @@ -709,9 +719,10 @@ function onConfigChanged() persistImageMap(map) end end - -- apply device_alias to selected device + -- apply device_alias to selected device (only when actually changed) local alias = noctalia.getConfig("device_alias") - if alias ~= nil then + if alias ~= nil and alias ~= lastDeviceAlias then + lastDeviceAlias = alias local sel = noctalia.state.get("pc.selected") if sel and sel ~= "" then local amap = noctalia.state.get("pc.aliasMap") or {} @@ -744,6 +755,10 @@ function onIpc(event, payload) end -- Top-level init runs once at load. +-- Seed config trackers so the first onConfigChanged after reload doesn't +-- re-apply default custom_image/device_alias and wipe a device's image/alias. +lastCustomImage = noctalia.getConfig("custom_image") or "" +lastDeviceAlias = noctalia.getConfig("device_alias") or "" detectBackend() refreshDevices() -- Command channel: UI entries write { op, device, seq, ... } to "pc.cmd". diff --git a/phone-connect/translations/en.json b/phone-connect/translations/en.json index da68016..8a298db 100644 --- a/phone-connect/translations/en.json +++ b/phone-connect/translations/en.json @@ -1,118 +1,101 @@ { - "action": { - "accept": "Accept", - "browse": "Browse", - "clipboard": "Clipboard", - "pair": "Pair", - "ping": "Ping", - "refresh": "Refresh", - "reject": "Reject", - "ring": "Ring", - "share": "Share", - "sms": "SMS", - "switch": "Switch device", - "unpair": "Unpair" + "settings": { + "state_update_interval": { + "label": "State Update Interval", + "description": "Seconds between automatic device state refreshes. 0 disables auto-refresh." + }, + "enable_charging_animation": { + "label": "Show Charging Fill", + "description": "Highlight the bar widget when the device is charging." + }, + "custom_image": { + "label": "Device Image", + "description": "Set a custom image for the selected device. Leave empty for default icon." + }, + "device_alias": { + "label": "Device Alias", + "description": "Custom display name for the selected device." + }, + "language": { + "label": "Language", + "description": "Display language", + "en": "English", + "zh-hans": "简体中文" + }, + "enable_clipboard_action": { + "label": "Show Clipboard Action", + "description": "Show a quick action to send the clipboard to the device." + }, + "panel_placement": { + "label": "Panel Placement", + "description": "How the details panel opens: attached to the bar, or floating.", + "attached": "Attached to bar", + "floating": "Floating (center)", + "widget": "Below widget (floating)" + } }, - "error": { - "accept_failed": "Failed to accept pairing", - "browse_failed": "Failed to browse device", - "clipboard_failed": "Failed to send clipboard", - "no_backend": "No backend", - "pairing_failed": "Pairing failed", - "ping_failed": "Failed to send ping", - "reject_failed": "Failed to reject pairing", - "ring_failed": "Failed to ring device", - "share_failed": "Failed to share", - "unpair_failed": "Unpair failed" + "status": { + "unavailable": "Unavailable", + "no_devices": "No devices", + "offline": "Offline", + "connected": "Connected", + "not_paired": "Not paired", + "no_backend": "KDE Connect not running" + }, + "action": { + "ring": "Ring", + "ping": "Ping", + "clipboard": "Clipboard", + "share": "Share", + "browse": "Browse", + "sms": "SMS", + "pair": "Pair", + "accept": "Accept", + "reject": "Reject", + "unpair": "Unpair", + "refresh": "Refresh", + "switch": "Switch device" }, "notify": { - "clipboard_sent": "Clipboard sent", - "opening_browser": "Opening file browser...", - "paired": "Device paired", - "pairing_sent": "Pairing request sent", - "ping_sent": "Ping sent to {name}", "ringing": "Ringing {name}...", - "sms_sent": "SMS sent", - "unpaired": "Device unpaired" + "ping_sent": "Ping sent to {name}", + "clipboard_sent": "Clipboard sent", + "pairing_sent": "Pairing request sent", + "paired": "Device paired", + "unpaired": "Device unpaired", + "opening_browser": "Opening file browser...", + "sms_sent": "SMS sent" + }, + "error": { + "ring_failed": "Failed to ring device", + "ping_failed": "Failed to send ping", + "clipboard_failed": "Failed to send clipboard", + "share_failed": "Failed to share", + "browse_failed": "Failed to browse device", + "pairing_failed": "Pairing failed", + "accept_failed": "Failed to accept pairing", + "reject_failed": "Failed to reject pairing", + "unpair_failed": "Unpair failed", + "no_backend": "No backend", + "cli_failed": "Command failed" }, "panel": { - "battery": "Battery", - "devices_connected": "{connected} connected • {paired} paired", + "title": "Phone Connect", "empty": "No devices found. Pair a device in KDE Connect.", - "image_path": "Custom Image", - "network": "Network", - "no_media": "No media playing", - "no_recent_images": "No recent images found", + "devices_connected": "{connected} connected • {paired} paired", "now_playing": "Now Playing", - "paired": "Paired", - "pairing": "Pairing", - "recent_images": "Recent Images", + "no_media": "No media playing", + "image_path": "Custom Image", "sms": "Send SMS", "sms_dest": "Phone number", "sms_msg": "Message", "sms_send": "Send", - "title": "Phone Connect", + "battery": "Battery", + "network": "Network", + "pairing": "Pairing", "type": "Type", - "unknown": "unknown", - "unpaired": "Unpaired" - }, - "settings": { - "custom_image": { - "description": "Set a custom image for the selected device. Leave empty for default icon.", - "label": "Device Image" - }, - "device_alias": { - "description": "Custom display name for the selected device.", - "label": "Device Alias" - }, - "enable_charging_animation": { - "description": "Highlight the bar widget when the device is charging.", - "label": "Show Charging Fill" - }, - "enable_clipboard_action": { - "description": "Show a quick action to send the clipboard to the device.", - "label": "Show Clipboard Action" - }, - "language": { - "description": "Display language", - "en": "English", - "label": "Language", - "zh_hans": "简体中文" - }, - "max_recent_images": { - "description": "Number of recent images to display.", - "label": "Max Recent Images" - }, - "panel_placement": { - "attached": "Attached to bar", - "description": "How the details panel opens: attached to the bar, or floating.", - "floating": "Floating (center)", - "label": "Panel Placement", - "widget": "Below widget (floating)" - }, - "scan_subdirectories": { - "description": "Recursively scan subdirectories of the recent images path.", - "label": "Scan Subdirectories" - }, - "show_device_placeholder": { - "description": "Show the device graphic in the details panel.", - "label": "Show Device Placeholder" - }, - "show_ongoing_media": { - "description": "Show media currently playing on the phone.", - "label": "Show Ongoing Media" - }, - "state_update_interval": { - "description": "Seconds between automatic device state refreshes. 0 disables auto-refresh.", - "label": "State Update Interval" - } - }, - "status": { - "connected": "Connected", - "no_backend": "KDE Connect not running", - "no_devices": "No devices", - "not_paired": "Not paired", - "offline": "Offline", - "unavailable": "Unavailable" + "paired": "Paired", + "unpaired": "Unpaired", + "unknown": "unknown" } } diff --git a/phone-connect/translations/zh-Hans.json b/phone-connect/translations/zh-Hans.json new file mode 100644 index 0000000..21a638b --- /dev/null +++ b/phone-connect/translations/zh-Hans.json @@ -0,0 +1,101 @@ +{ + "settings": { + "state_update_interval": { + "label": "状态刷新间隔", + "description": "自动刷新设备状态的间隔秒数。0 表示禁用。" + }, + "enable_charging_animation": { + "label": "充电高亮", + "description": "设备充电时高亮显示栏部件。" + }, + "custom_image": { + "label": "设备头像", + "description": "选中设备后在此设置自定义头像图片。留空恢复默认图标。" + }, + "device_alias": { + "label": "设备别名", + "description": "当前选中设备的自定义显示名称。" + }, + "language": { + "label": "界面语言", + "description": "显示语言。", + "en": "English", + "zh-hans": "简体中文" + }, + "enable_clipboard_action": { + "label": "显示剪贴板操作", + "description": "显示一键发送剪贴板到设备的快捷操作。" + }, + "panel_placement": { + "label": "面板弹出方式", + "description": "详情面板的弹出方式。", + "attached": "贴合栏", + "floating": "悬浮居中", + "widget": "部件下方悬浮" + } + }, + "status": { + "unavailable": "不可用", + "no_devices": "无设备", + "offline": "离线", + "connected": "已连接", + "not_paired": "未配对", + "no_backend": "KDE Connect 未运行" + }, + "action": { + "ring": "响铃", + "ping": "Ping", + "clipboard": "剪贴板", + "share": "分享", + "browse": "浏览", + "sms": "短信", + "pair": "配对", + "accept": "接受", + "reject": "拒绝", + "unpair": "取消配对", + "refresh": "刷新", + "switch": "切换设备" + }, + "notify": { + "ringing": "正在响铃 {name}...", + "ping_sent": "已向 {name} 发送 Ping", + "clipboard_sent": "剪贴板已发送", + "pairing_sent": "已发送配对请求", + "paired": "设备已配对", + "unpaired": "设备已取消配对", + "opening_browser": "正在打开文件浏览器...", + "sms_sent": "短信已发送" + }, + "error": { + "ring_failed": "响铃失败", + "ping_failed": "发送 Ping 失败", + "clipboard_failed": "发送剪贴板失败", + "share_failed": "分享失败", + "browse_failed": "浏览设备失败", + "pairing_failed": "配对失败", + "accept_failed": "接受配对失败", + "reject_failed": "拒绝配对失败", + "unpair_failed": "取消配对失败", + "no_backend": "无后端", + "cli_failed": "命令失败" + }, + "panel": { + "title": "手机连接", + "empty": "未找到设备。请在 KDE Connect 中配对设备。", + "devices_connected": "已连接 {connected} · 已配对 {paired}", + "now_playing": "正在播放", + "no_media": "无媒体播放", + "image_path": "自定义图片", + "sms": "发送短信", + "sms_dest": "手机号码", + "sms_msg": "短信内容", + "sms_send": "发送", + "battery": "电量", + "network": "网络", + "pairing": "配对", + "type": "类型", + "paired": "已配对", + "unpaired": "未配对", + "unknown": "未知" + } +} diff --git a/phone-connect/widget.luau b/phone-connect/widget.luau index bbabf92..90aff6c 100644 --- a/phone-connect/widget.luau +++ b/phone-connect/widget.luau @@ -91,8 +91,8 @@ local function render() if reachable and hasCharge then -- battery glyph + percent, colored by level local batColor = "on_surface" - if charge <= 20 then batColor = "error" - elseif charging then batColor = "primary" end + if charging then batColor = "primary" + elseif charge <= 20 then batColor = "error" end table.insert(kids, ui.glyph({ name = batteryGlyph(charge, charging), size = 12, color = batColor })) table.insert(kids, ui.label({