import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick.Dialogs ApplicationWindow { id: root visible: true width: 800 height: 600 title: qsTr("LocalSend") property string currentSessionId: "" property var currentFiles: [] property string currentSenderAlias: "" property string currentSenderIp: "" property var receiveProgress: ({}) property string selectedDeviceFingerprint: "" StackView { id: stackView anchors.fill: parent Component.onCompleted: { push(homePageComponent) } } 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) } } } 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: palette.mid } } } Label { text: qsTr("Save to: %1").arg(appController.downloadPath) color: palette.mid font.pixelSize: 12 } } } Dialog { id: receiveProgressDialog 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 { text: qsTr("%1% complete").arg(Math.round(calculateTotalProgress())) color: palette.mid } } property var progressData: ({}) } 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() } } } } Connections { target: appController function onReceiveRequest(sessionId, senderAlias, senderIp, files) { currentSessionId = sessionId currentSenderAlias = senderAlias currentSenderIp = senderIp currentFiles = files if (appController.quickSave) { appController.acceptReceive(sessionId) receiveProgressDialog.open() } else { receiveDialog.open() } } function onReceiveProgress(sessionId, fileId, progress) { if (sessionId === currentSessionId) { receiveProgress[fileId] = progress receiveProgress = Object.assign({}, receiveProgress) receiveProgressDialog.progressData = receiveProgress if (!receiveProgressDialog.visible) { receiveProgressDialog.open() } } } function onReceiveCompleted(sessionId) { if (sessionId === currentSessionId) { receiveProgressDialog.close() receiveProgress = {} currentSessionId = "" } } function onReceiveError(sessionId, error) { if (sessionId === currentSessionId) { receiveProgressDialog.close() errorDialog.text = error errorDialog.open() } } 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() } } Dialog { id: errorDialog anchors.centerIn: parent modal: true standardButtons: Dialog.Ok title: qsTr("Error") property alias text: errorLabel.text Label { id: errorLabel } } Dialog { id: successDialog anchors.centerIn: parent modal: true standardButtons: Dialog.Ok title: qsTr("Success") property alias text: successLabel.text Label { id: successLabel } } 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 } 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: "transparent" radius: 8 border.color: palette.mid border.width: 1 } 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: palette.mid font.pixelSize: 12 } } Button { text: qsTr("Send") enabled: !appController.sending onClicked: { selectedDeviceFingerprint = modelData.fingerprint fileDialog.open() } } } } Label { anchors.centerIn: parent text: qsTr("No devices found") color: palette.mid visible: deviceListView.count === 0 } } RowLayout { Layout.fillWidth: true Button { text: qsTr("Refresh") onClicked: appController.refreshDevices() } Item { Layout.fillWidth: true } Label { text: qsTr("Alias: %1").arg(appController.alias) color: palette.mid } } } 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 } 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 } } 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() } } }