removed qml tray app

This commit is contained in:
Lasan Mahaliyana
2026-06-19 20:09:23 +05:30
parent 8b456eeaf8
commit b970cf4826
40 changed files with 0 additions and 3575 deletions
-60
View File
@@ -1,60 +0,0 @@
cmake_minimum_required(VERSION 3.21)
project(nearby_qml_tray_app LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Qml Quick QuickControls2 DBus)
# Where the Bazel-built Nearby .so and header were installed
set(NEARBY_PREFIX "/usr/local" CACHE PATH "Install prefix for the Nearby shared library")
find_library(NEARBY_SHARING_LIB nearby_sharing_api_shared
HINTS "${NEARBY_PREFIX}/lib" REQUIRED)
find_path(NEARBY_SHARING_INCLUDE sharing/linux/nearby_sharing_api.h
HINTS "${NEARBY_PREFIX}/include" REQUIRED)
find_library(QRENCODE_LIB
NAMES qrencode libqrencode.so.4
HINTS /usr/lib64 /usr/lib /usr/local/lib
REQUIRED)
qt_add_executable(nearby_qml_file_tray_app
file_share_tray_main.cpp
file_share_tray_controller.cc
file_share_tray_controller.h
file_share_state.cc
file_share_state.h
share_target_model.h
transfer_model.h
string_utils.cc
string_utils.h
status_mapper.cc
status_mapper.h
qr_code_generator.cc
qr_code_generator.h
notification_manager.cpp
notification_manager.h
third_party/libqrencode/qrencode_compat.h
resources_file_share.qrc
)
target_include_directories(nearby_qml_file_tray_app PRIVATE "${NEARBY_SHARING_INCLUDE}")
target_link_libraries(nearby_qml_file_tray_app PRIVATE
Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Qml Qt6::Quick Qt6::QuickControls2 Qt6::DBus
"${QRENCODE_LIB}"
"${NEARBY_SHARING_LIB}"
)
set_target_properties(nearby_qml_file_tray_app PROPERTIES
INSTALL_RPATH "$ORIGIN/../lib"
)
include(GNUInstallDirs)
install(TARGETS nearby_qml_file_tray_app RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
install(FILES "${NEARBY_SHARING_LIB}" DESTINATION "${CMAKE_INSTALL_LIBDIR}")
@@ -1,110 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "components"
ApplicationWindow {
id: root
width: 980
height: 760
minimumWidth: 820
minimumHeight: 620
visible: true
title: "Quick Share"
background: Rectangle { color: "#f0fdf4" }
onClosing: function(close) {
close.accepted = false
root.hide()
fileShareController.hideToTray()
}
SettingsPanel {
id: settingsPanel
}
ColumnLayout {
anchors.fill: parent
spacing: 0
AppHeader {
onSettingsRequested: settingsPanel.open()
}
// ── Body ─────────────────────────────────────────────────────────
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
SideBar {}
// ── Main content (white panel) ────────────────────────────────
Rectangle {
id: mainContent
Layout.fillWidth: true
Layout.fillHeight: true
color: "#ffffff"
topLeftRadius: 48
clip: true
readonly property bool isSendMode: fileShareController.pendingSendFilePath.length > 0
// ── Idle: animated blob ───────────────────────────────────
AnimatedBlob { visible: !mainContent.isSendMode }
// ── Non-idle: scrollable device + transfer cards ──────────
Flickable {
id: mainFlickable
anchors.fill: parent
clip: true
visible: mainContent.isSendMode
contentWidth: width
contentHeight: mainCol.implicitHeight + 96
ScrollBar.vertical: ScrollBar {}
ColumnLayout {
id: mainCol
x: 48
y: 48
width: mainFlickable.width - 96
spacing: 16
SendUrlPanel {
Layout.alignment: Qt.AlignHCenter
width: Math.max(240, Math.min(mainCol.width, 420))
}
Label {
text: "Nearby devices"
font.pixelSize: 20
font.weight: Font.Medium
color: "#111827"
}
Item {
Layout.fillWidth: true
implicitHeight: deviceFlow.childrenRect.height
visible: fileShareController.discoveredTargets.length > 0
Flow {
id: deviceFlow
width: parent.width
spacing: 20
Repeater {
model: fileShareController.discoveredTargets
delegate: DeviceCard {}
}
}
}
}
}
}
}
}
}
-95
View File
@@ -1,95 +0,0 @@
# Nearby File Share Tray App
This folder contains the Qt/QML **FileShareTray** application — a system tray
app for file sharing via Nearby Sharing, wired to:
- `nearby::sharing::linux::NearbySharingApi`
- Send mode (discover nearby share targets + send file)
- Receive mode (incoming requests + accept/reject)
- Transfer status list (progress + transfer status)
- Persistent tray behavior (window close hides app to tray)
- Process log redirection to file (`stdout`/`stderr`)
## Files
- `file_share_tray_main.cpp`: Qt app bootstrap + system tray behavior.
- `file_share_tray_controller.h/.cc`: QML-facing backend wrapper around Nearby Sharing.
- `FileShareTray.qml`: Top-level UI for the file share tray app.
- `components/`: Shared QML UI components used by `FileShareTray.qml`.
- `resources_file_share.qrc`: Embeds `FileShareTray.qml` and components.
## Runtime behavior
- Close button does **not** terminate the process; it hides to tray.
- Use tray icon menu to show/hide/quit.
- Mode `Send`:
- Starts discovery.
- Shows discovered share targets.
- Sends the selected file to a chosen target.
- Mode `Receive`:
- Starts advertising.
- Shows pending incoming transfer requests.
- Lets you accept/reject incoming requests.
- Transfers are shown with target, direction, status, and progress.
- `stdout` and `stderr` are redirected at startup to the configured `logPath`
setting.
- Default log path is `/tmp/nearby_qml_file_tray.log` when `logPath` is unset.
- If `logPath` is changed from Settings, restart the app to apply redirection.
## Building
This CMake app links against the installed Nearby shared library and header:
- `libnearby_sharing_api_shared.so`
- `sharing/linux/nearby_sharing_api.h`
Install them first (repo root):
```bash
./sharing/linux/install_nearby_sharing_service.sh
```
Then build the app (from `sharing/linux/qml_tray_app`):
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DNEARBY_PREFIX=/usr/local
cmake --build build -j
```
## Bundle `libnearby_sharing_api_shared.so` with the app
From `sharing/linux/qml_tray_app`:
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$PWD/dist"
cmake --build build -j
cmake --install build
```
Bundle output:
- `dist/bin/nearby_qml_file_tray_app`
- `dist/bin/libnearby_sharing_api_shared.so`
The app is installed with `INSTALL_RPATH=$ORIGIN`, so it resolves the Nearby
shared library from the same folder in the bundle.
## Build a distributable `.zip` (includes runtime dependencies)
From `sharing/linux/qml_tray_app`:
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
cpack --config build/CPackConfig.cmake -G ZIP
```
Output:
- `build/nearby_qml_tray_app-Linux-x86_64.zip`
This zip is created from the CMake install tree and includes:
- `nearby_qml_file_tray_app`
- `libnearby_sharing_api_shared.so`
- Qt runtime libs/plugins/QML imports discovered by Qt deploy tooling
@@ -1,80 +0,0 @@
set(_output_so "${OUTPUT_SO}")
set(_bazel_executable "${BAZEL_EXECUTABLE}")
set(_bazel_target "${BAZEL_TARGET}")
set(_bazel_build_options "${BAZEL_BUILD_OPTIONS}")
set(_repo_root "${REPO_ROOT}")
set(_rebuild_inputs "${REBUILD_INPUTS}")
# Values passed via -D can arrive wrapped in literal quotes when emitted from
# a custom command. Strip one outer quote pair if present.
foreach(_var IN ITEMS _output_so _bazel_executable _bazel_target _bazel_build_options _repo_root)
string(REGEX REPLACE "^\"(.*)\"$" "\\1" ${_var} "${${_var}}")
endforeach()
set(_needs_rebuild TRUE)
if(EXISTS "${_output_so}")
# Rebuild if any tracked input file is newer than the output.
set(_needs_rebuild FALSE)
foreach(_input IN LISTS _rebuild_inputs)
if(EXISTS "${_input}")
if("${_input}" IS_NEWER_THAN "${_output_so}")
set(_needs_rebuild TRUE)
message(STATUS "Input changed since last Bazel build: ${_input}")
break()
endif()
endif()
endforeach()
if(NOT _needs_rebuild)
# Reuse an existing .so when it already exports the NearbySharingApi symbols.
# This avoids stale-cache link failures after facade changes.
find_program(_nm_program nm)
if(_nm_program)
execute_process(
COMMAND "${_nm_program}" -D -C "${_output_so}"
RESULT_VARIABLE _nm_result
OUTPUT_VARIABLE _nm_output
ERROR_QUIET
)
if(_nm_result EQUAL 0)
string(FIND "${_nm_output}" "nearby::sharing::NearbySharingApi::NearbySharingApi()" _api_ctor_idx)
string(FIND "${_nm_output}" " U nearby::api::ImplementationPlatform::CreateScheduledExecutor()" _undef_platform_idx)
string(FIND "${_nm_output}" " U nearby::SystemClock::ElapsedRealtime()" _undef_clock_idx)
string(FIND "${_nm_output}" " U nearby::Crypto::Sha256(" _undef_crypto_idx)
if(NOT _api_ctor_idx EQUAL -1
AND _undef_platform_idx EQUAL -1
AND _undef_clock_idx EQUAL -1
AND _undef_crypto_idx EQUAL -1)
set(_needs_rebuild FALSE)
else()
set(_needs_rebuild TRUE)
endif()
else()
set(_needs_rebuild TRUE)
endif()
endif()
endif()
if(NOT _needs_rebuild)
message(STATUS "Using existing Bazel library: ${_output_so}")
return()
endif()
message(STATUS "Existing Bazel library is stale/incompatible, rebuilding: ${_output_so}")
endif()
separate_arguments(_bazel_build_options_list NATIVE_COMMAND "${_bazel_build_options}")
message(STATUS "Bazel library not found, building ${_bazel_target}")
execute_process(
COMMAND "${_bazel_executable}" build ${_bazel_build_options_list} "${_bazel_target}"
WORKING_DIRECTORY "${_repo_root}"
RESULT_VARIABLE BAZEL_BUILD_RESULT
)
if(NOT BAZEL_BUILD_RESULT EQUAL 0)
message(FATAL_ERROR "Bazel build failed for ${_bazel_target} (exit ${BAZEL_BUILD_RESULT})")
endif()
if(NOT EXISTS "${_output_so}")
message(FATAL_ERROR "Bazel build completed but expected output is missing: ${_output_so}")
endif()
@@ -1,41 +0,0 @@
set(_output_static_lib "${OUTPUT_STATIC_LIB}")
set(_output_linkopts_file "${OUTPUT_LINKOPTS_FILE}")
set(_bazel_executable "${BAZEL_EXECUTABLE}")
set(_bazel_target "${BAZEL_TARGET}")
set(_bazel_build_options "${BAZEL_BUILD_OPTIONS}")
set(_bazel_env_cc "${BAZEL_ENV_CC}")
set(_bazel_env_cxx "${BAZEL_ENV_CXX}")
set(_repo_root "${REPO_ROOT}")
foreach(_var IN ITEMS _output_static_lib _output_linkopts_file _bazel_executable _bazel_target _bazel_build_options _bazel_env_cc _bazel_env_cxx _repo_root)
string(REGEX REPLACE "^\"(.*)\"$" "\\1" ${_var} "${${_var}}")
endforeach()
if(EXISTS "${_output_static_lib}" AND EXISTS "${_output_linkopts_file}")
message(STATUS "Using existing Bazel static archive: ${_output_static_lib}")
return()
endif()
separate_arguments(_bazel_build_options_list NATIVE_COMMAND "${_bazel_build_options}")
message(STATUS "Building Bazel static archive ${_bazel_target}")
execute_process(
COMMAND "${CMAKE_COMMAND}" -E env
"CC=${_bazel_env_cc}"
"CXX=${_bazel_env_cxx}"
"${_bazel_executable}" build ${_bazel_build_options_list} "${_bazel_target}"
WORKING_DIRECTORY "${_repo_root}"
RESULT_VARIABLE _bazel_build_result
)
if(NOT _bazel_build_result EQUAL 0)
message(FATAL_ERROR "Bazel build failed for ${_bazel_target} (exit ${_bazel_build_result})")
endif()
if(NOT EXISTS "${_output_static_lib}")
message(FATAL_ERROR "Expected static archive is missing: ${_output_static_lib}")
endif()
if(NOT EXISTS "${_output_linkopts_file}")
message(FATAL_ERROR "Expected Bazel link options file is missing: ${_output_linkopts_file}")
endif()
@@ -1,416 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
Item {
anchors.fill: parent
readonly property color textPrimary: "#111827"
readonly property color textMuted: "#6b7280"
readonly property color textSoft: "#4b5563"
readonly property color cardSurface: "#ffffff"
readonly property color cardBorder: "#d1fae5"
readonly property bool isSendMode: fileShareController.pendingSendFilePath.length > 0
readonly property var incomingTransfer: findIncomingTransfer()
readonly property var incomingTarget: findTargetForTransfer(incomingTransfer)
readonly property bool hasIncomingTransfer: incomingTransfer !== null
&& incomingTarget !== null
readonly property bool isReceivingActive: hasIncomingTransfer
&& String(incomingTransfer.status || "") !== "Complete"
readonly property real receivingIntensity: {
if (!isReceivingActive)
return 0
var numeric = Number(incomingTransfer.progress)
if (!isFinite(numeric) || numeric < 0)
numeric = 0
return Math.max(0.35, Math.min(1.0, numeric))
}
property real blobChaos: receivingIntensity
property string dismissedTransferKey: ""
Behavior on blobChaos {
NumberAnimation {
duration: 220
easing.type: Easing.OutCubic
}
}
function isIncomingTransferActive(status) {
return status === "InProgress"
|| status === "Queued"
|| status === "Connecting"
|| status === "AwaitingLocalConfirmation"
|| status === "AwaitingRemoteAcceptance"
|| status === "Complete"
}
function transferKey(transfer) {
if (!transfer)
return ""
return String(transfer.targetId || "")
+ "|" + String(transfer.status || "")
+ "|" + String(transfer.filePath || "")
+ "|" + String(transfer.fileName || "")
}
function findIncomingTransfer() {
var transfers = fileShareController.transfers
var latestCompleted = null
for (var i = transfers.length - 1; i >= 0; --i) {
var entry = transfers[i]
if (!entry)
continue
if (String(entry.direction || "") !== "incoming")
continue
var status = String(entry.status || "")
if (!isIncomingTransferActive(status))
continue
if (status === "Complete") {
if (latestCompleted === null)
latestCompleted = entry
continue
}
return entry
}
return latestCompleted
}
function findTargetForTransfer(transfer) {
if (!transfer)
return null
var targets = fileShareController.discoveredTargets
for (var i = 0; i < targets.length; ++i) {
var entry = targets[i]
if (entry && entry.id === transfer.targetId)
return entry
}
return {
id: transfer.targetId,
name: String(transfer.targetName || "Incoming device"),
isIncoming: true
}
}
function incomingHeadline(status) {
if (status === "Complete")
return "Received"
if (status === "AwaitingLocalConfirmation")
return "Incoming transfer"
if (status === "AwaitingRemoteAcceptance")
return "Preparing transfer"
if (status === "Connecting")
return "Connecting"
return "Receiving"
}
// The card only becomes actionable once the file has landed on disk and we
// have a local path to open.
function incomingClickReady() {
return hasIncomingTransfer
&& String(incomingTransfer.status || "") === "Complete"
&& String(incomingTransfer.filePath || "").length > 0
}
Label {
x: 48; y: 48
visible: fileShareController.running
text: isSendMode
? "Ready to send"
: "Ready to receive"
font.pixelSize: 20
font.weight: Font.Medium
color: textPrimary
}
Canvas {
id: blobCanvas3
width: 380; height: 380
anchors.centerIn: parent
visible: !isSendMode
property real t: 0
property double lastMs: Date.now()
Timer {
interval: 16
running: true
repeat: true
onTriggered: {
var now = Date.now()
var dt = Math.min(0.05, Math.max(0.0, (now - blobCanvas3.lastMs) * 0.001))
blobCanvas3.lastMs = now
blobCanvas3.t += dt * (Math.PI * 2 / Math.max(4.6, 8.0 - blobChaos * 2.6))
blobCanvas3.requestPaint()
}
}
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
var chaos = blobChaos
var cx = width / 2 + Math.sin(t * 0.33) * chaos * 10
var cy = height / 2 + Math.cos(t * 0.27) * chaos * 8
var n = 10
var pts = []
for (var i = 0; i < n; i++) {
var a = (i / n) * Math.PI * 2 - Math.PI / 2
var r = 150 + chaos * 22
+ Math.sin(a * 2 + t) * (11 + chaos * 14)
+ Math.cos(a * 3 - t * 0.2) * (8 + chaos * 10)
+ Math.sin(a * 1.5 + t * 0.7) * (6 + chaos * 8)
+ Math.sin(a * 1.5 + t) * (4 + chaos * 6)
+ Math.cos(a * 5 - t * 1.2) * (chaos * 9)
+ Math.sin(a * 7 + t * 0.85) * (chaos * 6)
pts.push({ x: cx + Math.cos(a) * r, y: cy + Math.sin(a) * r })
}
var len = pts.length
ctx.beginPath()
for (var j = 0; j < len; j++) {
var p0 = pts[(j - 1 + len) % len]
var p1 = pts[j]
var p2 = pts[(j + 1) % len]
var p3 = pts[(j + 2) % len]
var cp1x = p1.x + (p2.x - p0.x) / 6
var cp1y = p1.y + (p2.y - p0.y) / 6
var cp2x = p2.x - (p3.x - p1.x) / 6
var cp2y = p2.y - (p3.y - p1.y) / 6
if (j === 0) ctx.moveTo(p1.x, p1.y)
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y)
}
ctx.closePath()
var grad = ctx.createRadialGradient(cx - 40, cy - 40, 0, cx, cy, 150)
grad.addColorStop(0, "#e7faed")
ctx.fillStyle = grad
ctx.fill()
}
}
Canvas {
id: blobCanvas2
width: 380; height: 380
anchors.centerIn: parent
visible: !isSendMode
property real t: 0
property double lastMs: Date.now()
Timer {
interval: 16
running: true
repeat: true
onTriggered: {
var now = Date.now()
var dt = Math.min(0.05, Math.max(0.0, (now - blobCanvas2.lastMs) * 0.001))
blobCanvas2.lastMs = now
blobCanvas2.t += dt * (Math.PI * 2 / Math.max(4.9, 8.0 - blobChaos * 2.2))
blobCanvas2.requestPaint()
}
}
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
var chaos = blobChaos
var cx = width / 2 + Math.cos(t * 0.29) * chaos * 8
var cy = height / 2 + Math.sin(t * 0.41) * chaos * 11
var n = 10
var pts = []
for (var i = 0; i < n; i++) {
var a = (i / n) * Math.PI * 2 - Math.PI / 2
var r = 140 + chaos * 18
+ Math.sin(a * 2 + t) * (11 + chaos * 12)
+ Math.cos(a * 3 - t * 0.8) * (8 + chaos * 9)
+ Math.sin(a * 1.5 + t * 0.23) * (6 + chaos * 7)
+ Math.sin(a * 1.5 + t * 0.85) * (4 + chaos * 6)
+ Math.cos(a * 4 + t * 1.1) * (chaos * 7)
+ Math.sin(a * 6 - t * 0.95) * (chaos * 5)
pts.push({ x: cx + Math.cos(a) * r, y: cy + Math.sin(a) * r })
}
var len = pts.length
ctx.beginPath()
for (var j = 0; j < len; j++) {
var p0 = pts[(j - 1 + len) % len]
var p1 = pts[j]
var p2 = pts[(j + 1) % len]
var p3 = pts[(j + 2) % len]
var cp1x = p1.x + (p2.x - p0.x) / 6
var cp1y = p1.y + (p2.y - p0.y) / 6
var cp2x = p2.x - (p3.x - p1.x) / 6
var cp2y = p2.y - (p3.y - p1.y) / 6
if (j === 0) ctx.moveTo(p1.x, p1.y)
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y)
}
ctx.closePath()
var grad = ctx.createRadialGradient(cx - 40, cy - 40, 0, cx, cy, 150)
grad.addColorStop(0, "#caeada")
ctx.fillStyle = grad
ctx.fill()
}
}
Canvas {
id: blobCanvas
width: 380; height: 380
anchors.centerIn: parent
visible: !isSendMode
property real t: 0
property double lastMs: Date.now()
Timer {
interval: 16
running: true
repeat: true
onTriggered: {
var now = Date.now()
var dt = Math.min(0.05, Math.max(0.0, (now - blobCanvas.lastMs) * 0.001))
blobCanvas.lastMs = now
blobCanvas.t += dt * (Math.PI * 2 / Math.max(4.4, 8.0 - blobChaos * 3.0))
blobCanvas.requestPaint()
}
}
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
var chaos = blobChaos
var cx = width / 2 + Math.sin(t * 0.55) * chaos * 12
var cy = height / 2 + Math.cos(t * 0.47) * chaos * 10
var n = 10
var pts = []
for (var i = 0; i < n; i++) {
var a = (i / n) * Math.PI * 2 - Math.PI / 2
var r = 130 + chaos * 15
+ Math.sin(a * 2 + t) * (11 + chaos * 15)
+ Math.cos(a * 3 - t * 0.6) * (8 + chaos * 12)
+ Math.sin(a * 1.5 + t * 0.35) * (6 + chaos * 9)
+ Math.sin(a + t * 0.75) * (3 + chaos * 6)
+ Math.cos(a * 5 - t * 1.35) * (chaos * 10)
+ Math.sin(a * 8 + t * 0.92) * (chaos * 6)
pts.push({ x: cx + Math.cos(a) * r, y: cy + Math.sin(a) * r })
}
var len = pts.length
ctx.beginPath()
for (var j = 0; j < len; j++) {
var p0 = pts[(j - 1 + len) % len]
var p1 = pts[j]
var p2 = pts[(j + 1) % len]
var p3 = pts[(j + 2) % len]
var cp1x = p1.x + (p2.x - p0.x) / 6
var cp1y = p1.y + (p2.y - p0.y) / 6
var cp2x = p2.x - (p3.x - p1.x) / 6
var cp2y = p2.y - (p3.y - p1.y) / 6
if (j === 0) ctx.moveTo(p1.x, p1.y)
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y)
}
ctx.closePath()
var grad = ctx.createRadialGradient(cx - 40, cy - 40, 0, cx, cy, 150)
grad.addColorStop(0, "#acdac4")
ctx.fillStyle = grad
ctx.fill()
}
}
Rectangle {
id: incomingTransferCard
anchors.centerIn: parent
visible: !isSendMode && hasIncomingTransfer
&& transferKey(incomingTransfer) !== dismissedTransferKey
width: 200
height: 210
radius: 34
color: cardSurface
border.color: cardBorder
border.width: 1
z: 10
Rectangle {
anchors.fill: parent
anchors.margins: 10
radius: parent.radius - 10
color: "#ffffff"
opacity: 0.84
}
Column {
anchors.fill: parent
anchors.margins: 24
spacing: 10
Label {
anchors.horizontalCenter: parent.horizontalCenter
text: incomingHeadline(String(incomingTransfer.status || ""))
font.pixelSize: 12
font.weight: Font.DemiBold
color: "#059669"
horizontalAlignment: Text.AlignHCenter
}
DeviceCard {
anchors.horizontalCenter: parent.horizontalCenter
modelData: incomingTarget
}
Label {
width: parent.width
visible: String(incomingTransfer.fileName || "").length > 0
text: String(incomingTransfer.fileName || "")
font.pixelSize: 13
font.weight: Font.StyleItalic
color: textPrimary
wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
}
MouseArea {
anchors.fill: parent
enabled: incomingClickReady()
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
dismissedTransferKey = transferKey(incomingTransfer)
fileShareController.openFileLocation(String(incomingTransfer.filePath || ""))
}
}
}
onIncomingTransferChanged: {
if (!incomingTransfer) {
dismissedTransferKey = ""
return
}
if (transferKey(incomingTransfer) !== dismissedTransferKey)
return
if (String(incomingTransfer.status || "") !== "Complete")
dismissedTransferKey = ""
}
Label {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 48
visible: !isSendMode
text: fileShareController.statusMessage
font.pixelSize: 13
color: textMuted
}
}
@@ -1,62 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
Layout.fillWidth: true
height: 80
signal settingsRequested()
readonly property color textPrimary: "#111827"
readonly property color textMuted: "#6b7280"
readonly property color accent: "#16a34a"
RowLayout {
anchors.fill: parent
anchors.leftMargin: 24
anchors.rightMargin: 24
ColumnLayout {
spacing: 2
Label {
text: "Device name"
font.pixelSize: 12
color: textMuted
}
Label {
text: fileShareController.deviceName
font.pixelSize: 22
font.weight: Font.Medium
color: textPrimary
}
}
Item { Layout.fillWidth: true }
Rectangle {
width: 40
height: 40
radius: 12
color: settingsBtn.containsMouse ? "#dcfce7" : "transparent"
border.color: settingsBtn.containsMouse ? "#86efac" : "transparent"
Label {
anchors.centerIn: parent
text: "⚙"
font.pixelSize: 18
color: settingsBtn.containsMouse ? accent : textMuted
}
MouseArea {
id: settingsBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: settingsRequested()
}
}
}
}
@@ -1,224 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
required property var modelData
width: 116
height: deviceColumn.implicitHeight
readonly property color surface: "#ffffff"
readonly property color textPrimary: "#111827"
readonly property color textMuted: "#6b7280"
readonly property color avatarFill: "#dcfce7"
readonly property color avatarBorder: "#bbf7d0"
readonly property color ringBase: "#d1fae5"
readonly property color ringActive: "#10b981"
readonly property color ringComplete: "#16a34a"
readonly property color ringFailed: "#ef4444"
readonly property bool canSend: fileShareController.mode === "Send"
&& fileShareController.pendingSendFilePath.length > 0
readonly property string targetName: modelData.name && modelData.name.length > 0
? modelData.name : "Unknown device"
readonly property var transferData: transferForTarget()
readonly property string transferStatus: transferData ? String(transferData.status || "") : ""
readonly property bool hasTransfer: transferData !== null
readonly property bool isTransferActive: transferStatus === "InProgress"
|| transferStatus === "Queued"
|| transferStatus === "Connecting"
|| transferStatus === "AwaitingLocalConfirmation"
|| transferStatus === "AwaitingRemoteAcceptance"
readonly property bool isTransferComplete: transferStatus === "Complete"
readonly property bool isTransferFailed: hasTransfer && !isTransferActive && !isTransferComplete
readonly property bool isConnecting: transferStatus === "Connecting"
property bool showCompletionTick: false
property string previousTransferStatus: ""
readonly property real transferProgress: {
if (!hasTransfer)
return 0
if (isTransferComplete || isTransferFailed)
return 1
var numeric = Number(transferData.progress)
if (!isFinite(numeric) || numeric < 0)
numeric = 0
if (isTransferActive && numeric === 0)
return 0.08
return Math.max(0, Math.min(1, numeric))
}
readonly property color ringColor: isTransferComplete ? ringComplete
: isTransferFailed ? ringFailed
: ringActive
function initialLetter(label) {
if (!label || label.length === 0) return "?"
return label.charAt(0).toUpperCase()
}
function transferForTarget() {
var transfers = fileShareController.transfers
for (var i = 0; i < transfers.length; ++i) {
var entry = transfers[i]
if (entry && entry.targetId === modelData.id)
return entry
}
return null
}
Column {
id: deviceColumn
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
spacing: 10
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
width: 84
height: 84
radius: 42
color: surface
opacity: canSend ? 1.0 : 0.5
Canvas {
id: progressRing
anchors.fill: parent
antialiasing: true
transformOrigin: Item.Center
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
var lineWidth = 5
var radius = (Math.min(width, height) - lineWidth) / 2
var center = width / 2
ctx.lineWidth = lineWidth
ctx.lineCap = "round"
if (!root.hasTransfer)
return
ctx.beginPath()
ctx.strokeStyle = root.ringColor
ctx.arc(center, center, radius, -Math.PI / 2,
-Math.PI / 2 + Math.PI * 2 * root.transferProgress, false)
ctx.stroke()
}
}
NumberAnimation {
id: connectingSpin
target: progressRing
property: "rotation"
from: 0
to: 360
duration: 1100
easing.type: Easing.Linear
loops: Animation.Infinite
running: root.isConnecting
}
Rectangle {
anchors.fill: parent
anchors.margins: 9
radius: width / 2
color: avatarFill
Label {
anchors.centerIn: parent
text: initialLetter(targetName)
font.pixelSize: 28
font.weight: Font.DemiBold
color: textPrimary
}
}
Rectangle {
anchors.fill: parent
anchors.margins: 9
radius: width / 2
color: "#16a34a"
opacity: showCompletionTick ? 0.94 : 0.0
visible: opacity > 0
Behavior on opacity {
NumberAnimation { duration: 160; easing.type: Easing.OutCubic }
}
Canvas {
anchors.centerIn: parent
width: 28
height: 28
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
ctx.strokeStyle = "#ffffff"
ctx.lineWidth = 4
ctx.lineCap = "round"
ctx.lineJoin = "round"
ctx.beginPath()
ctx.moveTo(width * 0.18, height * 0.56)
ctx.lineTo(width * 0.42, height * 0.8)
ctx.lineTo(width * 0.84, height * 0.24)
ctx.stroke()
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: canSend
onClicked: fileShareController.sendPendingFileToTarget(modelData.id)
}
}
Label {
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
horizontalAlignment: Text.AlignHCenter
text: targetName
font.pixelSize: 13
font.weight: Font.Bold
elide: Text.ElideRight
maximumLineCount: 2
wrapMode: Text.Wrap
color: textPrimary
}
}
Timer {
id: completionTickTimer
interval: 1200
repeat: false
onTriggered: root.showCompletionTick = false
}
onTransferDataChanged: progressRing.requestPaint()
onTransferProgressChanged: progressRing.requestPaint()
onRingColorChanged: progressRing.requestPaint()
onIsConnectingChanged: {
if (!isConnecting)
progressRing.rotation = 0
}
onTransferStatusChanged: {
if (transferStatus === "Complete" && previousTransferStatus.length > 0
&& previousTransferStatus !== "Complete") {
showCompletionTick = true
completionTickTimer.restart()
} else if (transferStatus !== "Complete") {
showCompletionTick = false
completionTickTimer.stop()
}
previousTransferStatus = transferStatus
}
Component.onCompleted: previousTransferStatus = transferStatus
}
@@ -1,131 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ColumnLayout {
id: root
property string urlText: fileShareController.qrCodeUrl
property var qrRows: fileShareController.qrCodeRows
property int qrSize: fileShareController.qrCodeSize
property string fileName: fileShareController.pendingSendFileName
readonly property color panelTint: "#ecfdf3"
readonly property color panelBorder: "#a7f3d0"
readonly property color qrPaper: "#fffdf7"
readonly property color qrInk: "#14532d"
readonly property color accentSoft: "#d1fae5"
readonly property color accentStrong: "#34d399"
readonly property bool compact: width < 360
readonly property real qrFrameSize: 360
readonly property real qrInnerSize: qrFrameSize - (compact ? 34 : 42)
spacing: compact ? 14 : 18
implicitWidth: 420
Label {
Layout.alignment: Qt.AlignHCenter
text: "Scan to connect"
font.pixelSize: compact ? 16 : 18
font.weight: Font.DemiBold
color: "#111827"
}
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: root.qrFrameSize
height: width
radius: compact ? 24 : 32
gradient: Gradient {
GradientStop { position: 0.0; color: "#f7fff9" }
GradientStop { position: 1.0; color: root.panelTint }
}
border.color: root.panelBorder
border.width: 1
Rectangle {
width: parent.width * 0.52
height: width
radius: width / 2
x: parent.width - width * 0.72
y: -width * 0.22
color: "#ffffff"
opacity: 0.35
}
Rectangle {
anchors.centerIn: parent
width: root.qrInnerSize
height: width
radius: compact ? 18 : 24
color: root.qrPaper
border.color: "#dcfce7"
border.width: 1
Canvas {
id: qrCanvas
anchors.fill: parent
anchors.margins: compact ? 16 : 22
antialiasing: true
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = root.qrPaper
ctx.fillRect(0, 0, width, height)
if (root.qrSize <= 0 || !root.qrRows || root.qrRows.length !== root.qrSize)
return
var quietZone = 4
var totalModules = root.qrSize + quietZone * 2
var moduleSize = Math.min(width, height) / totalModules
var drawSize = moduleSize * totalModules
var offsetX = (width - drawSize) / 2
var offsetY = (height - drawSize) / 2
var dotInset = moduleSize * 0.18
var dotSize = Math.max(1, moduleSize - dotInset * 2)
var dotRadius = dotSize/1.2
ctx.fillStyle = root.qrInk
for (var row = 0; row < root.qrSize; ++row) {
var rowData = root.qrRows[row]
for (var col = 0; col < root.qrSize; ++col) {
if (rowData.charAt(col) !== "1")
continue
var dotX = offsetX + (col + quietZone) * moduleSize + dotInset
var dotY = offsetY + (row + quietZone) * moduleSize + dotInset
ctx.beginPath()
ctx.arc(dotX + dotRadius, dotY + dotRadius, dotRadius, 0, Math.PI * 2)
ctx.fill()
}
}
}
}
Label {
anchors.centerIn: parent
visible: root.qrSize <= 0
text: "Preparing QR code..."
font.pixelSize: compact ? 12 : 13
color: "#6b7280"
}
}
}
Label {
Layout.alignment: Qt.AlignHCenter
text: root.fileName.length > 0 ? "Sending: " + root.fileName : ""
font.pixelSize: compact ? 12 : 13
color: "#6b7280"
visible: text.length > 0
}
onQrRowsChanged: qrCanvas.requestPaint()
onQrSizeChanged: qrCanvas.requestPaint()
}
@@ -1,234 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Drawer {
id: root
edge: Qt.RightEdge
width: 380
height: parent ? parent.height : 0
implicitWidth: 380
implicitHeight: parent ? parent.height : 0
readonly property color bg: "#f0fdf4"
readonly property color surface: "#ffffff"
readonly property color accent: "#38aa62"
readonly property color accentLight: "#dcfce7"
readonly property color borderColor: "#bbf7d0"
readonly property color textPrimary: "#111827"
readonly property color textMuted: "#6b7280"
background: Rectangle { color: root.bg }
ColumnLayout {
width: root.width
height: root.height
spacing: 0
// Header
Rectangle {
Layout.fillWidth: true
height: 64
color: "transparent"
RowLayout {
anchors.fill: parent
anchors.leftMargin: 20
anchors.rightMargin: 20
Label {
text: "Settings"
font.pixelSize: 20
font.weight: Font.Bold
color: root.textPrimary
}
Item { Layout.fillWidth: true }
Rectangle {
width: 32
height: 32
radius: 8
color: closeArea.containsMouse ? "#f3f4f6" : "transparent"
Label {
anchors.centerIn: parent
text: "✕"
font.pixelSize: 14
color: root.textMuted
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.close()
}
}
}
}
Flickable {
id: flick
Layout.fillWidth: true
Layout.fillHeight: true
clip: false
contentWidth: width
contentHeight: settingsCol.height + 32
ScrollBar.vertical: ScrollBar {}
Column {
id: settingsCol
x: 20
y: 20
width: flick.width - 40
spacing: 20
SectionLabel { text: "DEVICE" }
SectionCard {
width: settingsCol.width
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 12
spacing: 12
RowLayout {
Layout.fillWidth: true
spacing: 10
Label {
text: "Device name"
font.pixelSize: 13
color: root.textMuted
Layout.preferredWidth: 110
}
ThemedField {
text: fileShareController.deviceName
onEditingFinished: fileShareController.deviceName = text
}
}
}
}
SectionLabel { text: "SHARING" }
SectionCard {
width: settingsCol.width
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 12
spacing: 12
Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
font.pixelSize: 12
color: root.textMuted
text: "Nearby Sharing uses built-in transport and discovery settings."
}
RowLayout {
Layout.fillWidth: true
spacing: 10
Label {
Layout.fillWidth: true
color: root.textPrimary
font.pixelSize: 13
text: "Auto-accept incoming"
}
ThemedToggle {
checked: fileShareController.autoAcceptIncoming
onToggled: fileShareController.autoAcceptIncoming = checked
}
}
RowLayout {
Layout.fillWidth: true
spacing: 10
Label {
Layout.fillWidth: true
color: root.textPrimary
font.pixelSize: 13
text: "Enable 5 GHz hotspot"
}
ThemedToggle {
checked: fileShareController.enable5GhzHotspot
onToggled: fileShareController.enable5GhzHotspot = checked
}
}
}
}
SectionLabel { text: "LOGGING" }
SectionCard {
width: settingsCol.width
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 12
spacing: 12
RowLayout {
Layout.fillWidth: true
spacing: 10
Label {
text: "Log path"
font.pixelSize: 13
color: root.textMuted
Layout.preferredWidth: 110
}
ThemedField {
font.pixelSize: 11
text: fileShareController.logPath
onEditingFinished: fileShareController.logPath = text
}
}
}
}
}
}
}
component SectionLabel: Label {
font.pixelSize: 11
font.weight: Font.DemiBold
font.letterSpacing: 0.8
color: root.accent
}
component SectionCard: Rectangle {
radius: 12
color: root.surface
border.color: root.borderColor
height: (children.length > 0 ? children[0].implicitHeight : 0) + 24
}
component ThemedField: TextField {
Layout.fillWidth: true
implicitHeight: 38
font.pixelSize: 13
leftPadding: 12
rightPadding: 12
topPadding: 0
bottomPadding: 0
verticalAlignment: TextInput.AlignVCenter
color: root.textPrimary
background: Rectangle {
radius: 8
color: "#f9fafb"
border.color: parent.activeFocus ? root.accent : root.borderColor
border.width: parent.activeFocus ? 2 : 1
}
}
component ThemedToggle: Switch {
palette.highlight: root.accent
}
}
@@ -1,153 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
Layout.preferredWidth: 280
Layout.fillHeight: true
readonly property color surface: "#ffffff"
readonly property color cardBorder: "#bbf7d0"
readonly property color textPrimary: "#111827"
readonly property color textMuted: "#6b7280"
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 0
// Receive mode: visibility info
ColumnLayout {
visible: fileShareController.pendingSendFilePath.length === 0
Layout.fillWidth: true
spacing: 0
Label {
Layout.leftMargin: 12
Layout.topMargin: 16
Layout.bottomMargin: 8
text: "Visibility state"
color: textMuted
font.pixelSize: 13
}
Rectangle {
Layout.fillWidth: true
height: 52
radius: 12
color: "#e8faf0"
border.color: cardBorder
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
Label {
Layout.fillWidth: true
text: !fileShareController.running
? "Inactive"
: fileShareController.mode === "Send" ? "Discovering" : "Always visible"
font.weight: Font.Medium
color: textPrimary
}
}
}
Label {
Layout.fillWidth: true
Layout.leftMargin: 12
Layout.topMargin: 8
Layout.rightMargin: 12
text: !fileShareController.running
? "The service is not running. Start it to discover or receive files."
: fileShareController.mode === "Send"
? "Discovering nearby devices. Select a device below to send your file."
: "Nearby devices can share files with you. You'll be notified and must approve each transfer."
wrapMode: Text.WordWrap
font.pixelSize: 12
color: textMuted
}
}
// Send mode: outbound file info
ColumnLayout {
visible: fileShareController.pendingSendFilePath.length > 0
Layout.fillWidth: true
spacing: 0
Label {
Layout.leftMargin: 12
Layout.topMargin: 16
Layout.bottomMargin: 8
text: "Sharing 1 file"
font.weight: Font.Medium
color: textPrimary
}
Rectangle {
Layout.leftMargin: 12
width: 72
height: 72
radius: 12
color: surface
Label {
anchors.centerIn: parent
text: "📄"
font.pixelSize: 28
}
}
Label {
Layout.fillWidth: true
Layout.leftMargin: 12
Layout.topMargin: 8
Layout.rightMargin: 12
text: fileShareController.pendingSendFileName
elide: Text.ElideRight
font.pixelSize: 13
color: textMuted
}
Label {
Layout.fillWidth: true
Layout.leftMargin: 12
Layout.topMargin: 12
Layout.rightMargin: 12
text: "Make sure both devices are unlocked, close together, and have Bluetooth turned on."
wrapMode: Text.WordWrap
font.pixelSize: 12
color: textMuted
}
}
Item { Layout.fillHeight: true }
// Cancel (only visible in send mode)
Rectangle {
visible: fileShareController.pendingSendFilePath.length > 0
Layout.leftMargin: 12
Layout.bottomMargin: 12
height: 40
width: cancelLbl.implicitWidth + 24
radius: 12
color: "#f3f4f6"
border.color: "#d1d5db"
Label {
id: cancelLbl
anchors.centerIn: parent
text: "Cancel"
font.weight: Font.Medium
color: textPrimary
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: fileShareController.switchToReceiveMode()
}
}
}
}
@@ -1,157 +0,0 @@
#include "file_share_state.h"
#include "status_mapper.h"
FileShareState::FileShareState() = default;
void FileShareState::AddOrUpdateTarget(qlonglong id, const QString& name,
bool is_incoming) {
target_names_[id] = name;
if (discovered_row_by_target_.contains(id)) {
const int row_index = discovered_row_by_target_.value(id);
if (row_index >= 0 && row_index < discovered_targets_.size()) {
QVariantMap target;
target[QStringLiteral("id")] = id;
target[QStringLiteral("name")] = name;
target[QStringLiteral("isIncoming")] = is_incoming;
discovered_targets_[row_index] = target;
return;
}
}
QVariantMap target;
target[QStringLiteral("id")] = id;
target[QStringLiteral("name")] = name;
target[QStringLiteral("isIncoming")] = is_incoming;
discovered_row_by_target_[id] = discovered_targets_.size();
discovered_targets_.append(target);
}
void FileShareState::RemoveTarget(qlonglong id) {
if (HasActiveTransferForTarget(id)) {
AddPendingTargetRemoval(id);
return;
}
RemovePendingTargetRemoval(id);
target_names_.remove(id);
if (!discovered_row_by_target_.contains(id)) {
return;
}
const int removed_index = discovered_row_by_target_.take(id);
if (removed_index < 0 || removed_index >= discovered_targets_.size()) {
return;
}
discovered_targets_.removeAt(removed_index);
for (auto it = discovered_row_by_target_.begin();
it != discovered_row_by_target_.end(); ++it) {
if (it.value() > removed_index) {
it.value() = it.value() - 1;
}
}
}
QString FileShareState::GetTargetName(qlonglong id) const {
const QString name = target_names_.value(id).trimmed();
return name.isEmpty() ? QStringLiteral("Unknown device") : name;
}
bool FileShareState::HasTarget(qlonglong id) const {
return discovered_row_by_target_.contains(id);
}
void FileShareState::AddOrUpdateTransfer(
qlonglong target_id, const QString& target_name, const QString& status,
double progress, qulonglong transferred_bytes, const QString& direction,
const QString& file_name, const QString& file_path) {
QVariantMap transfer{
{QStringLiteral("targetId"), target_id},
{QStringLiteral("targetName"), target_name},
{QStringLiteral("status"), status},
{QStringLiteral("progress"), progress},
{QStringLiteral("transferredBytes"), transferred_bytes},
{QStringLiteral("direction"), direction},
{QStringLiteral("fileName"), file_name},
{QStringLiteral("filePath"), file_path},
};
if (transfer_row_by_target_.contains(target_id)) {
const int row_index = transfer_row_by_target_.value(target_id);
if (row_index >= 0 && row_index < transfers_.size()) {
transfers_[row_index] = transfer;
return;
}
}
transfer_row_by_target_.insert(target_id, transfers_.size());
transfers_.append(transfer);
}
void FileShareState::RemoveTransfer(qlonglong target_id) {
if (!transfer_row_by_target_.contains(target_id)) {
return;
}
const int removed_index = transfer_row_by_target_.take(target_id);
if (removed_index < 0 || removed_index >= transfers_.size()) {
return;
}
transfers_.removeAt(removed_index);
for (auto it = transfer_row_by_target_.begin();
it != transfer_row_by_target_.end(); ++it) {
if (it.value() > removed_index) {
it.value() = it.value() - 1;
}
}
}
bool FileShareState::HasActiveTransferForTarget(qlonglong target_id) const {
for (const QVariant& row_value : transfers_) {
const QVariantMap row = row_value.toMap();
if (row.value(QStringLiteral("targetId")).toLongLong() != target_id) {
continue;
}
const QString status = row.value(QStringLiteral("status")).toString();
if (StatusMapper::IsActiveTransferStatus(status)) {
return true;
}
}
return false;
}
bool FileShareState::HasActiveTransfers() const {
for (const QVariant& row_value : transfers_) {
const QVariantMap row = row_value.toMap();
const QString status = row.value(QStringLiteral("status")).toString();
if (StatusMapper::IsActiveTransferStatus(status)) {
return true;
}
}
return false;
}
void FileShareState::AddPendingTargetRemoval(qlonglong id) {
pending_target_removals_.insert(id);
}
void FileShareState::RemovePendingTargetRemoval(qlonglong id) {
pending_target_removals_.remove(id);
}
bool FileShareState::IsPendingTargetRemoval(qlonglong id) const {
return pending_target_removals_.contains(id);
}
void FileShareState::ClearAll() {
discovered_targets_.clear();
discovered_row_by_target_.clear();
target_names_.clear();
transfers_.clear();
transfer_row_by_target_.clear();
pending_target_removals_.clear();
}
@@ -1,122 +0,0 @@
#ifndef SHARING_LINUX_QML_TRAY_APP_FILE_SHARE_STATE_H_
#define SHARING_LINUX_QML_TRAY_APP_FILE_SHARE_STATE_H_
#include <QHash>
#include <QString>
#include <QStringList>
#include <QVariantList>
#include <QVariantMap>
#include <QSet>
#include "share_target_model.h"
#include "transfer_model.h"
class FileShareState {
public:
FileShareState();
// Getters
QString mode() const { return mode_; }
QString deviceName() const { return device_name_; }
QString statusMessage() const { return status_message_; }
bool running() const { return running_; }
bool autoAcceptIncoming() const { return auto_accept_incoming_; }
bool enable5GhzHotspot() const { return enable_5ghz_hotspot_; }
QString pendingSendFileName() const { return pending_send_file_name_; }
QString pendingSendFilePath() const { return pending_send_file_path_; }
qlonglong pendingSendTargetId() const { return pending_send_target_id_; }
QVariantList discoveredTargets() const { return discovered_targets_; }
QVariantList transfers() const { return transfers_; }
QString qrCodeUrl() const { return qr_code_url_; }
QStringList qrCodeRows() const { return qr_code_rows_; }
int qrCodeSize() const { return qr_code_size_; }
QString logPath() const { return log_path_; }
// Setters
void SetMode(const QString& mode) { mode_ = mode; }
void SetDeviceName(const QString& name) { device_name_ = name; }
void SetStatusMessage(const QString& message) { status_message_ = message; }
void SetRunning(bool running) { running_ = running; }
void SetAutoAcceptIncoming(bool enabled) { auto_accept_incoming_ = enabled; }
void SetEnable5GhzHotspot(bool enabled) { enable_5ghz_hotspot_ = enabled; }
void SetPendingSendFile(const QString& file_path, const QString& file_name,
qlonglong target_id) {
pending_send_file_path_ = file_path;
pending_send_file_name_ = file_name;
pending_send_target_id_ = target_id;
}
void ClearPendingSendFile() {
pending_send_file_path_.clear();
pending_send_file_name_.clear();
pending_send_target_id_ = 0;
}
void SetQrCodeData(const QString& url, const QStringList& rows, int size) {
qr_code_url_ = url;
qr_code_rows_ = rows;
qr_code_size_ = size;
}
void SetLogPath(const QString& path) { log_path_ = path; }
// Target management
void AddOrUpdateTarget(qlonglong id, const QString& name, bool is_incoming);
void RemoveTarget(qlonglong id);
QString GetTargetName(qlonglong id) const;
bool HasTarget(qlonglong id) const;
// Transfer management
void AddOrUpdateTransfer(qlonglong target_id, const QString& target_name,
const QString& status, double progress,
qulonglong transferred_bytes,
const QString& direction, const QString& file_name,
const QString& file_path);
void RemoveTransfer(qlonglong target_id);
bool HasActiveTransferForTarget(qlonglong target_id) const;
bool HasActiveTransfers() const;
// Pending target removal management
void AddPendingTargetRemoval(qlonglong id);
void RemovePendingTargetRemoval(qlonglong id);
bool IsPendingTargetRemoval(qlonglong id) const;
void ClearAll();
private:
QString mode_ = QStringLiteral("Receive");
QString device_name_ = QStringLiteral("NearbyLinux");
QString status_message_ = QStringLiteral("Idle");
bool running_ = false;
bool auto_accept_incoming_ = true;
bool enable_5ghz_hotspot_ = true;
// QR Code
QString qr_code_url_;
QStringList qr_code_rows_;
int qr_code_size_ = 0;
QString log_path_ = QStringLiteral("/tmp/nearby_qml_file_tray.log");
// Pending send state
QString pending_send_file_path_;
QString pending_send_file_name_;
qlonglong pending_send_target_id_ = 0;
// Discovered targets
QVariantList discovered_targets_;
QHash<qlonglong, int> discovered_row_by_target_;
QHash<qlonglong, QString> target_names_;
// Transfers
QVariantList transfers_;
QHash<qlonglong, int> transfer_row_by_target_;
// Pending removals
QSet<qlonglong> pending_target_removals_;
};
#endif // SHARING_LINUX_QML_TRAY_APP_FILE_SHARE_STATE_H_
@@ -1,587 +0,0 @@
#include "file_share_tray_controller.h"
#include <iostream>
#include <QClipboard>
#include <QDesktopServices>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QGuiApplication>
#include <QMetaObject>
#include <QSettings>
#include <QSysInfo>
#include <QTimer>
#include <QUrl>
#include "string_utils.h"
#include "status_mapper.h"
#include "qr_code_generator.h"
FileShareTrayController::FileShareTrayController(QObject* parent)
: QObject(parent) {
const QString host = QSysInfo::machineHostName().trimmed();
if (!host.isEmpty()) {
state_.SetDeviceName(host);
}
loadSettings();
initializeService();
}
FileShareTrayController::~FileShareTrayController() {
stop();
if (service_) {
service_->Shutdown([](NearbySharingApi::StatusCode) {});
}
}
void FileShareTrayController::initializeService() {
service_ = std::make_unique<NearbySharingApi>(state_.deviceName().toStdString());
service_->Set5GhzHotspotEnabled(state_.enable5GhzHotspot());
state_.SetQrCodeData(QString::fromStdString(service_->GetQrCodeUrl()), {}, 0);
updateQrCodeData();
emit qrCodeUrlChanged();
emit qrCodeChanged();
attachServiceListeners();
// service_ ->StartFastInitiationScanning([](auto a)
// {
// std::cout << "Probably fine";
// });
}
void FileShareTrayController::updateQrCodeData() {
const auto qr_data = QrCodeGenerator::GenerateQrCode(state_.qrCodeUrl());
state_.SetQrCodeData(state_.qrCodeUrl(), qr_data.rows, qr_data.size);
emit qrCodeChanged();
}
void FileShareTrayController::attachServiceListeners() {
NearbySharingApi::Listener listener;
listener.target_discovered_cb = [this](const NearbySharingApi::ShareTargetInfo& info) {
QMetaObject::invokeMethod(this, [this, info]() { updateTargetFromInfo(info); },
Qt::QueuedConnection);
};
listener.target_updated_cb = [this](const NearbySharingApi::ShareTargetInfo& info) {
QMetaObject::invokeMethod(this, [this, info]() { updateTargetFromInfo(info); },
Qt::QueuedConnection);
};
listener.target_lost_cb = [this](int64_t share_target_id) {
QMetaObject::invokeMethod(
this, [this, share_target_id]() {
state_.RemoveTarget(share_target_id);
emit discoveredTargetsChanged();
},
Qt::QueuedConnection);
};
listener.transfer_update_cb = [this](const NearbySharingApi::TransferUpdateInfo& update) {
QMetaObject::invokeMethod(this, [this, update]() { handleTransferUpdate(update); },
Qt::QueuedConnection);
};
service_->SetListener(std::move(listener));
}
void FileShareTrayController::updateTargetFromInfo(
const NearbySharingApi::ShareTargetInfo& info) {
const QString name = StringUtils::TrimmedOrFallback(
StringUtils::FromStdString(info.device_name),
QStringLiteral("Unknown device"));
state_.AddOrUpdateTarget(info.id, name, info.is_incoming);
emit discoveredTargetsChanged();
}
void FileShareTrayController::handleTransferUpdate(
const NearbySharingApi::TransferUpdateInfo& update) {
const QString target_name = StringUtils::TrimmedFromStdString(update.device_name);
if (!target_name.isEmpty()) {
state_.AddOrUpdateTarget(update.share_target_id, target_name, update.is_incoming);
}
const QString name = state_.GetTargetName(update.share_target_id);
const QString status = StatusMapper::TransferStatusToString(update.status);
const QString direction =
update.is_incoming ? QStringLiteral("incoming") : QStringLiteral("outgoing");
QString file_name = StringUtils::FromStdString(update.first_file_name);
if (file_name.isEmpty() && !update.is_incoming &&
state_.pendingSendTargetId() == update.share_target_id &&
!state_.pendingSendFileName().isEmpty()) {
file_name = state_.pendingSendFileName();
}
state_.AddOrUpdateTransfer(update.share_target_id, name, status, update.progress,
update.transferred_bytes, direction, file_name,
StringUtils::FromStdString(update.first_file_path));
emit transfersChanged();
setStatus(QStringLiteral("%1 (%2)").arg(status, name));
// Auto-accept incoming transfers if enabled
if (update.status == NearbySharingApi::TransferStatus::kAwaitingLocalConfirmation &&
state_.autoAcceptIncoming()) {
service_->Accept(update.share_target_id, [](NearbySharingApi::StatusCode) {});
}
// Handle final transfer status
if (StatusMapper::IsFinalTransferStatus(update.status)) {
handleTransferComplete(update);
}
}
void FileShareTrayController::handleTransferComplete(
const NearbySharingApi::TransferUpdateInfo& update) {
const bool success = update.status == NearbySharingApi::TransferStatus::kComplete;
const QString name = state_.GetTargetName(update.share_target_id);
if (update.is_incoming) {
handleIncomingTransferComplete(update, name, success);
} else {
handleOutgoingTransferComplete(update, name, success);
}
// Cleanup pending send state
if (state_.pendingSendTargetId() == update.share_target_id) {
state_.ClearPendingSendFile();
emit pendingSendFilePathChanged();
emit pendingSendFileNameChanged();
// Auto-switch to receive mode after successful send
if (!update.is_incoming && success) {
switchToReceiveMode();
}
}
// Deferred target removal
if (state_.IsPendingTargetRemoval(update.share_target_id)) {
QTimer::singleShot(1400, this, [this, target_id = update.share_target_id]() {
if (state_.IsPendingTargetRemoval(target_id)) {
state_.RemoveTarget(target_id);
emit discoveredTargetsChanged();
}
});
}
}
void FileShareTrayController::handleIncomingTransferComplete(
const NearbySharingApi::TransferUpdateInfo& update, const QString& name,
bool success) {
if (!success) {
emit requestTrayMessage(
QStringLiteral("Receive failed"),
QStringLiteral("Transfer from %1 failed").arg(name));
return;
}
const QString file_name =
StringUtils::FromStdString(update.first_file_name).isEmpty()
? QStringLiteral("file")
: StringUtils::FromStdString(update.first_file_name);
// Check for received URL
for (const auto& text : update.text_attachments) {
if (text.type == NearbySharingApi::TextAttachmentType::kUrl) {
const QString link = StringUtils::TrimmedFromStdString(text.text_body);
if (!link.isEmpty()) {
emit requestCopyLinkTrayMessage(QStringLiteral("Link received"),
QStringLiteral("%1 from %2").arg(link, name),
link);
return;
}
}
}
// Check for received text
if (!update.text_attachments.empty()) {
const QString text_summary = [&]() {
for (const auto& text : update.text_attachments) {
const QString title = StringUtils::TrimmedFromStdString(text.text_title);
if (!title.isEmpty()) return title;
const QString body = StringUtils::TrimmedFromStdString(text.text_body);
if (!body.isEmpty()) return body;
}
return QStringLiteral("Text");
}();
emit requestTrayMessage(QStringLiteral("Text received"),
QStringLiteral("%1 from %2").arg(text_summary, name));
return;
}
emit requestTrayMessage(QStringLiteral("File received"),
QStringLiteral("%1 from %2").arg(file_name, name));
}
void FileShareTrayController::handleOutgoingTransferComplete(
const NearbySharingApi::TransferUpdateInfo& update, const QString& name,
bool success) {
const QString file_name =
StringUtils::FromStdString(update.first_file_name).isEmpty()
? QStringLiteral("file")
: StringUtils::FromStdString(update.first_file_name);
if (success) {
emit requestTrayMessage(QStringLiteral("Send complete"),
QStringLiteral("%1 sent to %2").arg(file_name, name));
} else {
emit requestTrayMessage(QStringLiteral("Send failed"),
QStringLiteral("%1 failed to send to %2").arg(file_name, name));
}
}
void FileShareTrayController::loadSettings() {
QSettings settings(QStringLiteral("Nearby"), QStringLiteral("QmlFileTrayApp"));
const QString stored_device_name =
settings.value(QStringLiteral("deviceName"), state_.deviceName())
.toString()
.trimmed();
if (!stored_device_name.isEmpty()) {
state_.SetDeviceName(stored_device_name);
}
const bool stored_auto_accept =
settings.value(QStringLiteral("autoAcceptIncoming"), true).toBool();
state_.SetAutoAcceptIncoming(stored_auto_accept);
const bool stored_enable_5ghz_hotspot =
settings.value(QStringLiteral("enable5GhzHotspot"), true).toBool();
state_.SetEnable5GhzHotspot(stored_enable_5ghz_hotspot);
const QString stored_log_path =
settings.value(QStringLiteral("logPath"), QStringLiteral("/tmp/nearby_qml_file_tray.log"))
.toString()
.trimmed();
if (!stored_log_path.isEmpty()) {
state_.SetLogPath(stored_log_path);
}
}
void FileShareTrayController::saveSettings() const {
QSettings settings(QStringLiteral("Nearby"), QStringLiteral("QmlFileTrayApp"));
settings.setValue(QStringLiteral("deviceName"), state_.deviceName());
settings.setValue(QStringLiteral("autoAcceptIncoming"), state_.autoAcceptIncoming());
settings.setValue(QStringLiteral("enable5GhzHotspot"),
state_.enable5GhzHotspot());
settings.setValue(QStringLiteral("logPath"), state_.logPath());
}
void FileShareTrayController::setDeviceName(const QString& device_name) {
const QString trimmed = device_name.trimmed();
if (trimmed.isEmpty() || trimmed == state_.deviceName()) {
return;
}
state_.SetDeviceName(trimmed);
saveSettings();
emit deviceNameChanged();
if (state_.running()) {
stop();
initializeService();
start();
}
}
void FileShareTrayController::setAutoAcceptIncoming(bool enabled) {
if (enabled == state_.autoAcceptIncoming()) {
return;
}
state_.SetAutoAcceptIncoming(enabled);
saveSettings();
emit autoAcceptIncomingChanged();
}
void FileShareTrayController::setEnable5GhzHotspot(bool enabled) {
if (enabled == state_.enable5GhzHotspot()) {
return;
}
state_.SetEnable5GhzHotspot(enabled);
if (service_) {
service_->Set5GhzHotspotEnabled(enabled);
}
saveSettings();
emit enable5GhzHotspotChanged();
}
void FileShareTrayController::setLogPath(const QString& path) {
const QString trimmed = path.trimmed();
if (trimmed.isEmpty() || trimmed == state_.logPath()) {
return;
}
state_.SetLogPath(trimmed);
saveSettings();
emit logPathChanged();
}
void FileShareTrayController::start() {
if (state_.running()) {
return;
}
state_.SetRunning(true);
emit runningChanged();
}
void FileShareTrayController::stop() {
if (!state_.running()) {
return;
}
state_.SetRunning(false);
emit runningChanged();
service_->StopSendMode([](NearbySharingApi::StatusCode) {});
service_->StopReceiveMode([](NearbySharingApi::StatusCode) {});
state_.ClearAll();
emit discoveredTargetsChanged();
emit transfersChanged();
setStatus(QStringLiteral("Stopped"));
}
void FileShareTrayController::startSendMode() {
service_->StopReceiveMode([this](NearbySharingApi::StatusCode status) {
if (status == NearbySharingApi::StatusCode::kOk ||
status == NearbySharingApi::StatusCode::kStatusAlreadyStopped) {
service_->StartSendMode([this](NearbySharingApi::StatusCode status) {
QMetaObject::invokeMethod(
this,
[this, status]() {
setStatus(QStringLiteral("StartSendMode: %1")
.arg(StatusMapper::ApiStatusToString(status)));
if (status != NearbySharingApi::StatusCode::kOk) {
state_.SetRunning(false);
emit runningChanged();
}
},
Qt::QueuedConnection);
});
}
});
}
void FileShareTrayController::startReceiveMode() {
service_->StopSendMode([this](NearbySharingApi::StatusCode status) {
if (status == NearbySharingApi::StatusCode::kOk ||
status == NearbySharingApi::StatusCode::kStatusAlreadyStopped) {
service_->StartReceiveMode([this](NearbySharingApi::StatusCode status) {
QMetaObject::invokeMethod(
this,
[this, status]() {
setStatus(QStringLiteral("StartReceiveMode: %1")
.arg(StatusMapper::ApiStatusToString(status)));
if (status != NearbySharingApi::StatusCode::kOk) {
state_.SetRunning(false);
emit runningChanged();
}
},
Qt::QueuedConnection);
});
}
});
}
void FileShareTrayController::switchToReceiveMode() {
if (state_.running() && state_.HasActiveTransfers()) {
setStatus(QStringLiteral("Cannot switch mode while transfer is active"));
emit requestTrayMessage(
QStringLiteral("Transfer in progress"),
QStringLiteral("Wait for the current transfer to complete."));
return;
}
if (state_.running())
{
startReceiveMode();
state_.SetMode(QStringLiteral("Receive"));
emit modeChanged();
}
}
void FileShareTrayController::switchToSendModeWithFile(const QString& file_path) {
const QString trimmed_path = file_path.trimmed();
QFileInfo info(trimmed_path);
if (trimmed_path.isEmpty() || !info.exists() || !info.isFile()) {
setStatus(QStringLiteral("Selected file is not valid"));
emit requestTrayMessage(QStringLiteral("Send canceled"),
QStringLiteral("Please choose a valid file."));
return;
}
state_.SetPendingSendFile(info.absoluteFilePath(), info.fileName(), 0);
emit pendingSendFilePathChanged();
emit pendingSendFileNameChanged();
if (state_.running() && state_.HasActiveTransfers()) {
setStatus(QStringLiteral("Cannot switch mode while transfer is active"));
emit requestTrayMessage(
QStringLiteral("Transfer in progress"),
QStringLiteral("Wait for the current transfer to complete."));
return;
}
if (state_.running())
{
startSendMode();
state_.SetMode(QStringLiteral("Send"));
emit modeChanged();
}
setStatus(QStringLiteral("Discovery started. Choose a nearby device."));
emit requestTrayMessage(
QStringLiteral("Send mode"),
QStringLiteral("Selected %1. Choose a nearby device to send.")
.arg(info.fileName()));
}
void FileShareTrayController::sendPendingFileToTarget(qlonglong share_target_id) {
if (share_target_id <= 0) {
return;
}
const QString file_path = state_.pendingSendFilePath();
QFileInfo file_info(file_path);
if (file_path.isEmpty() || !file_info.exists() || !file_info.isFile()) {
setStatus(QStringLiteral("Selected file is not available"));
emit requestTrayMessage(QStringLiteral("Send failed"),
QStringLiteral("Selected file is not available."));
return;
}
const QString target_name = state_.GetTargetName(share_target_id);
state_.SetPendingSendFile(file_path, file_info.fileName(), share_target_id);
state_.AddOrUpdateTransfer(share_target_id, target_name, QStringLiteral("Queued"), 0.0, 0,
QStringLiteral("outgoing"), file_info.fileName(),
file_info.absoluteFilePath());
emit transfersChanged();
service_->SendFile(
share_target_id, file_info.absoluteFilePath().toStdString(),
[this, share_target_id](NearbySharingApi::StatusCode status) {
QMetaObject::invokeMethod(
this,
[this, share_target_id, status]() {
const QString target_name = state_.GetTargetName(share_target_id);
if (status == NearbySharingApi::StatusCode::kOk) {
setStatus(QStringLiteral("Sending %1 to %2")
.arg(state_.pendingSendFileName(), target_name));
return;
}
emit requestTrayMessage(
QStringLiteral("Send failed"),
QStringLiteral("Could not send to %1").arg(target_name));
state_.AddOrUpdateTransfer(share_target_id, target_name,
QStringLiteral("Failed"), 0.0, 0,
QStringLiteral("outgoing"),
state_.pendingSendFileName(),
state_.pendingSendFilePath());
emit transfersChanged();
state_.SetPendingSendFile("", "", 0);
},
Qt::QueuedConnection);
});
}
void FileShareTrayController::copyTextToClipboard(const QString& text) {
const QString trimmed = text.trimmed();
if (trimmed.isEmpty()) {
return;
}
QClipboard* clipboard = QGuiApplication::clipboard();
if (clipboard == nullptr) {
emit requestTrayMessage(QStringLiteral("Copy failed"),
QStringLiteral("Clipboard is not available."));
return;
}
clipboard->setText(trimmed, QClipboard::Clipboard);
setStatus(QStringLiteral("Connection URL copied to clipboard"));
emit requestTrayMessage(QStringLiteral("URL copied"),
QStringLiteral("Link copied to clipboard."));
}
void FileShareTrayController::openFileLocation(const QString& file_path) {
const QString trimmed = file_path.trimmed();
if (trimmed.isEmpty()) {
emit requestTrayMessage(QStringLiteral("Open location failed"),
QStringLiteral("No received file location is available."));
return;
}
QFileInfo info(trimmed);
QString target_path;
if (info.exists() && info.isFile()) {
// Open the containing folder so the file is visible in the user's file
// manager regardless of the desktop environment.
target_path = info.absolutePath();
} else if (info.exists() && info.isDir()) {
target_path = info.absoluteFilePath();
} else {
// Some transfer updates can outlive the exact file entry we saw earlier;
// fall back to the parent directory when it still exists.
const QFileInfo parent_info(info.absolutePath());
if (parent_info.exists() && parent_info.isDir()) {
target_path = parent_info.absoluteFilePath();
}
}
if (target_path.isEmpty()) {
emit requestTrayMessage(QStringLiteral("Open location failed"),
QStringLiteral("The file location is no longer available."));
return;
}
const bool opened =
QDesktopServices::openUrl(QUrl::fromLocalFile(target_path));
if (!opened) {
emit requestTrayMessage(QStringLiteral("Open location failed"),
QStringLiteral("Could not open the file location."));
}
}
void FileShareTrayController::clearTransfers() {
state_.ClearAll();
emit discoveredTargetsChanged();
emit transfersChanged();
}
void FileShareTrayController::hideToTray() {
// This is handled by the main window, but can be extended here if needed
}
void FileShareTrayController::setStatus(const QString& status) {
if (status == state_.statusMessage()) {
return;
}
state_.SetStatusMessage(status);
emit statusMessageChanged();
}
void FileShareTrayController::notifyStateChange(const QString& property) {
if (property == QStringLiteral("mode")) {
emit modeChanged();
} else if (property == QStringLiteral("deviceName")) {
emit deviceNameChanged();
} else if (property == QStringLiteral("statusMessage")) {
emit statusMessageChanged();
} else if (property == QStringLiteral("running")) {
emit runningChanged();
} else if (property == QStringLiteral("autoAcceptIncoming")) {
emit autoAcceptIncomingChanged();
} else if (property == QStringLiteral("enable5GhzHotspot")) {
emit enable5GhzHotspotChanged();
} else if (property == QStringLiteral("discoveredTargets")) {
emit discoveredTargetsChanged();
} else if (property == QStringLiteral("transfers")) {
emit transfersChanged();
}
}
@@ -1,109 +0,0 @@
#ifndef SHARING_LINUX_QML_TRAY_APP_FILE_SHARE_TRAY_CONTROLLER_H_
#define SHARING_LINUX_QML_TRAY_APP_FILE_SHARE_TRAY_CONTROLLER_H_
#include <QObject>
#include <memory>
#include "file_share_state.h"
#include <sharing/linux/nearby_sharing_api.h>
using NearbySharingApi = nearby::sharing::NearbySharingApi;
class FileShareTrayController : public QObject {
Q_OBJECT
Q_PROPERTY(QString mode READ mode NOTIFY modeChanged)
Q_PROPERTY(QString deviceName READ deviceName WRITE setDeviceName NOTIFY deviceNameChanged)
Q_PROPERTY(QString statusMessage READ statusMessage NOTIFY statusMessageChanged)
Q_PROPERTY(bool running READ running NOTIFY runningChanged)
Q_PROPERTY(QString pendingSendFileName READ pendingSendFileName NOTIFY pendingSendFileNameChanged)
Q_PROPERTY(QString pendingSendFilePath READ pendingSendFilePath NOTIFY pendingSendFilePathChanged)
Q_PROPERTY(QVariantList discoveredTargets READ discoveredTargets NOTIFY discoveredTargetsChanged)
Q_PROPERTY(QVariantList transfers READ transfers NOTIFY transfersChanged)
Q_PROPERTY(bool autoAcceptIncoming READ autoAcceptIncoming WRITE setAutoAcceptIncoming NOTIFY autoAcceptIncomingChanged)
Q_PROPERTY(bool enable5GhzHotspot READ enable5GhzHotspot WRITE setEnable5GhzHotspot NOTIFY enable5GhzHotspotChanged)
Q_PROPERTY(QString qrCodeUrl READ qrCodeUrl NOTIFY qrCodeUrlChanged)
Q_PROPERTY(QStringList qrCodeRows READ qrCodeRows NOTIFY qrCodeChanged)
Q_PROPERTY(int qrCodeSize READ qrCodeSize NOTIFY qrCodeChanged)
Q_PROPERTY(QString logPath READ logPath WRITE setLogPath NOTIFY logPathChanged)
public:
explicit FileShareTrayController(QObject* parent = nullptr);
~FileShareTrayController() override;
// Property accessors
QString mode() const { return state_.mode(); }
QString deviceName() const { return state_.deviceName(); }
QString statusMessage() const { return state_.statusMessage(); }
bool running() const { return state_.running(); }
QString pendingSendFileName() const { return state_.pendingSendFileName(); }
QString pendingSendFilePath() const { return state_.pendingSendFilePath(); }
QVariantList discoveredTargets() const { return state_.discoveredTargets(); }
QVariantList transfers() const { return state_.transfers(); }
bool autoAcceptIncoming() const { return state_.autoAcceptIncoming(); }
bool enable5GhzHotspot() const { return state_.enable5GhzHotspot(); }
QString qrCodeUrl() const { return state_.qrCodeUrl(); }
QStringList qrCodeRows() const { return state_.qrCodeRows(); }
int qrCodeSize() const { return state_.qrCodeSize(); }
QString logPath() const { return state_.logPath(); }
// Public methods
void setDeviceName(const QString& device_name);
void setAutoAcceptIncoming(bool enabled);
void setEnable5GhzHotspot(bool enabled);
void setLogPath(const QString& path);
Q_INVOKABLE void start();
Q_INVOKABLE void stop();
Q_INVOKABLE void switchToReceiveMode();
Q_INVOKABLE void switchToSendModeWithFile(const QString& file_path);
Q_INVOKABLE void sendPendingFileToTarget(qlonglong share_target_id);
Q_INVOKABLE void copyTextToClipboard(const QString& text);
Q_INVOKABLE void openFileLocation(const QString& file_path);
Q_INVOKABLE void clearTransfers();
Q_INVOKABLE void hideToTray();
signals:
void modeChanged();
void deviceNameChanged();
void statusMessageChanged();
void runningChanged();
void pendingSendFileNameChanged();
void pendingSendFilePathChanged();
void discoveredTargetsChanged();
void transfersChanged();
void autoAcceptIncomingChanged();
void enable5GhzHotspotChanged();
void qrCodeUrlChanged();
void qrCodeChanged();
void logPathChanged();
void requestTrayMessage(const QString& title, const QString& body);
void requestCopyLinkTrayMessage(const QString& title, const QString& body,
const QString& link);
private:
void initializeService();
void attachServiceListeners();
void loadSettings();
void saveSettings() const;
void updateQrCodeData();
void startSendMode();
void startReceiveMode();
void updateTargetFromInfo(const NearbySharingApi::ShareTargetInfo& info);
void handleTransferUpdate(const NearbySharingApi::TransferUpdateInfo& update);
void handleTransferComplete(const NearbySharingApi::TransferUpdateInfo& update);
void handleIncomingTransferComplete(const NearbySharingApi::TransferUpdateInfo& update,
const QString& name, bool success);
void handleOutgoingTransferComplete(const NearbySharingApi::TransferUpdateInfo& update,
const QString& name, bool success);
void setStatus(const QString& status);
void notifyStateChange(const QString& property);
std::unique_ptr<NearbySharingApi> service_;
FileShareState state_;
};
#endif // SHARING_LINUX_QML_TRAY_APP_FILE_SHARE_TRAY_CONTROLLER_H_
@@ -1,240 +0,0 @@
#include <QAction>
#include <QApplication>
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QIcon>
#include <QMenu>
#include <QPainter>
#include <QPalette>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickWindow>
#include <QSettings>
#include <QStyleHints>
#include <QSystemTrayIcon>
#include <fcntl.h>
#include <unistd.h>
#include "file_share_tray_controller.h"
#include "notification_manager.h"
namespace {
constexpr char kDefaultLogPath[] = "/tmp/nearby_qml_file_tray.log";
bool EnsureLogDirectory(const QString& file_path) {
const QFileInfo file_info(file_path);
QDir directory = file_info.absoluteDir();
if (directory.exists()) {
return true;
}
return directory.mkpath(QStringLiteral("."));
}
bool RedirectStdStreamsToFile(const QString& file_path) {
const QByteArray encoded_path = QFile::encodeName(file_path);
const int fd = ::open(encoded_path.constData(), O_CREAT | O_APPEND | O_WRONLY, 0644);
if (fd < 0) {
return false;
}
const bool redirected_stdout = ::dup2(fd, STDOUT_FILENO) >= 0;
const bool redirected_stderr = ::dup2(fd, STDERR_FILENO) >= 0;
::close(fd);
return redirected_stdout && redirected_stderr;
}
QString ResolveConfiguredLogPath() {
QSettings settings(QStringLiteral("Nearby"), QStringLiteral("QmlFileTrayApp"));
const QString configured_path =
settings.value(QStringLiteral("logPath"),
QString::fromLatin1(kDefaultLogPath))
.toString()
.trimmed();
if (configured_path.isEmpty()) {
return QString::fromLatin1(kDefaultLogPath);
}
return configured_path;
}
void RedirectProcessLogsToConfiguredPath() {
QString log_path = ResolveConfiguredLogPath();
if (EnsureLogDirectory(log_path) && RedirectStdStreamsToFile(log_path)) {
return;
}
const QString fallback_path = QString::fromLatin1(kDefaultLogPath);
if (log_path == fallback_path) {
return;
}
if (!EnsureLogDirectory(fallback_path)) {
return;
}
RedirectStdStreamsToFile(fallback_path);
}
QIcon BuildTintedSymbolicIcon(const QString& source, const QColor& color) {
QIcon source_icon(source);
if (source_icon.isNull()) {
return QIcon();
}
QIcon tinted_icon;
for (int size : {16, 18, 20, 22, 24, 32}) {
QPixmap pixmap = source_icon.pixmap(size, size);
if (pixmap.isNull()) {
continue;
}
QPixmap tinted(pixmap.size());
tinted.fill(Qt::transparent);
QPainter painter(&tinted);
painter.drawPixmap(0, 0, pixmap);
painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
painter.fillRect(tinted.rect(), color);
painter.end();
tinted_icon.addPixmap(tinted);
}
return tinted_icon;
}
} // namespace
int main(int argc, char* argv[]) {
RedirectProcessLogsToConfiguredPath();
QApplication app(argc, argv);
app.setQuitOnLastWindowClosed(false);
FileShareTrayController controller;
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("fileShareController", &controller);
engine.load(QUrl(QStringLiteral("qrc:/qml/FileShareTray.qml")));
if (engine.rootObjects().isEmpty()) {
return 1;
}
auto* window = qobject_cast<QQuickWindow*>(engine.rootObjects().first());
if (window == nullptr) {
return 1;
}
const auto resolve_tray_icon = [&app]() {
const QColor white = "white";
QIcon tray_icon = BuildTintedSymbolicIcon(
QStringLiteral(":/icons/tray_icon-symbolic.svg"), white);
if (tray_icon.isNull()) {
tray_icon = QIcon::fromTheme(QStringLiteral("network-wireless-symbolic"));
}
if (tray_icon.isNull()) {
tray_icon = QIcon(QStringLiteral(":/icons/tray_icon.png"));
}
if (tray_icon.isNull()) {
tray_icon = app.windowIcon();
}
return tray_icon;
};
QSystemTrayIcon tray(resolve_tray_icon());
tray.setToolTip(QStringLiteral("Nearby File Tray"));
NotificationManager notification_manager(&tray, &app);
QMenu tray_menu;
QAction* send_action = tray_menu.addAction(QStringLiteral("Send"));
QAction* receive_action = tray_menu.addAction(QStringLiteral("Receive"));
tray_menu.addSeparator();
QAction* show_action = tray_menu.addAction(QStringLiteral("Show"));
QAction* hide_action = tray_menu.addAction(QStringLiteral("Hide"));
tray_menu.addSeparator();
QAction* quit_action = tray_menu.addAction(QStringLiteral("Quit"));
QObject::connect(send_action, &QAction::triggered, window,
[&controller, window]() {
const QString file = QFileDialog::getOpenFileName(
nullptr, QStringLiteral("Select file to send"));
if (file.isEmpty()) {
return;
}
controller.switchToSendModeWithFile(file);
window->show();
window->raise();
window->requestActivate();
});
QObject::connect(receive_action, &QAction::triggered,
[&controller, window]() {
controller.switchToReceiveMode();
window->show();
window->raise();
window->requestActivate();
});
QObject::connect(show_action, &QAction::triggered, window, [window]() {
window->show();
window->raise();
window->requestActivate();
});
QObject::connect(hide_action, &QAction::triggered, window, [window]() {
window->hide();
});
QObject::connect(quit_action, &QAction::triggered, &app,
[&controller, &app]() {
controller.stop();
app.quit();
});
QObject::connect(&tray, &QSystemTrayIcon::activated, window,
[window](QSystemTrayIcon::ActivationReason reason) {
if (reason != QSystemTrayIcon::Trigger &&
reason != QSystemTrayIcon::DoubleClick) {
return;
}
if (window->isVisible()) {
window->hide();
} else {
window->show();
window->raise();
window->requestActivate();
}
});
QObject::connect(&controller, &FileShareTrayController::requestTrayMessage,
&notification_manager, &NotificationManager::ShowNotification);
QObject::connect(&controller,
&FileShareTrayController::requestCopyLinkTrayMessage,
&notification_manager,
[&notification_manager](const QString& title,
const QString& body,
const QString& link) {
notification_manager.ShowCopyableNotification(
title, body, link, QStringLiteral("Copy link"));
});
QObject::connect(&app, &QCoreApplication::aboutToQuit, &controller,
[&controller]() { controller.stop(); });
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
QObject::connect(app.styleHints(), &QStyleHints::colorSchemeChanged, &tray,
[&tray, &resolve_tray_icon](Qt::ColorScheme) {
tray.setIcon(resolve_tray_icon());
});
#endif
tray.setContextMenu(&tray_menu);
tray.show();
controller.start();
//controller.
controller.switchToReceiveMode();
return app.exec();
}
@@ -1,21 +0,0 @@
Converted with ❤️ by ConvertICO.com
Tool used: convertico.com/icon-maker/
Free, no account needed - bookmark us for next time!
Conversion details:
Original file : nearby-linux-desktop.png
Converted on : 2026-03-22
Sizes included : 16x16px, 32x32px, 48x48px, 64x64px, 128x128px, 256x256px
Shape : Rounded
Background : transparent
Format : PNG
Files in this archive:
*.png - Individual PNG icons at each selected size
*.ico - Multi-resolution ICO file (if ICO format selected)
How to use your ICO as a favicon:
<link rel="icon" href="/favicon.ico" type="image/x-icon">
Need help? convertico.com/contact/
Convert more images at ConvertICO.com
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

@@ -1,159 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
MODE="user"
PREFIX=""
usage() {
cat <<'USAGE'
Usage: ./install_nearby_file_share.sh [options]
Install Nearby File Share app artifacts from an extracted release bundle.
Options:
--user Install under $HOME/.local (default)
--system Install under /usr/local (uses sudo when needed)
--prefix DIR Custom install prefix (overrides --user/--system default)
-h, --help Show this help
Examples:
./install_nearby_file_share.sh
./install_nearby_file_share.sh --system
./install_nearby_file_share.sh --prefix "$HOME/.local"
USAGE
}
nearest_existing_parent() {
local path="$1"
while [[ ! -e "$path" ]]; do
path="$(dirname "$path")"
done
printf '%s\n' "$path"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--user)
MODE="user"
shift
;;
--system)
MODE="system"
shift
;;
--prefix)
PREFIX="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage
exit 1
;;
esac
done
if [[ -z "$PREFIX" ]]; then
if [[ "$MODE" == "system" ]]; then
PREFIX="/usr/local"
else
PREFIX="$HOME/.local"
fi
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BIN_SRC="$SCRIPT_DIR/bin/nearby_qml_file_tray_app"
LIB_SRC="$SCRIPT_DIR/lib/libnearby_sharing_api_shared.so"
DESKTOP_SRC="$SCRIPT_DIR/share/applications/nearby-file-share.desktop"
ICON_SRC_STAGED="$SCRIPT_DIR/share/icons/hicolor/256x256/apps/nearby-file-share.png"
ICON_SRC_FALLBACK="$SCRIPT_DIR/nearby-linux-desktop.png"
ICON_SRC="$ICON_SRC_STAGED"
if [[ ! -f "$ICON_SRC" && -f "$ICON_SRC_FALLBACK" ]]; then
ICON_SRC="$ICON_SRC_FALLBACK"
fi
HEADER_SRC="$SCRIPT_DIR/include/sharing/linux/nearby_sharing_api.h"
for required in "$BIN_SRC" "$LIB_SRC" "$DESKTOP_SRC" "$ICON_SRC"; do
if [[ ! -f "$required" ]]; then
echo "Missing required bundle artifact: $required" >&2
echo "Run this installer from the extracted release bundle root." >&2
exit 1
fi
done
BINDIR="$PREFIX/bin"
LIBDIR="$PREFIX/lib"
INCLUDEDIR="$PREFIX/include/sharing/linux"
DESKTOP_DIR="$PREFIX/share/applications"
ICON_DIR="$PREFIX/share/icons/hicolor/256x256/apps"
NEEDS_ELEVATION=0
for path in "$BINDIR" "$LIBDIR" "$DESKTOP_DIR" "$ICON_DIR"; do
parent="$(nearest_existing_parent "$path")"
if [[ ! -w "$parent" ]]; then
NEEDS_ELEVATION=1
break
fi
done
INSTALL_PREFIX=()
if [[ "$NEEDS_ELEVATION" -eq 1 && "$(id -u)" -ne 0 ]]; then
if ! command -v sudo >/dev/null 2>&1; then
echo "Install requires elevated privileges, but sudo is not available." >&2
exit 1
fi
INSTALL_PREFIX=(sudo)
fi
TMP_DESKTOP="$(mktemp)"
trap 'rm -f "$TMP_DESKTOP"' EXIT
sed \
-e "s|^Exec=.*|Exec=${BINDIR}/nearby_qml_file_tray_app|" \
-e "s|^Icon=.*|Icon=${ICON_DIR}/nearby-file-share.png|" \
"$DESKTOP_SRC" > "$TMP_DESKTOP"
echo "[1/5] Installing application binary"
"${INSTALL_PREFIX[@]}" install -d "$BINDIR"
"${INSTALL_PREFIX[@]}" install -m 0755 "$BIN_SRC" "$BINDIR/"
echo "[2/5] Installing shared library"
"${INSTALL_PREFIX[@]}" install -d "$LIBDIR"
"${INSTALL_PREFIX[@]}" install -m 0755 "$LIB_SRC" "$LIBDIR/"
if [[ -f "$HEADER_SRC" ]]; then
echo "[3/5] Installing public header"
"${INSTALL_PREFIX[@]}" install -d "$INCLUDEDIR"
"${INSTALL_PREFIX[@]}" install -m 0644 "$HEADER_SRC" "$INCLUDEDIR/"
else
echo "[3/5] Header not present in bundle; skipping"
fi
echo "[4/5] Installing desktop entry and icon"
"${INSTALL_PREFIX[@]}" install -d "$DESKTOP_DIR"
"${INSTALL_PREFIX[@]}" install -d "$ICON_DIR"
"${INSTALL_PREFIX[@]}" install -m 0644 "$TMP_DESKTOP" "$DESKTOP_DIR/nearby-file-share.desktop"
"${INSTALL_PREFIX[@]}" install -m 0644 "$ICON_SRC" "$ICON_DIR/nearby-file-share.png"
echo "[5/5] Refreshing desktop/system caches"
if command -v update-desktop-database >/dev/null 2>&1; then
"${INSTALL_PREFIX[@]}" update-desktop-database "$DESKTOP_DIR" || true
fi
if command -v ldconfig >/dev/null 2>&1 && [[ "$PREFIX" == "/usr" || "$PREFIX" == "/usr/local" ]]; then
"${INSTALL_PREFIX[@]}" ldconfig || true
fi
echo "Installed Nearby File Share:"
echo " binary : $BINDIR/nearby_qml_file_tray_app"
echo " library: $LIBDIR/libnearby_sharing_api_shared.so"
echo " desktop: $DESKTOP_DIR/nearby-file-share.desktop"
echo " icon : $ICON_DIR/nearby-file-share.png"
if [[ -f "$HEADER_SRC" ]]; then
echo " header : $INCLUDEDIR/nearby_sharing_api.h"
fi
@@ -1,10 +0,0 @@
[Desktop Entry]
Type=Application
Name=Nearby File Share
Comment=Share files with nearby devices using Nearby Connections
Exec=nearby_qml_file_tray_app
Icon=nearby-file-share
Categories=Utility;Network;FileTransfer;
Keywords=share;file;nearby;transfer;
StartupNotify=false
Terminal=false
Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

@@ -1,210 +0,0 @@
#include "notification_manager.h"
#include <QAbstractButton>
#include <QClipboard>
#include <QCoreApplication>
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusReply>
#include <QDir>
#include <QFileInfo>
#include <QGuiApplication>
#include <QIcon>
#include <QMessageBox>
#include <QPixmap>
#include <QPushButton>
#include <QStandardPaths>
#include <QSystemTrayIcon>
#include <QVariantMap>
namespace {
constexpr char kNotificationsService[] = "org.freedesktop.Notifications";
constexpr char kNotificationsPath[] = "/org/freedesktop/Notifications";
constexpr char kNotificationsInterface[] = "org.freedesktop.Notifications";
constexpr char kCopyActionId[] = "copy_value";
constexpr char kDesktopEntryId[] = "nearby-file-share";
} // namespace
NotificationManager::NotificationManager(QSystemTrayIcon* tray_icon,
QObject* parent)
: QObject(parent), tray_icon_(tray_icon) {
QDBusConnection session_bus = QDBusConnection::sessionBus();
if (!session_bus.isConnected()) {
return;
}
session_bus.connect(QString::fromLatin1(kNotificationsService),
QString::fromLatin1(kNotificationsPath),
QString::fromLatin1(kNotificationsInterface),
QStringLiteral("ActionInvoked"), this,
SLOT(OnActionInvoked(uint,QString)));
session_bus.connect(QString::fromLatin1(kNotificationsService),
QString::fromLatin1(kNotificationsPath),
QString::fromLatin1(kNotificationsInterface),
QStringLiteral("NotificationClosed"), this,
SLOT(OnNotificationClosed(uint,uint)));
QDBusInterface notification_interface(
QString::fromLatin1(kNotificationsService),
QString::fromLatin1(kNotificationsPath),
QString::fromLatin1(kNotificationsInterface), session_bus);
QDBusReply<QStringList> capabilities_reply =
notification_interface.call(QStringLiteral("GetCapabilities"));
if (capabilities_reply.isValid()) {
supports_actions_ =
capabilities_reply.value().contains(QStringLiteral("actions"));
}
}
void NotificationManager::ShowNotification(const QString& title,
const QString& body) {
if (tray_icon_ != nullptr) {
tray_icon_->showMessage(title, body, QSystemTrayIcon::Information, 4000);
}
}
void NotificationManager::ShowCopyableNotification(
const QString& title, const QString& body, const QString& text_to_copy,
const QString& action_label) {
const QString trimmed_text = text_to_copy.trimmed();
const QString trimmed_action_label = action_label.trimmed().isEmpty()
? QStringLiteral("Copy")
: action_label.trimmed();
if (trimmed_text.isEmpty()) {
ShowNotification(title, body);
return;
}
if (supports_actions_) {
QDBusInterface notification_interface(
QString::fromLatin1(kNotificationsService),
QString::fromLatin1(kNotificationsPath),
QString::fromLatin1(kNotificationsInterface),
QDBusConnection::sessionBus());
const QString application_name = QCoreApplication::applicationName();
const QString notification_icon = EnsureNotificationIconPath();
QVariantMap hints{{QStringLiteral("desktop-entry"),
QString::fromLatin1(kDesktopEntryId)}};
if (!notification_icon.isEmpty()) {
hints.insert(QStringLiteral("image-path"), notification_icon);
}
QDBusReply<uint> reply = notification_interface.call(
QStringLiteral("Notify"), application_name,
static_cast<uint>(0),
notification_icon.isEmpty() ? QString::fromLatin1(kDesktopEntryId)
: notification_icon,
title, body,
QStringList{QString::fromLatin1(kCopyActionId), trimmed_action_label},
hints, 8000);
if (reply.isValid()) {
copy_actions_.insert(reply.value(), CopyActionState{trimmed_text});
return;
}
}
ShowFallbackDialog(title, body, trimmed_text, trimmed_text,
trimmed_action_label);
}
void NotificationManager::OnActionInvoked(uint notification_id,
const QString& action_key) {
if (action_key != QString::fromLatin1(kCopyActionId)) {
return;
}
auto it = copy_actions_.find(notification_id);
if (it == copy_actions_.end()) {
return;
}
CopyTextToClipboard(it->text_to_copy, QStringLiteral("Copied"),
QStringLiteral("Copied to clipboard."));
copy_actions_.erase(it);
}
void NotificationManager::OnNotificationClosed(uint notification_id,
uint reason) {
Q_UNUSED(reason);
copy_actions_.remove(notification_id);
}
void NotificationManager::CopyTextToClipboard(
const QString& text_to_copy, const QString& confirmation_title,
const QString& confirmation_body) const {
QClipboard* clipboard = QGuiApplication::clipboard();
if (clipboard != nullptr) {
clipboard->setText(text_to_copy);
}
if (tray_icon_ != nullptr) {
tray_icon_->showMessage(confirmation_title, confirmation_body,
QSystemTrayIcon::Information, 2500);
}
}
QString NotificationManager::EnsureNotificationIconPath() {
if (!notification_icon_path_.isEmpty() &&
QFileInfo::exists(notification_icon_path_)) {
return notification_icon_path_;
}
if (tray_icon_ == nullptr || tray_icon_->icon().isNull()) {
return {};
}
QString cache_dir =
QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
if (cache_dir.isEmpty()) {
cache_dir = QDir::tempPath() + QStringLiteral("/nearby-file-share");
}
QDir dir(cache_dir);
if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) {
return {};
}
const QString icon_path = dir.filePath(QStringLiteral("notification-icon.png"));
const QPixmap icon_pixmap = tray_icon_->icon().pixmap(128, 128);
if (icon_pixmap.isNull() || !icon_pixmap.save(icon_path, "PNG")) {
return {};
}
notification_icon_path_ = icon_path;
return notification_icon_path_;
}
void NotificationManager::ShowFallbackDialog(const QString& title,
const QString& body,
const QString& informative_text,
const QString& text_to_copy,
const QString& action_label) {
auto* message_box =
new QMessageBox(QMessageBox::Information, title, body, QMessageBox::NoButton);
message_box->setAttribute(Qt::WA_DeleteOnClose);
message_box->setTextFormat(Qt::PlainText);
message_box->setInformativeText(informative_text);
message_box->setWindowFlag(Qt::WindowStaysOnTopHint);
if (tray_icon_ != nullptr && !tray_icon_->icon().isNull()) {
message_box->setWindowIcon(tray_icon_->icon());
}
QAbstractButton* copy_button =
message_box->addButton(action_label, QMessageBox::ActionRole);
message_box->addButton(QMessageBox::Close);
QObject::connect(message_box, &QMessageBox::buttonClicked, message_box,
[this, message_box, copy_button, text_to_copy](
QAbstractButton* button) {
if (button == copy_button) {
CopyTextToClipboard(text_to_copy, QStringLiteral("Copied"),
QStringLiteral("Copied to clipboard."));
}
message_box->close();
});
message_box->show();
message_box->raise();
message_box->activateWindow();
}
@@ -1,47 +0,0 @@
#ifndef SHARING_LINUX_QML_TRAY_APP_NOTIFICATION_MANAGER_H_
#define SHARING_LINUX_QML_TRAY_APP_NOTIFICATION_MANAGER_H_
#include <QObject>
#include <QHash>
#include <QString>
class QSystemTrayIcon;
class NotificationManager : public QObject {
Q_OBJECT
public:
explicit NotificationManager(QSystemTrayIcon* tray_icon,
QObject* parent = nullptr);
void ShowNotification(const QString& title, const QString& body);
void ShowCopyableNotification(const QString& title, const QString& body,
const QString& text_to_copy,
const QString& action_label);
private slots:
void OnActionInvoked(uint notification_id, const QString& action_key);
void OnNotificationClosed(uint notification_id, uint reason);
private:
struct CopyActionState {
QString text_to_copy;
};
void CopyTextToClipboard(const QString& text_to_copy,
const QString& confirmation_title,
const QString& confirmation_body) const;
QString EnsureNotificationIconPath();
void ShowFallbackDialog(const QString& title, const QString& body,
const QString& informative_text,
const QString& text_to_copy,
const QString& action_label);
bool supports_actions_ = false;
QHash<uint, CopyActionState> copy_actions_;
QString notification_icon_path_;
QSystemTrayIcon* tray_icon_ = nullptr;
};
#endif // SHARING_LINUX_QML_TRAY_APP_NOTIFICATION_MANAGER_H_
@@ -1,48 +0,0 @@
#include "qr_code_generator.h"
#include <QByteArray>
#include "third_party/libqrencode/qrencode_compat.h"
namespace QrCodeGenerator {
QrCodeData GenerateQrCode(const QString& url) {
QrCodeData result;
result.size = 0;
const QByteArray encoded_url = url.trimmed().toUtf8();
if (encoded_url.isEmpty()) {
return result;
}
QRcode* qr_code = QRcode_encodeData(
encoded_url.size(),
reinterpret_cast<const unsigned char*>(encoded_url.constData()), 0,
QR_ECLEVEL_M);
if (qr_code == nullptr || qr_code->data == nullptr || qr_code->width <= 0) {
if (qr_code != nullptr) {
QRcode_free(qr_code);
}
return result;
}
result.size = qr_code->width;
result.rows.reserve(result.size);
for (int row = 0; row < result.size; ++row) {
QString row_data;
row_data.reserve(result.size);
for (int col = 0; col < result.size; ++col) {
const unsigned char module =
qr_code->data[row * result.size + col] & 0x1;
row_data.append(module ? QLatin1Char('1') : QLatin1Char('0'));
}
result.rows.append(row_data);
}
QRcode_free(qr_code);
return result;
}
} // namespace QrCodeGenerator
@@ -1,18 +0,0 @@
#ifndef SHARING_LINUX_QML_TRAY_APP_QR_CODE_GENERATOR_H_
#define SHARING_LINUX_QML_TRAY_APP_QR_CODE_GENERATOR_H_
#include <QString>
#include <QStringList>
namespace QrCodeGenerator {
struct QrCodeData {
QStringList rows;
int size;
};
QrCodeData GenerateQrCode(const QString& url);
} // namespace QrCodeGenerator
#endif // SHARING_LINUX_QML_TRAY_APP_QR_CODE_GENERATOR_H_
@@ -1,15 +0,0 @@
<RCC>
<qresource prefix="/qml">
<file>FileShareTray.qml</file>
<file>components/AppHeader.qml</file>
<file>components/SideBar.qml</file>
<file>components/AnimatedBlob.qml</file>
<file>components/SendUrlPanel.qml</file>
<file>components/DeviceCard.qml</file>
<file>components/SettingsPanel.qml</file>
</qresource>
<qresource prefix="/icons">
<file>tray_icon.png</file>
<file alias="tray_icon-symbolic.svg">tray_icon-symbolic.svg</file>
</qresource>
</RCC>
@@ -1,30 +0,0 @@
#ifndef SHARING_LINUX_QML_TRAY_APP_SHARE_TARGET_MODEL_H_
#define SHARING_LINUX_QML_TRAY_APP_SHARE_TARGET_MODEL_H_
#include <QString>
#include <QVariantMap>
class ShareTarget {
public:
explicit ShareTarget(qlonglong id, const QString& name, bool is_incoming)
: id_(id), name_(name), is_incoming_(is_incoming) {}
qlonglong id() const { return id_; }
QString name() const { return name_; }
bool isIncoming() const { return is_incoming_; }
QVariantMap toVariantMap() const {
QVariantMap map;
map[QStringLiteral("id")] = id_;
map[QStringLiteral("name")] = name_;
map[QStringLiteral("isIncoming")] = is_incoming_;
return map;
}
private:
qlonglong id_;
QString name_;
bool is_incoming_;
};
#endif // SHARING_LINUX_QML_TRAY_APP_SHARE_TARGET_MODEL_H_
@@ -1,44 +0,0 @@
#include "status_mapper.h"
namespace StatusMapper {
QString TransferStatusToString(NearbySharingApi::TransferStatus status) {
return QString::fromStdString(NearbySharingApi::TransferStatusToString(status));
}
QString ApiStatusToString(NearbySharingApi::StatusCode status) {
return QString::fromStdString(NearbySharingApi::StatusCodeToString(status));
}
bool IsActiveTransferStatus(const QString& status) {
return status == QStringLiteral("Queued") ||
status == QStringLiteral("Connecting") ||
status == QStringLiteral("AwaitingLocalConfirmation") ||
status == QStringLiteral("AwaitingRemoteAcceptance") ||
status == QStringLiteral("InProgress");
}
bool IsFinalTransferStatus(NearbySharingApi::TransferStatus status) {
switch (status) {
case NearbySharingApi::TransferStatus::kComplete:
case NearbySharingApi::TransferStatus::kFailed:
case NearbySharingApi::TransferStatus::kRejected:
case NearbySharingApi::TransferStatus::kCancelled:
case NearbySharingApi::TransferStatus::kTimedOut:
case NearbySharingApi::TransferStatus::kMediaUnavailable:
case NearbySharingApi::TransferStatus::kNotEnoughSpace:
case NearbySharingApi::TransferStatus::kUnsupportedAttachmentType:
case NearbySharingApi::TransferStatus::kDeviceAuthenticationFailed:
case NearbySharingApi::TransferStatus::kIncompletePayloads:
return true;
case NearbySharingApi::TransferStatus::kUnknown:
case NearbySharingApi::TransferStatus::kConnecting:
case NearbySharingApi::TransferStatus::kAwaitingLocalConfirmation:
case NearbySharingApi::TransferStatus::kAwaitingRemoteAcceptance:
case NearbySharingApi::TransferStatus::kInProgress:
return false;
}
return false;
}
} // namespace StatusMapper
@@ -1,21 +0,0 @@
#ifndef SHARING_LINUX_QML_TRAY_APP_STATUS_MAPPER_H_
#define SHARING_LINUX_QML_TRAY_APP_STATUS_MAPPER_H_
#include <QString>
#include "sharing/linux/nearby_sharing_api.h"
using NearbySharingApi = nearby::sharing::NearbySharingApi;
namespace StatusMapper {
QString TransferStatusToString(NearbySharingApi::TransferStatus status);
QString ApiStatusToString(NearbySharingApi::StatusCode status);
bool IsActiveTransferStatus(const QString& status);
bool IsFinalTransferStatus(NearbySharingApi::TransferStatus status);
} // namespace StatusMapper
#endif // SHARING_LINUX_QML_TRAY_APP_STATUS_MAPPER_H_
@@ -1,18 +0,0 @@
#include "string_utils.h"
namespace StringUtils {
QString TrimmedOrFallback(const QString& value, const QString& fallback) {
const QString trimmed = value.trimmed();
return trimmed.isEmpty() ? fallback : trimmed;
}
QString FromStdString(const std::string& value) {
return QString::fromStdString(value);
}
QString TrimmedFromStdString(const std::string& value) {
return QString::fromStdString(value).trimmed();
}
} // namespace StringUtils
-17
View File
@@ -1,17 +0,0 @@
#ifndef SHARING_LINUX_QML_TRAY_APP_STRING_UTILS_H_
#define SHARING_LINUX_QML_TRAY_APP_STRING_UTILS_H_
#include <QString>
#include <string>
namespace StringUtils {
QString TrimmedOrFallback(const QString& value, const QString& fallback);
QString FromStdString(const std::string& value);
QString TrimmedFromStdString(const std::string& value);
} // namespace StringUtils
#endif // SHARING_LINUX_QML_TRAY_APP_STRING_UTILS_H_
@@ -1,32 +0,0 @@
#ifndef SHARING_LINUX_QML_TRAY_APP_THIRD_PARTY_LIBQRENCODE_QRENCODE_COMPAT_H_
#define SHARING_LINUX_QML_TRAY_APP_THIRD_PARTY_LIBQRENCODE_QRENCODE_COMPAT_H_
// Minimal libqrencode ABI used by this app when the runtime shared library is
// available but the development headers are not installed.
#if defined(__cplusplus)
extern "C" {
#endif
typedef enum {
QR_ECLEVEL_L = 0,
QR_ECLEVEL_M,
QR_ECLEVEL_Q,
QR_ECLEVEL_H
} QRecLevel;
typedef struct {
int version;
int width;
unsigned char* data;
} QRcode;
QRcode* QRcode_encodeData(int size, const unsigned char* data, int version,
QRecLevel level);
void QRcode_free(QRcode* qrcode);
#if defined(__cplusplus)
} // extern "C"
#endif
#endif // SHARING_LINUX_QML_TRAY_APP_THIRD_PARTY_LIBQRENCODE_QRENCODE_COMPAT_H_
@@ -1,50 +0,0 @@
#ifndef SHARING_LINUX_QML_TRAY_APP_TRANSFER_MODEL_H_
#define SHARING_LINUX_QML_TRAY_APP_TRANSFER_MODEL_H_
#include <QString>
#include <QVariantMap>
class Transfer {
public:
Transfer(qlonglong target_id, const QString& target_name,
const QString& status, double progress, qulonglong transferred_bytes,
const QString& direction, const QString& file_name)
: target_id_(target_id),
target_name_(target_name),
status_(status),
progress_(progress),
transferred_bytes_(transferred_bytes),
direction_(direction),
file_name_(file_name) {}
qlonglong targetId() const { return target_id_; }
QString targetName() const { return target_name_; }
QString status() const { return status_; }
double progress() const { return progress_; }
qulonglong transferredBytes() const { return transferred_bytes_; }
QString direction() const { return direction_; }
QString fileName() const { return file_name_; }
QVariantMap toVariantMap() const {
QVariantMap map;
map[QStringLiteral("targetId")] = target_id_;
map[QStringLiteral("targetName")] = target_name_;
map[QStringLiteral("status")] = status_;
map[QStringLiteral("progress")] = progress_;
map[QStringLiteral("transferredBytes")] = static_cast<qulonglong>(transferred_bytes_);
map[QStringLiteral("direction")] = direction_;
map[QStringLiteral("fileName")] = file_name_;
return map;
}
private:
qlonglong target_id_;
QString target_name_;
QString status_;
double progress_;
qulonglong transferred_bytes_;
QString direction_;
QString file_name_;
};
#endif // SHARING_LINUX_QML_TRAY_APP_TRANSFER_MODEL_H_
@@ -1,14 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>

Before

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB