removed qml tray app
@@ -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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
¬ification_manager, &NotificationManager::ShowNotification);
|
||||
QObject::connect(&controller,
|
||||
&FileShareTrayController::requestCopyLinkTrayMessage,
|
||||
¬ification_manager,
|
||||
[¬ification_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
|
||||
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 643 B |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
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
|
||||
|
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
|
||||
@@ -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 |
|
Before Width: | Height: | Size: 2.0 KiB |