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: "" DropArea { id: dropArea anchors.fill: parent z: 9999 onEntered: function(drag) { if (drag.hasUrls) { drag.accepted = true dropOverlay.visible = true } else { drag.accepted = false } } onDropped: function(drop) { dropOverlay.visible = false if (drop.hasUrls) { var paths = [] for (var i = 0; i < drop.urls.length; i++) { paths.push(drop.urls[i].toString()) } if (paths.length > 0) { appController.addFiles(paths) } } } onExited: { dropOverlay.visible = false } Rectangle { id: dropOverlay visible: false anchors.fill: parent color: "#200080FF" z: 10000 Label { anchors.centerIn: parent text: qsTr("Drop files here to add them") font.pixelSize: 24 font.bold: true color: "#0080FF" } Rectangle { anchors.fill: parent anchors.margins: 8 color: "transparent" radius: 12 border.color: "#0080FF" border.width: 3 } } } StackView { id: stackView anchors.fill: parent Component.onCompleted: { push(homePageComponent) } } FileDialog { id: fileDialog title: qsTr("Select Files") fileMode: FileDialog.OpenFiles onAccepted: { var paths = [] for (var i = 0; i < selectedFiles.length; i++) { paths.push(selectedFiles[i].toString()) } if (paths.length > 0) { appController.addFiles(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) receiveProgressDialog.open() } 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("From: %1").arg(appController.currentReceiveSenderAlias) font.bold: true } Label { text: appController.currentReceiveFileName ? qsTr("%1 (%2/%3)").arg(appController.currentReceiveFileName) .arg(appController.currentReceiveFileIndex) .arg(appController.totalReceiveFiles) : qsTr("Waiting for data...") elide: Text.ElideMiddle Layout.maximumWidth: 400 } ProgressBar { Layout.fillWidth: true from: 0 to: 100 value: appController.receiveProgress } Label { text: qsTr("%1% complete").arg(Math.round(appController.receiveProgress)) color: palette.mid } } footer: DialogButtonBox { Button { text: qsTr("Cancel") DialogButtonBox.buttonRole: DialogButtonBox.RejectRole onClicked: { appController.cancelReceive() receiveProgressDialog.close() } } } } Dialog { id: sendProgressDialog anchors.centerIn: parent modal: true closePolicy: Popup.NoAutoClose title: qsTr("Sending Files") ColumnLayout { spacing: 12 Label { text: appController.currentSendFileName ? qsTr("%1 (%2/%3)").arg(appController.currentSendFileName) .arg(appController.currentSendFileIndex) .arg(appController.totalSendFiles) : qsTr("Preparing...") elide: Text.ElideMiddle Layout.maximumWidth: 400 } 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 onReceiveCompleted(sessionId) { receiveProgressDialog.close() } function onReceiveError(sessionId, error) { receiveProgressDialog.close() errorDialog.text = error errorDialog.open() } function onSendingChanged() { if (appController.sending && !sendProgressDialog.visible) { sendProgressDialog.open() } } function onSendCompleted(sessionId) { sendProgressDialog.close() successDialog.text = qsTr("Files sent successfully!") successDialog.open() } function onSendError(error) { sendProgressDialog.close() pinDialog.close() errorDialog.text = error errorDialog.open() } function onSendCanceled() { sendProgressDialog.close() errorDialog.text = qsTr("Transfer canceled by receiver") errorDialog.open() } function onPinRequired(firstAttempt) { pinDialog.isFirstAttempt = firstAttempt pinDialog.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 } } Dialog { id: pinDialog anchors.centerIn: parent modal: true closePolicy: Popup.NoAutoClose title: qsTr("PIN Required") property bool isFirstAttempt: true ColumnLayout { spacing: 12 Label { text: pinDialog.isFirstAttempt ? qsTr("The receiver requires a PIN to accept files.") : qsTr("Invalid PIN. Please try again.") wrapMode: Text.WordWrap Layout.fillWidth: true color: pinDialog.isFirstAttempt ? palette.text : "red" } TextField { id: pinInput Layout.fillWidth: true placeholderText: qsTr("Enter PIN") echoMode: TextInput.Password focus: true onAccepted: pinDialog.submitPin() } } footer: DialogButtonBox { Button { text: qsTr("Cancel") DialogButtonBox.buttonRole: DialogButtonBox.RejectRole } Button { text: qsTr("Submit") DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole enabled: pinInput.text.length > 0 } } onAccepted: submitPin() onRejected: { appController.cancelSend() sendProgressDialog.close() close() } function submitPin() { if (pinInput.text.length > 0) { appController.retryWithPin(pinInput.text) pinInput.text = "" } } onOpened: { pinInput.text = "" pinInput.forceActiveFocus() } } Dialog { id: setPinDialog anchors.centerIn: parent modal: true title: qsTr("Set Receive PIN") ColumnLayout { spacing: 12 Label { text: qsTr("Enter a PIN that senders must provide to transfer files to this device. Leave empty to disable.") wrapMode: Text.WordWrap Layout.fillWidth: true } TextField { id: setPinInput Layout.fillWidth: true placeholderText: qsTr("Enter PIN") onAccepted: setPinDialog.accepted() } } footer: DialogButtonBox { Button { text: qsTr("Cancel") DialogButtonBox.buttonRole: DialogButtonBox.RejectRole } Button { text: qsTr("OK") DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole } } onAccepted: { appController.receivePin = setPinInput.text } onOpened: { setPinInput.text = appController.receivePin setPinInput.forceActiveFocus() } } 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" } 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 ColumnLayout { Layout.fillWidth: true spacing: 8 RowLayout { Layout.fillWidth: true Label { text: qsTr("Selected Files") font.bold: true font.pixelSize: 16 } Item { Layout.fillWidth: true } Button { text: qsTr("Add Files") onClicked: fileDialog.open() } Button { text: qsTr("Clear All") visible: appController.hasPendingFiles onClicked: appController.clearPendingFiles() } } ListView { id: pendingFilesList Layout.fillWidth: true Layout.preferredHeight: Math.min(180, contentHeight) model: appController.pendingFiles spacing: 4 visible: appController.hasPendingFiles clip: true delegate: Pane { width: ListView.view.width padding: 8 background: Rectangle { color: "transparent" radius: 6 border.color: palette.mid border.width: 1 } RowLayout { anchors.fill: parent spacing: 8 Label { text: modelData.fileName Layout.fillWidth: true elide: Text.ElideMiddle } Label { text: formatSize(modelData.size) color: palette.mid font.pixelSize: 12 } ToolButton { text: "\u2715" font.pixelSize: 14 onClicked: appController.removePendingFile(index) } } } } Label { visible: !appController.hasPendingFiles text: qsTr("No files selected. Click \"Add Files\" or drag and drop files here.") color: palette.mid Layout.fillWidth: true wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter } } 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 color: palette.text } Label { text: "%1:%2".arg(modelData.ip).arg(modelData.port) color: palette.mid font.pixelSize: 12 } } Button { visible: appController.hasPendingFiles && !appController.sending text: qsTr("Send") enabled: !appController.sending onClicked: { if (appController.hasPendingFiles && !appController.sending) { appController.sendTo(modelData.fingerprint) } } } } } 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 } Label { text: qsTr("Receive PIN:") } RowLayout { Layout.fillWidth: true Label { text: appController.receivePin.length > 0 ? qsTr("Enabled") : qsTr("Disabled") color: appController.receivePin.length > 0 ? "green" : palette.mid } Item { Layout.fillWidth: true } Button { text: appController.receivePin.length > 0 ? qsTr("Change") : qsTr("Set PIN") onClicked: setPinDialog.open() } Button { text: qsTr("Remove") visible: appController.receivePin.length > 0 onClicked: appController.receivePin = "" } } } 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() } } }