Skip to content

Commit c79b0af

Browse files
committed
feat(drag-drop): enhance error handling and share intent management in Android plugin
1 parent f189015 commit c79b0af

7 files changed

Lines changed: 164 additions & 53 deletions

File tree

frontend/src/components/sender/PairedDevicesPanel.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export function PairedDevicesPanel({
9494
const Icon = deviceTypeIcon(device.device_type)
9595
const inviteStatus = pairedInviteStatus[device.endpoint_id]
9696
const isActive = isPairedDeviceActive(device)
97+
const isOnline = device.online
9798
const isSending = inviteStatus === 'sending'
9899
const anotherDeviceSelected = Object.entries(
99100
pairedInviteStatus
@@ -107,6 +108,7 @@ export function PairedDevicesPanel({
107108
!hasTicket ||
108109
isSending ||
109110
!isActive ||
111+
!isOnline ||
110112
anotherDeviceSelected ||
111113
inviteStatus === 'sent'
112114
return (

frontend/src/hooks/useDragDrop.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ export function useDragDrop(
167167
startCopy: (
168168
onStart: (path: string, size: bigint) => void,
169169
onEvent: (event: { progress: number }) => void,
170-
onComplete: (path: string) => void
170+
onComplete: (path: string) => void,
171+
onError?: (message: string) => void
171172
) => Promise<{ cancelJob: () => Promise<void> } | null>,
172173
pathType: 'file' | 'directory'
173174
) => {
@@ -188,6 +189,18 @@ export function useDragDrop(
188189
setCopyTotalBytes('0')
189190
cancelRef.current = null
190191
await triggerFilesSelect([path], pathType)
192+
},
193+
(message) => {
194+
setIsCopying(false)
195+
setCopyProgress(0)
196+
setCopyFileName('')
197+
setCopyTotalBytes('0')
198+
cancelRef.current = null
199+
showAlert(
200+
t('common:errors.fileDialogFailed'),
201+
message,
202+
'error'
203+
)
191204
}
192205
)
193206

@@ -197,7 +210,7 @@ export function useDragDrop(
197210

198211
return Boolean(handler)
199212
},
200-
[triggerFilesSelect]
213+
[showAlert, t, triggerFilesSelect]
201214
)
202215

203216
const consumeAndroidShare = useCallback(async () => {
@@ -437,6 +450,7 @@ export function useDragDrop(
437450

438451
let disposed = false
439452
let unlistenShare: (() => void) | undefined
453+
const retryTimers: number[] = []
440454

441455
const run = () => {
442456
if (!disposed) {
@@ -452,13 +466,20 @@ export function useDragDrop(
452466
}
453467
// Cold start: intent may already be pending before listeners registered.
454468
run()
469+
// Native load() posts shareReceived after WebView is ready; these retries
470+
// cover the case where the first consume ran before the URI was stashed.
471+
retryTimers.push(window.setTimeout(run, 400))
472+
retryTimers.push(window.setTimeout(run, 1200))
455473
}
456474

457475
void setup()
458476

459477
return () => {
460478
disposed = true
461479
unlistenShare?.()
480+
for (const id of retryTimers) {
481+
window.clearTimeout(id)
482+
}
462483
}
463484
}, [])
464485

frontend/src/hooks/useSender.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -947,7 +947,7 @@ export function useSender(): UseSenderReturn {
947947
pairedDevices.find((d) => d.endpoint_id === endpointId) ?? null
948948
const deviceName =
949949
device?.display_name ?? t('common:sender.pairedDevices.unknownPeer')
950-
if (device && !isPairedDeviceActive(device)) {
950+
if (device && (!isPairedDeviceActive(device) || !device.online)) {
951951
return false
952952
}
953953
incrementPairedSendCount(endpointId)

frontend/src/plugins/nativeUtils.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type CopyProgress = {
1010
totalBytes: string
1111
progress: number
1212
cachedPath?: string
13+
error?: string
1314
}
1415

1516
export class FileSelectedHandler {
@@ -54,16 +55,21 @@ type CopyHandlers = {
5455
onStart: (path: string, size: bigint) => void
5556
onEvent: (event: CopyProgress) => void
5657
onComplete: (path: string) => void
58+
onError?: (message: string) => void
5759
}
5860

5961
function bindCopyChannel(
6062
channel: { onmessage: (event: CopyProgress) => void },
6163
handlers: CopyHandlers
6264
) {
6365
channel.onmessage = (event: CopyProgress) => {
64-
if (event.progress === 0 && event.cachedPath) {
65-
handlers.onStart(event.cachedPath, BigInt(event.totalBytes))
66-
} else if (event.progress === 1 && event.cachedPath) {
66+
if (event.error) {
67+
handlers.onError?.(event.error)
68+
return
69+
}
70+
if (event.cachedPath && (event.progress === 0 || event.progress === 0.0)) {
71+
handlers.onStart(event.cachedPath, BigInt(event.totalBytes || '0'))
72+
} else if (event.cachedPath && event.progress >= 1) {
6773
handlers.onComplete(event.cachedPath)
6874
} else {
6975
handlers.onEvent(event)
@@ -74,7 +80,8 @@ function bindCopyChannel(
7480
export async function selectSendDocument(
7581
onStart: (path: string, size: bigint) => void,
7682
onEvent: (event: CopyProgress) => void,
77-
onComplete: (path: string) => void
83+
onComplete: (path: string) => void,
84+
onError?: (message: string) => void
7885
): Promise<FileSelectedHandler | null> {
7986
if (!IS_TAURI) {
8087
const selected = await openDialog({ multiple: true, directory: false })
@@ -89,7 +96,7 @@ export async function selectSendDocument(
8996

9097
const { Channel } = await import('@tauri-apps/api/core')
9198
const channel = new Channel<CopyProgress>()
92-
bindCopyChannel(channel, { onStart, onEvent, onComplete })
99+
bindCopyChannel(channel, { onStart, onEvent, onComplete, onError })
93100
const response = await invoke<boolean | undefined>(
94101
'plugin:native-utils|select_send_document',
95102
{
@@ -103,7 +110,8 @@ export async function selectSendDocument(
103110
export async function selectSendFolder(
104111
onStart: (path: string, size: bigint) => void,
105112
onEvent: (event: CopyProgress) => void,
106-
onComplete: (path: string) => void
113+
onComplete: (path: string) => void,
114+
onError?: (message: string) => void
107115
): Promise<FileSelectedHandler | null> {
108116
if (!IS_TAURI) {
109117
const selected = await openDialog({ multiple: false, directory: true })
@@ -117,7 +125,7 @@ export async function selectSendFolder(
117125

118126
const { Channel } = await import('@tauri-apps/api/core')
119127
const channel = new Channel<CopyProgress>()
120-
bindCopyChannel(channel, { onStart, onEvent, onComplete })
128+
bindCopyChannel(channel, { onStart, onEvent, onComplete, onError })
121129
const response = await invoke<boolean>(
122130
'plugin:native-utils|select_send_folder',
123131
{
@@ -132,13 +140,14 @@ export async function selectSendFolder(
132140
export async function consumeShareIntent(
133141
onStart: (path: string, size: bigint) => void,
134142
onEvent: (event: CopyProgress) => void,
135-
onComplete: (path: string) => void
143+
onComplete: (path: string) => void,
144+
onError?: (message: string) => void
136145
): Promise<FileSelectedHandler | null> {
137146
if (!IS_TAURI) return null
138147

139148
const { Channel } = await import('@tauri-apps/api/core')
140149
const channel = new Channel<CopyProgress>()
141-
bindCopyChannel(channel, { onStart, onEvent, onComplete })
150+
bindCopyChannel(channel, { onStart, onEvent, onComplete, onError })
142151
const response = await invoke<boolean | undefined>(
143152
'plugin:native-utils|consume_share_intent',
144153
{ channel }

scripts/package-windows-portable.js

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,48 @@ function copyDir(src, dest) {
111111
}
112112
}
113113

114+
/**
115+
* Tauri packages `bundle.resources` into installers but often does not leave a
116+
* persistent `resources/` tree next to the release exe. Prefer that tree when
117+
* present; otherwise assemble it from src-tauri paths listed in tauri.conf.json.
118+
*/
119+
function stageResources(releaseDir, stagingAppDir) {
120+
const stagedResources = path.join(stagingAppDir, 'resources')
121+
const releaseResources = path.join(releaseDir, 'resources')
122+
123+
if (fs.existsSync(releaseResources)) {
124+
copyDir(releaseResources, stagedResources)
125+
console.log(`Bundled resources/ from ${releaseResources}`)
126+
return
127+
}
128+
129+
const confPath = path.join(repoRoot, 'src-tauri', 'tauri.conf.json')
130+
const conf = JSON.parse(fs.readFileSync(confPath, 'utf8'))
131+
const resourceEntries = conf?.bundle?.resources
132+
if (!Array.isArray(resourceEntries) || resourceEntries.length === 0) {
133+
throw new Error(
134+
`No resources/ next to the exe and no bundle.resources in ${confPath}`
135+
)
136+
}
137+
138+
const srcTauri = path.join(repoRoot, 'src-tauri')
139+
for (const entry of resourceEntries) {
140+
if (typeof entry !== 'string') {
141+
throw new Error(
142+
`Unsupported bundle.resources entry (expected string path): ${JSON.stringify(entry)}`
143+
)
144+
}
145+
const from = path.join(srcTauri, entry)
146+
if (!fs.existsSync(from) || !fs.statSync(from).isFile()) {
147+
throw new Error(`bundle.resources file missing: ${from}`)
148+
}
149+
copyFile(from, path.join(stagedResources, entry))
150+
}
151+
console.log(
152+
`Assembled resources/ from src-tauri (${resourceEntries.length} file(s); Tauri did not leave resources/ under ${releaseDir})`
153+
)
154+
}
155+
114156
function stagePortablePayload(releaseDir, stagingAppDir) {
115157
fs.rmSync(stagingAppDir, { recursive: true, force: true })
116158
fs.mkdirSync(stagingAppDir, { recursive: true })
@@ -134,13 +176,7 @@ function stagePortablePayload(releaseDir, stagingAppDir) {
134176
copyFile(loader, path.join(stagingAppDir, 'WebView2Loader.dll'))
135177
}
136178

137-
const resources = path.join(releaseDir, 'resources')
138-
if (!fs.existsSync(resources)) {
139-
throw new Error(
140-
`Missing resources/ next to ${path.basename(builtExe)} at ${releaseDir}. The portable ZIP must include the same resources as the installer.`
141-
)
142-
}
143-
copyDir(resources, path.join(stagingAppDir, 'resources'))
179+
stageResources(releaseDir, stagingAppDir)
144180

145181
fs.writeFileSync(path.join(stagingAppDir, PORTABLE_MARKER), 'portable\n', 'utf8')
146182
fs.writeFileSync(

src-tauri/WiX/context-menu.wxs

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,27 @@
44
55
The app owns registration at runtime (HKCU) so the settings toggle can
66
enable/disable without elevation. Older builds wrote the same verb under
7-
HKLM on install — those leftovers (and any HKCU keys from the app) are
8-
removed on uninstall. We intentionally do NOT create the verb here.
7+
HKLM on install — those leftovers are removed on uninstall.
8+
9+
Intentionally HKLM-only: mixing HKCU/HKMU RemoveRegistryKey with a
10+
per-machine MSI component makes WiX light.exe fail (ICE / link abort).
11+
HKCU keys are cleaned by the NSIS uninstall hooks and the in-app toggle.
912
-->
1013
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
1114
<Fragment>
1215
<DirectoryRef Id="INSTALLDIR">
1316
<Component Id="ContextMenuRegistryFiles"
1417
Guid="3C4F8A2B-7D19-4E5F-8B3A-C6D0E2F14975">
15-
<!-- KeyPath marker so the component is installed; no shell verb is created. -->
16-
<RegistryValue Root="HKMU"
17-
Key="Software\n0des\AltSendme"
18-
Name="ContextMenuCleanup"
19-
Type="integer"
20-
Value="1"
21-
KeyPath="yes"/>
18+
<RegistryKey Root="HKLM"
19+
Key="Software\n0des\AltSendme"
20+
Action="createAndRemoveOnUninstall">
21+
<RegistryValue Name="ContextMenuCleanup"
22+
Type="integer"
23+
Value="1"
24+
KeyPath="yes"/>
25+
</RegistryKey>
2226

23-
<!-- Machine-wide leftovers from older MSI builds -->
27+
<!-- Machine-wide leftovers from older MSI builds that created the verb -->
2428
<RemoveRegistryKey Root="HKLM"
2529
Key="SOFTWARE\Classes\*\shell\Send with AltSendme"
2630
Action="removeOnUninstall"/>
@@ -30,17 +34,6 @@
3034
<RemoveRegistryKey Root="HKLM"
3135
Key="SOFTWARE\Classes\Directory\Background\shell\Send with AltSendme"
3236
Action="removeOnUninstall"/>
33-
34-
<!-- Per-user keys written by the in-app toggle -->
35-
<RemoveRegistryKey Root="HKCU"
36-
Key="Software\Classes\*\shell\Send with AltSendme"
37-
Action="removeOnUninstall"/>
38-
<RemoveRegistryKey Root="HKCU"
39-
Key="Software\Classes\Directory\shell\Send with AltSendme"
40-
Action="removeOnUninstall"/>
41-
<RemoveRegistryKey Root="HKCU"
42-
Key="Software\Classes\Directory\Background\shell\Send with AltSendme"
43-
Action="removeOnUninstall"/>
4437
</Component>
4538
</DirectoryRef>
4639
</Fragment>

0 commit comments

Comments
 (0)