2026-04-24 20:20:24 +08:00
|
|
|
import QtQuick
|
|
|
|
|
import QtQuick.Controls
|
|
|
|
|
import QtQuick.Layouts
|
2026-04-27 15:06:59 +08:00
|
|
|
import QtQuick.Dialogs
|
2026-04-24 20:20:24 +08:00
|
|
|
|
|
|
|
|
ApplicationWindow {
|
|
|
|
|
id: root
|
|
|
|
|
visible: true
|
|
|
|
|
width: 800
|
|
|
|
|
height: 600
|
|
|
|
|
title: qsTr("LocalSend")
|
|
|
|
|
|
2026-04-27 15:06:59 +08:00
|
|
|
property string currentSessionId: ""
|
|
|
|
|
property var currentFiles: []
|
|
|
|
|
property string currentSenderAlias: ""
|
|
|
|
|
property string currentSenderIp: ""
|
|
|
|
|
property var receiveProgress: ({})
|
|
|
|
|
|
2026-04-24 20:20:24 +08:00
|
|
|
StackView {
|
|
|
|
|
id: stackView
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
|
|
|
|
Component.onCompleted: {
|
|
|
|
|
push(homePageComponent)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 15:06:59 +08:00
|
|
|
Dialog {
|
|
|
|
|
id: receiveDialog
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
modal: true
|
|
|
|
|
closePolicy: Popup.NoAutoClose
|
|
|
|
|
standardButtons: Dialog.Ok | Dialog.Cancel
|
|
|
|
|
title: qsTr("Receive Files")
|
|
|
|
|
|
|
|
|
|
onAccepted: {
|
|
|
|
|
appController.acceptReceive(currentSessionId)
|
|
|
|
|
currentSessionId = ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onRejected: {
|
|
|
|
|
appController.declineReceive(currentSessionId)
|
|
|
|
|
currentSessionId = ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ColumnLayout {
|
|
|
|
|
spacing: 12
|
|
|
|
|
|
|
|
|
|
Label {
|
|
|
|
|
text: qsTr("From: %1 (%2)").arg(currentSenderAlias).arg(currentSenderIp)
|
|
|
|
|
font.bold: true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Label {
|
|
|
|
|
text: qsTr("Files:")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ListView {
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
Layout.preferredHeight: Math.min(200, contentHeight)
|
|
|
|
|
width: 400
|
|
|
|
|
model: currentFiles
|
|
|
|
|
spacing: 4
|
|
|
|
|
|
|
|
|
|
delegate: RowLayout {
|
|
|
|
|
width: ListView.view.width
|
|
|
|
|
spacing: 8
|
|
|
|
|
|
|
|
|
|
Label {
|
|
|
|
|
text: modelData.fileName
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
elide: Text.ElideMiddle
|
|
|
|
|
}
|
|
|
|
|
Label {
|
|
|
|
|
text: formatSize(modelData.size)
|
|
|
|
|
color: "gray"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Label {
|
|
|
|
|
text: qsTr("Save to: %1").arg(appController.downloadPath)
|
|
|
|
|
color: palette.mid
|
|
|
|
|
font.pixelSize: 12
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Dialog {
|
|
|
|
|
id: progressDialog
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
modal: true
|
|
|
|
|
closePolicy: Popup.NoAutoClose
|
|
|
|
|
title: qsTr("Receiving Files")
|
|
|
|
|
|
|
|
|
|
ColumnLayout {
|
|
|
|
|
spacing: 12
|
|
|
|
|
|
|
|
|
|
Label {
|
|
|
|
|
id: progressLabel
|
|
|
|
|
text: qsTr("Receiving from %1...").arg(currentSenderAlias)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ProgressBar {
|
|
|
|
|
id: totalProgressBar
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
from: 0
|
|
|
|
|
to: 100
|
|
|
|
|
value: calculateTotalProgress()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Label {
|
|
|
|
|
text: qsTr("%1% complete").arg(Math.round(totalProgressBar.value))
|
|
|
|
|
color: "gray"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
property var progressData: ({})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Connections {
|
|
|
|
|
target: appController
|
|
|
|
|
|
|
|
|
|
function onReceiveRequest(sessionId, senderAlias, senderIp, files) {
|
|
|
|
|
currentSessionId = sessionId
|
|
|
|
|
currentSenderAlias = senderAlias
|
|
|
|
|
currentSenderIp = senderIp
|
|
|
|
|
currentFiles = files
|
|
|
|
|
|
|
|
|
|
if (appController.quickSave) {
|
|
|
|
|
appController.acceptReceive(sessionId)
|
|
|
|
|
} else {
|
|
|
|
|
receiveDialog.open()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onReceiveProgress(sessionId, fileId, progress) {
|
|
|
|
|
if (sessionId === currentSessionId) {
|
|
|
|
|
receiveProgress[fileId] = progress
|
|
|
|
|
receiveProgress = Object.assign({}, receiveProgress)
|
|
|
|
|
progressDialog.progressData = receiveProgress
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onReceiveCompleted(sessionId) {
|
|
|
|
|
if (sessionId === currentSessionId) {
|
|
|
|
|
progressDialog.close()
|
|
|
|
|
receiveProgress = {}
|
|
|
|
|
currentSessionId = ""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onReceiveError(sessionId, error) {
|
|
|
|
|
if (sessionId === currentSessionId) {
|
|
|
|
|
progressDialog.close()
|
|
|
|
|
errorDialog.text = error
|
|
|
|
|
errorDialog.open()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Dialog {
|
|
|
|
|
id: errorDialog
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
modal: true
|
|
|
|
|
standardButtons: Dialog.Ok
|
|
|
|
|
title: qsTr("Error")
|
|
|
|
|
property alias text: errorLabel.text
|
|
|
|
|
|
|
|
|
|
Label {
|
|
|
|
|
id: errorLabel
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatSize(bytes) {
|
|
|
|
|
if (bytes < 1024) return bytes + " B"
|
|
|
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"
|
|
|
|
|
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + " MB"
|
|
|
|
|
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function calculateTotalProgress() {
|
|
|
|
|
if (!currentFiles || currentFiles.length === 0) return 0
|
|
|
|
|
var total = 0
|
|
|
|
|
var count = 0
|
|
|
|
|
for (var i = 0; i < currentFiles.length; i++) {
|
|
|
|
|
var fileId = currentFiles[i].id
|
|
|
|
|
if (receiveProgress[fileId] !== undefined) {
|
|
|
|
|
total += receiveProgress[fileId] * 100
|
|
|
|
|
count++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return count > 0 ? total / currentFiles.length : 0
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 20:20:24 +08:00
|
|
|
Component {
|
|
|
|
|
id: homePageComponent
|
|
|
|
|
Page {
|
|
|
|
|
id: homePage
|
|
|
|
|
|
|
|
|
|
signal openSettings()
|
|
|
|
|
|
|
|
|
|
header: ToolBar {
|
|
|
|
|
RowLayout {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
Label {
|
|
|
|
|
text: qsTr("LocalSend")
|
|
|
|
|
font.bold: true
|
|
|
|
|
font.pixelSize: 20
|
|
|
|
|
Layout.leftMargin: 16
|
|
|
|
|
}
|
|
|
|
|
Item { Layout.fillWidth: true }
|
|
|
|
|
ToolButton {
|
|
|
|
|
text: qsTr("Settings")
|
|
|
|
|
onClicked: homePage.openSettings()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ColumnLayout {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
anchors.margins: 16
|
|
|
|
|
spacing: 16
|
|
|
|
|
|
|
|
|
|
Label {
|
|
|
|
|
text: qsTr("Nearby Devices")
|
|
|
|
|
font.bold: true
|
|
|
|
|
font.pixelSize: 16
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ListView {
|
|
|
|
|
id: deviceListView
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
Layout.fillHeight: true
|
|
|
|
|
|
|
|
|
|
property var devices: appController.devices
|
|
|
|
|
model: devices
|
|
|
|
|
spacing: 8
|
|
|
|
|
|
|
|
|
|
delegate: Pane {
|
|
|
|
|
width: ListView.view.width
|
|
|
|
|
padding: 12
|
|
|
|
|
|
|
|
|
|
background: Rectangle {
|
|
|
|
|
color: Qt.lighter("gray", 1.8)
|
|
|
|
|
radius: 8
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RowLayout {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
|
|
|
|
Column {
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
Label {
|
|
|
|
|
text: modelData.alias || modelData.ip
|
|
|
|
|
font.bold: true
|
|
|
|
|
}
|
|
|
|
|
Label {
|
|
|
|
|
text: "%1:%2".arg(modelData.ip).arg(modelData.port)
|
|
|
|
|
color: "gray"
|
|
|
|
|
font.pixelSize: 12
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Button {
|
|
|
|
|
text: qsTr("Send")
|
|
|
|
|
onClicked: {
|
2026-04-27 15:06:59 +08:00
|
|
|
// TODO: send to this device
|
2026-04-24 20:20:24 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RowLayout {
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
Button {
|
|
|
|
|
text: qsTr("Refresh")
|
|
|
|
|
onClicked: appController.refreshDevices()
|
|
|
|
|
}
|
|
|
|
|
Item { Layout.fillWidth: true }
|
|
|
|
|
Label {
|
|
|
|
|
text: qsTr("Alias: %1").arg(appController.alias)
|
2026-04-27 15:06:59 +08:00
|
|
|
color: palette.mid
|
2026-04-24 20:20:24 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onOpenSettings: stackView.push(settingsPageComponent)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Component {
|
|
|
|
|
id: settingsPageComponent
|
|
|
|
|
Page {
|
|
|
|
|
id: settingsPage
|
|
|
|
|
signal back()
|
|
|
|
|
|
|
|
|
|
header: ToolBar {
|
|
|
|
|
RowLayout {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
ToolButton {
|
|
|
|
|
text: qsTr("Back")
|
|
|
|
|
onClicked: settingsPage.back()
|
|
|
|
|
}
|
|
|
|
|
Label {
|
|
|
|
|
text: qsTr("Settings")
|
|
|
|
|
font.bold: true
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
Layout.leftMargin: 16
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ColumnLayout {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
anchors.margins: 16
|
|
|
|
|
spacing: 16
|
|
|
|
|
|
|
|
|
|
GridLayout {
|
|
|
|
|
columns: 2
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
|
|
|
|
Label { text: qsTr("Device Alias:") }
|
|
|
|
|
TextField {
|
|
|
|
|
id: aliasField
|
|
|
|
|
text: appController.alias
|
|
|
|
|
onEditingFinished: appController.alias = text
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Label { text: qsTr("Port:") }
|
|
|
|
|
SpinBox {
|
|
|
|
|
id: portField
|
|
|
|
|
value: appController.port
|
|
|
|
|
from: 1
|
|
|
|
|
to: 65535
|
|
|
|
|
onValueChanged: appController.port = value
|
|
|
|
|
}
|
2026-04-27 15:06:59 +08:00
|
|
|
|
|
|
|
|
Label { text: qsTr("Download Path:") }
|
|
|
|
|
RowLayout {
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
TextField {
|
|
|
|
|
id: downloadPathField
|
|
|
|
|
text: appController.downloadPath
|
|
|
|
|
onEditingFinished: appController.downloadPath = text
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
}
|
|
|
|
|
Button {
|
|
|
|
|
text: qsTr("Browse")
|
|
|
|
|
onClicked: folderDialog.open()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Label { text: qsTr("Quick Save:") }
|
|
|
|
|
CheckBox {
|
|
|
|
|
id: quickSaveCheck
|
|
|
|
|
checked: appController.quickSave
|
|
|
|
|
onCheckedChanged: appController.quickSave = checked
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FolderDialog {
|
|
|
|
|
id: folderDialog
|
|
|
|
|
onAccepted: {
|
|
|
|
|
downloadPathField.text = selectedFolder.toString().replace("file://", "")
|
|
|
|
|
appController.downloadPath = downloadPathField.text
|
|
|
|
|
}
|
2026-04-24 20:20:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Item { Layout.fillHeight: true }
|
|
|
|
|
|
|
|
|
|
Label {
|
|
|
|
|
text: qsTr("Server Status: %1").arg(appController.serverRunning ? qsTr("Running") : qsTr("Stopped"))
|
|
|
|
|
color: appController.serverRunning ? "green" : "red"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onBack: stackView.pop()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|