Files
localsend-qt/src/app/qml/main.qml

484 lines
15 KiB
QML
Raw Normal View History

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-27 16:08:40 +08:00
property string selectedDeviceFingerprint: ""
2026-04-27 15:06:59 +08:00
2026-04-24 20:20:24 +08:00
StackView {
id: stackView
anchors.fill: parent
Component.onCompleted: {
push(homePageComponent)
}
}
2026-04-27 16:08:40 +08:00
FileDialog {
id: fileDialog
title: qsTr("Select Files to Send")
fileMode: FileDialog.OpenFiles
onAccepted: {
var paths = []
for (var i = 0; i < selectedFiles.length; i++) {
paths.push(selectedFiles[i].toString().replace("file://", ""))
}
if (paths.length > 0 && selectedDeviceFingerprint !== "") {
appController.sendFiles(selectedDeviceFingerprint, paths)
}
}
}
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)
2026-04-27 16:08:40 +08:00
color: palette.mid
2026-04-27 15:06:59 +08:00
}
}
}
Label {
text: qsTr("Save to: %1").arg(appController.downloadPath)
color: palette.mid
font.pixelSize: 12
}
}
}
Dialog {
2026-04-27 16:08:40 +08:00
id: receiveProgressDialog
2026-04-27 15:06:59 +08:00
anchors.centerIn: parent
modal: true
closePolicy: Popup.NoAutoClose
title: qsTr("Receiving Files")
ColumnLayout {
spacing: 12
Label {
text: qsTr("Receiving from %1...").arg(currentSenderAlias)
}
ProgressBar {
Layout.fillWidth: true
from: 0
to: 100
value: calculateTotalProgress()
}
Label {
2026-04-27 16:08:40 +08:00
text: qsTr("%1% complete").arg(Math.round(calculateTotalProgress()))
color: palette.mid
2026-04-27 15:06:59 +08:00
}
}
property var progressData: ({})
}
2026-04-27 16:08:40 +08:00
Dialog {
id: sendProgressDialog
anchors.centerIn: parent
modal: true
closePolicy: Popup.NoAutoClose
title: qsTr("Sending Files")
ColumnLayout {
spacing: 12
Label {
text: qsTr("Sending files...")
}
ProgressBar {
Layout.fillWidth: true
from: 0
to: 100
value: appController.sendProgress
}
Label {
text: qsTr("%1% complete").arg(Math.round(appController.sendProgress))
color: palette.mid
}
}
footer: DialogButtonBox {
Button {
text: qsTr("Cancel")
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
onClicked: {
appController.cancelSend()
sendProgressDialog.close()
}
}
}
}
2026-04-27 15:06:59 +08:00
Connections {
target: appController
function onReceiveRequest(sessionId, senderAlias, senderIp, files) {
currentSessionId = sessionId
currentSenderAlias = senderAlias
currentSenderIp = senderIp
currentFiles = files
if (appController.quickSave) {
appController.acceptReceive(sessionId)
2026-04-27 16:08:40 +08:00
receiveProgressDialog.open()
2026-04-27 15:06:59 +08:00
} else {
receiveDialog.open()
}
}
function onReceiveProgress(sessionId, fileId, progress) {
if (sessionId === currentSessionId) {
receiveProgress[fileId] = progress
receiveProgress = Object.assign({}, receiveProgress)
2026-04-27 16:08:40 +08:00
receiveProgressDialog.progressData = receiveProgress
if (!receiveProgressDialog.visible) {
receiveProgressDialog.open()
}
2026-04-27 15:06:59 +08:00
}
}
function onReceiveCompleted(sessionId) {
if (sessionId === currentSessionId) {
2026-04-27 16:08:40 +08:00
receiveProgressDialog.close()
2026-04-27 15:06:59 +08:00
receiveProgress = {}
currentSessionId = ""
}
}
function onReceiveError(sessionId, error) {
if (sessionId === currentSessionId) {
2026-04-27 16:08:40 +08:00
receiveProgressDialog.close()
2026-04-27 15:06:59 +08:00
errorDialog.text = error
errorDialog.open()
}
}
2026-04-27 16:08:40 +08:00
function onSendProgress(progress) {
if (!sendProgressDialog.visible) {
sendProgressDialog.open()
}
}
function onSendCompleted(sessionId) {
sendProgressDialog.close()
successDialog.text = qsTr("Files sent successfully!")
successDialog.open()
}
function onSendError(error) {
sendProgressDialog.close()
errorDialog.text = error
errorDialog.open()
}
2026-04-27 15:06:59 +08:00
}
Dialog {
id: errorDialog
anchors.centerIn: parent
modal: true
standardButtons: Dialog.Ok
title: qsTr("Error")
property alias text: errorLabel.text
Label {
id: errorLabel
}
}
2026-04-27 16:08:40 +08:00
Dialog {
id: successDialog
anchors.centerIn: parent
modal: true
standardButtons: Dialog.Ok
title: qsTr("Success")
property alias text: successLabel.text
Label {
id: successLabel
}
}
2026-04-27 15:06:59 +08:00
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 {
2026-04-27 16:08:40 +08:00
color: "transparent"
2026-04-24 20:20:24 +08:00
radius: 8
2026-04-27 16:08:40 +08:00
border.color: palette.mid
border.width: 1
2026-04-24 20:20:24 +08:00
}
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)
2026-04-27 16:08:40 +08:00
color: palette.mid
2026-04-24 20:20:24 +08:00
font.pixelSize: 12
}
}
Button {
text: qsTr("Send")
2026-04-27 16:08:40 +08:00
enabled: !appController.sending
2026-04-24 20:20:24 +08:00
onClicked: {
2026-04-27 16:08:40 +08:00
selectedDeviceFingerprint = modelData.fingerprint
fileDialog.open()
2026-04-24 20:20:24 +08:00
}
}
}
}
2026-04-27 16:08:40 +08:00
Label {
anchors.centerIn: parent
text: qsTr("No devices found")
color: palette.mid
visible: deviceListView.count === 0
}
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()
}
}
}