Skip to content

Commit 92cddc2

Browse files
committed
refactor: 重构剪贴板上传逻辑,集中上传逻辑到一处
1 parent fce7700 commit 92cddc2

11 files changed

Lines changed: 140 additions & 188 deletions

src/screens/HomeScreen.tsx

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ import { useErrorStore } from '@/stores/errorStore';
2424
import { QuickLoadingPage } from '@/components/QuickLoadingPage';
2525
import { getClipboardSyncService } from '@/services/sync/ClipboardSyncService';
2626
import { createContentFromFile } from '@/utils/clipboard/clipboardContentUtils';
27-
import { setRemoteClipboard } from '@/services/sync/ClipboardSyncActions';
27+
import {
28+
setRemoteClipboard,
29+
uploadLocalClipboard,
30+
cancelUploadLocalClipboard,
31+
} from '@/services/sync/ClipboardSyncActions';
2832
import type { ProgressInfo } from '@/types/progress';
2933

3034
export function HomeScreen() {
@@ -153,7 +157,7 @@ export function HomeScreen() {
153157
fileUploadPayload.fileSize,
154158
{ signal }
155159
);
156-
await setRemoteClipboard(content, signal, (info) => {
160+
await setRemoteClipboard(content, 'external', signal, (info) => {
157161
setFileUploadProgress(info);
158162
});
159163
},
@@ -205,20 +209,7 @@ export function HomeScreen() {
205209
clearError();
206210

207211
console.log('[HomeScreen] Starting upload...');
208-
const result = await getClipboardSyncService().triggerUpload();
209-
console.log('[HomeScreen] Upload result:', JSON.stringify(result, null, 2));
210-
211-
if (result.success) {
212-
showMessage('剪贴板已上传到服务器', 'success');
213-
} else {
214-
const errorMessage = result.error || '上传失败';
215-
console.log('[HomeScreen] Upload failed, setting error:', errorMessage);
216-
setError({
217-
title: '上传失败',
218-
message: errorMessage,
219-
});
220-
showMessage('上传失败', 'error');
221-
}
212+
await uploadLocalClipboard(null);
222213
} catch (error: unknown) {
223214
console.error('[HomeScreen] Upload exception:', error);
224215
const errorMessage = error instanceof Error ? error.message : '无法上传到服务器';
@@ -230,7 +221,6 @@ export function HomeScreen() {
230221
normalizedMessage.includes('cancelled');
231222

232223
if (isCanceled) {
233-
showMessage('已取消上传', 'info');
234224
return;
235225
}
236226

@@ -254,7 +244,7 @@ export function HomeScreen() {
254244
return;
255245
}
256246

257-
getClipboardSyncService().cancelUpload();
247+
cancelUploadLocalClipboard();
258248
showMessage('正在取消上传...', 'info');
259249
}, [uploadingClipboard, showMessage]);
260250

src/screens/ProcessTextScreen.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/**
22
* Process Text Screen
33
* 处理来自 Android 文字选中菜单(PROCESS_TEXT)的上传请求。
4-
* 复用 QuickLoadingPage 和 uploadTextAndAddToHistory,与 ShareReceiveScreen 保持一致。
54
*/
65

76
import React, { useCallback } from 'react';
87
import { QuickLoadingPage } from '@/components/QuickLoadingPage';
98
import { useSettingsStore } from '@/stores/settingsStore';
10-
import { uploadTextAndAddToHistory } from '@/utils/uploadFile';
9+
import { createContentFromText } from '@/utils/clipboard/clipboardContentUtils';
10+
import { setRemoteClipboard } from '@/services/sync/ClipboardSyncActions';
1111

1212
interface ProcessTextScreenProps {
1313
text: string;
@@ -20,7 +20,8 @@ export const ProcessTextScreen: React.FC<ProcessTextScreenProps> = ({ text, onCo
2020
const task = useCallback(
2121
async (signal: AbortSignal) => {
2222
if (!activeServer) throw new Error('请先在设置中配置服务器');
23-
await uploadTextAndAddToHistory(text, { signal });
23+
const content = await createContentFromText(text, { signal });
24+
await setRemoteClipboard(content, 'external', signal);
2425
},
2526
[text, activeServer]
2627
);

src/screens/QuickTileLoadingScreen.tsx

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { ToastAndroid, Linking } from 'react-native';
33
import { SyncDirection } from '@/types/sync';
44
import { ClipboardContent } from '@/types/clipboard';
55
import { SyncManager } from '@/services/sync/SyncManager';
6+
import { setRemoteClipboard } from '@/services/sync/ClipboardSyncActions';
7+
import { localClipboard } from '@/services/clipboard/LocalClipboard';
68
import { openFile, shareFile, saveFile, saveToGallery } from '@/utils/fileActions';
79
import { isTextInvalid } from '@/utils/index';
810
import { QuickLoadingPage, SuccessButtonConfig } from '@/components/QuickLoadingPage';
@@ -33,21 +35,27 @@ export const QuickTileLoadingScreen: React.FC<QuickTileLoadingScreenProps> = ({
3335
setProgress(null);
3436
setPreviewText(undefined);
3537

36-
const syncMgr = SyncManager.getInstance();
37-
const result = await syncMgr.sync(
38-
direction,
39-
false,
40-
signal,
41-
(info) => setProgress(info),
42-
(preview) => setPreviewText(preview)
43-
);
38+
let content: ClipboardContent | null | undefined;
4439

45-
if (!result.success) {
46-
throw new Error(result.error || (isUpload ? '上传失败' : '同步失败'));
40+
if (isUpload) {
41+
content = await localClipboard.getClipboardContent();
42+
if (!content) throw new Error('剪贴板为空,无内容可上传');
43+
await setRemoteClipboard(content, 'external', signal, (info) => setProgress(info));
44+
} else {
45+
const syncMgr = SyncManager.getInstance();
46+
const result = await syncMgr.sync(
47+
direction,
48+
false,
49+
signal,
50+
(info) => setProgress(info),
51+
(preview) => setPreviewText(preview)
52+
);
53+
if (!result.success) {
54+
throw new Error(result.error || '同步失败');
55+
}
56+
content = result.content;
4757
}
4858

49-
const content = result.content;
50-
5159
// 只有文本类型才显示 Toast 提示
5260
if (content && content.type === 'Text' && !isTextInvalid(content.text)) {
5361
const preview = content.text.trim().replace(/\s+/g, ' ');

src/screens/ShareReceiveScreen.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import { View, ActivityIndicator, Text, StyleSheet, BackHandler } from 'react-na
99
import { useIncomingShare, clearSharedPayloads, getSharedPayloads } from 'expo-sharing';
1010
import { useTheme } from '@/hooks/useTheme';
1111
import { useSettingsStore } from '@/stores/settingsStore';
12-
import { uploadFileAndAddToHistory, uploadTextAndAddToHistory } from '@/utils/uploadFile';
13-
import { createContentFromFile } from '@/utils/clipboard/clipboardContentUtils';
12+
import {
13+
createContentFromFile,
14+
createContentFromText,
15+
} from '@/utils/clipboard/clipboardContentUtils';
16+
import { setRemoteClipboard } from '@/services/sync/ClipboardSyncActions';
1417
import { QuickLoadingPage } from '@/components/QuickLoadingPage';
1518
import type { ProgressInfo } from 'native-util';
1619

@@ -66,7 +69,8 @@ export const ShareReceiveScreen: React.FC<ShareReceiveScreenProps> = ({ onComple
6669
if (!text) throw new Error('分享的文字内容为空');
6770
setLoadingText('正在上传文字…');
6871
setPreviewText(text.slice(0, 100));
69-
await uploadTextAndAddToHistory(text, { signal });
72+
const textContent = await createContentFromText(text, { signal });
73+
await setRemoteClipboard(textContent, 'external', signal);
7074
clearSharedPayloads();
7175
return;
7276
}
@@ -92,12 +96,9 @@ export const ShareReceiveScreen: React.FC<ShareReceiveScreenProps> = ({ onComple
9296
undefined,
9397
{ signal }
9498
);
95-
await uploadFileAndAddToHistory(content, {
96-
signal,
97-
onProgress: (stage, info) => {
98-
setLoadingText(stage);
99-
setProgress(info ?? null);
100-
},
99+
await setRemoteClipboard(content, 'external', signal, (info) => {
100+
setLoadingText('正在上传文件…');
101+
setProgress(info ?? null);
101102
});
102103
clearSharedPayloads();
103104
},

src/services/clipboard/ClipboardMonitor.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,13 @@ export class ClipboardMonitor {
271271
await this.checkClipboard();
272272
}
273273

274+
/**
275+
* 获取上次已知的本地剪贴板内容缓存(不触发系统 API 读取)
276+
*/
277+
getLastContent(): ClipboardContent | null {
278+
return this.lastContent;
279+
}
280+
274281
/**
275282
* 手动更新上次已知内容,防止监听器将外部设置的剪贴板内容误判为用户新复制
276283
*/

src/services/sync/ClipboardChangedHandler.ts

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55

66
import { AppState, Platform, ToastAndroid } from 'react-native';
77
import { ClipboardContent } from '../../types/clipboard';
8-
import { SyncDirection, SyncResult } from '../../types/sync';
98
import type { AppConfig } from '../../types';
109
import { clipboardSyncState } from './SyncState';
1110
import { configService } from '../ConfigService';
1211
import { remoteClipboardMonitor } from './RemoteClipboardMonitor';
12+
import { uploadLocalClipboard } from './ClipboardSyncActions';
1313
import { SyncManager } from './SyncManager';
1414
import { historyService } from '../history/HistoryService';
1515
import { calculateTextHash } from '../../utils/hash';
@@ -20,7 +20,6 @@ class ClipboardChangedHandler {
2020

2121
private lastRemoteProfileHash: string | null = null;
2222
private lastLocalProfileHash: string | null = null;
23-
private isAutoSyncing = false;
2423

2524
private constructor() {}
2625

@@ -123,7 +122,7 @@ class ClipboardChangedHandler {
123122
const localMatchesRemote = remoteHash === this.lastLocalProfileHash;
124123
const activeServer = await configService.getActiveServer();
125124

126-
if (localMatchesRemote || !activeServer || this.isAutoSyncing) {
125+
if (localMatchesRemote || !activeServer) {
127126
return;
128127
}
129128

@@ -132,7 +131,6 @@ class ClipboardChangedHandler {
132131
return;
133132
}
134133

135-
this.isAutoSyncing = true;
136134
try {
137135
const result = await this.copyToLocalClipboard(content);
138136
if (result.success && Platform.OS === 'android') {
@@ -144,8 +142,6 @@ class ClipboardChangedHandler {
144142
}
145143
} catch (error) {
146144
console.error('[ClipboardChangedHandler] Auto-copy failed:', error);
147-
} finally {
148-
this.isAutoSyncing = false;
149145
}
150146
}
151147

@@ -191,28 +187,21 @@ class ClipboardChangedHandler {
191187
if (currentHash === this.lastLocalProfileHash) return;
192188
this.lastLocalProfileHash = currentHash;
193189

194-
if (this.isAutoSyncing) return;
195-
this.isAutoSyncing = true;
196-
197-
SyncManager.getInstance()
198-
.sync(SyncDirection.Upload, true)
199-
.then((result: SyncResult) => {
200-
if (result.success && !result.skipped && Platform.OS === 'android') {
201-
const preview =
202-
content.type === 'Text' && content.text
203-
? content.text.trim().replace(/\s+/g, ' ').slice(0, 30)
204-
: content.fileName || content.type;
205-
SyncManager.getInstance().updateForegroundNotification(`已上传: ${preview}`);
206-
if (config?.syncToastEnabled !== false) {
207-
ToastAndroid.show(`已上传\n${preview}`, ToastAndroid.SHORT);
208-
}
209-
remoteClipboardMonitor.refresh().catch(() => {});
190+
try {
191+
const uploaded = await uploadLocalClipboard(content);
192+
if (uploaded && Platform.OS === 'android') {
193+
const preview = this.getContentPreview(content);
194+
SyncManager.getInstance().updateForegroundNotification(`已上传: ${preview}`);
195+
if (config?.syncToastEnabled !== false) {
196+
ToastAndroid.show(`已上传\n${preview}`, ToastAndroid.SHORT);
210197
}
211-
})
212-
.catch((e: Error) => console.error('[ClipboardChangedHandler] Auto-upload failed:', e))
213-
.finally(() => {
214-
this.isAutoSyncing = false;
215-
});
198+
remoteClipboardMonitor.refresh().catch(() => {});
199+
}
200+
} catch (e: unknown) {
201+
if (!(e instanceof DOMException && e.name === 'AbortError')) {
202+
console.error('[ClipboardChangedHandler] Auto-upload failed:', e);
203+
}
204+
}
216205
}
217206

218207
async downloadRemoteFile(

src/services/sync/ClipboardSyncActions.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,48 @@ import { historyService } from '../history/HistoryService';
44
import { getClientService } from '../client/ClientService';
55
import { configService } from '../ConfigService';
66
import { remoteClipboardMonitor } from './RemoteClipboardMonitor';
7+
import { clipboardSyncState } from './SyncState';
8+
import { clipboardMonitor } from '../clipboard/ClipboardMonitor';
9+
10+
/** 同步优先级:external(高,外部触发)或 currentlocal(低,当前本地自动) */
11+
export type SyncPriority = 'external' | 'currentlocal';
12+
13+
const PRIORITY_LEVEL: Record<SyncPriority, number> = {
14+
external: 1,
15+
currentlocal: 0,
16+
};
717

818
/** 当前正在执行的上传控制器,用于取消上一个未完成的实例 */
919
let _currentController: AbortController | null = null;
20+
let _currentPriority: SyncPriority = 'currentlocal';
1021

1122
/**
1223
* 上传文件到远程剪贴板,并将内容添加到本地历史记录。
13-
* 同时只允许一个实例运行;新调用会取消尚未完成的上一次调用
24+
* 同时只允许一个实例运行;高优先级可打断同等或更低优先级,低优先级无法打断高优先级
1425
* @param content 已构建好的剪贴板内容(含 profileHash、fileUri 等)
26+
* @param priority 同步优先级,'external' 为外部触发(高),'currentlocal' 为本地自动(低)
1527
* @param signal 外部取消信号
1628
* @param onProgress 上传进度回调
29+
* @returns `true` 表示上传已完成,`false` 表示因低优先级被跳过(高优先级正在执行)
30+
* @throws 当被更高优先级打断(AbortError)或发生其他错误时抛出
1731
*/
1832
export async function setRemoteClipboard(
1933
content: ClipboardContent,
34+
priority: SyncPriority,
2035
signal: AbortSignal,
2136
onProgress?: (info: ProgressInfo) => void
22-
): Promise<void> {
23-
// 取消上一个正在进行的实例
24-
_currentController?.abort();
37+
): Promise<boolean> {
38+
// 仅在新优先级 >= 当前优先级时才打断正在进行的实例
39+
if (_currentController && PRIORITY_LEVEL[priority] >= PRIORITY_LEVEL[_currentPriority]) {
40+
_currentController.abort();
41+
} else if (_currentController) {
42+
// 低优先级无法打断高优先级,跳过本次执行
43+
return false;
44+
}
2545

2646
const controller = new AbortController();
2747
_currentController = controller;
48+
_currentPriority = priority;
2849

2950
// 将外部 signal 的取消转发给内部 controller
3051
const onExternalAbort = () => controller.abort(signal.reason);
@@ -47,10 +68,37 @@ export async function setRemoteClipboard(
4768
if (content.profileHash) {
4869
remoteClipboardMonitor.setLastContentHash(content.profileHash);
4970
}
71+
return true;
5072
} finally {
5173
signal.removeEventListener('abort', onExternalAbort);
5274
if (_currentController === controller) {
5375
_currentController = null;
5476
}
5577
}
5678
}
79+
80+
/**
81+
* 上传内容到远程剪贴板,并同步更新本地剪贴板卡片的上传状态(uploadingClipboard)。
82+
* 内部使用引用计数,支持与其他上传操作并发而不互相干扰状态。
83+
* @param content 剪贴板内容;为 null/undefined 时自动从本地剪贴板读取
84+
* @returns `true` 表示上传已完成,`false` 表示因低优先级被跳过或无内容可上传
85+
*/
86+
export async function uploadLocalClipboard(
87+
content: ClipboardContent | null | undefined,
88+
onProgress?: (info: ProgressInfo) => void
89+
): Promise<boolean> {
90+
const actualContent = content ?? clipboardMonitor.getLastContent();
91+
if (!actualContent) return false;
92+
const controller = new AbortController();
93+
clipboardSyncState.setUploadingClipboard(true);
94+
try {
95+
return await setRemoteClipboard(actualContent, 'currentlocal', controller.signal, onProgress);
96+
} finally {
97+
clipboardSyncState.setUploadingClipboard(false);
98+
}
99+
}
100+
101+
/** 取消当前正在进行的本地上传(如有) */
102+
export function cancelUploadLocalClipboard(): void {
103+
_currentController?.abort();
104+
}

0 commit comments

Comments
 (0)