diff --git a/app/lib/main.dart b/app/lib/main.dart index 7662457..680d2ce 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -176,6 +176,7 @@ class _CopyPasteAppState extends State Future _initShell() async { windowManager.addListener(this); + _startListening(); final isFirstRun = widget.storage.isFirstRun; await _appWindow.init(); if (Platform.isWindows || Platform.isMacOS) { @@ -210,7 +211,6 @@ class _CopyPasteAppState extends State } } - _startListening(); AutoUpdateService.onUpdateAvailable = _onUpdateAvailable; unawaited(AutoUpdateService.initialize()); } diff --git a/app/lib/screens/main_screen.dart b/app/lib/screens/main_screen.dart index 828cabb..4202133 100644 --- a/app/lib/screens/main_screen.dart +++ b/app/lib/screens/main_screen.dart @@ -65,6 +65,7 @@ class MainScreenState extends State { ClipboardTab _currentTab = ClipboardTab.recent; List _items = []; bool _loading = false; + bool _pendingReload = false; int _selectedIndex = -1; int _expandedIndex = -1; Timer? _reloadDebounce; @@ -131,6 +132,7 @@ class MainScreenState extends State { Future _loadItems() async { if (_loading) return; + _pendingReload = false; setState(() => _loading = true); try { @@ -158,11 +160,22 @@ class MainScreenState extends State { AppLogger.error('Failed to load items: $e'); setState(() => _loading = false); } + + if (_pendingReload) { + _pendingReload = false; + _currentPage = 0; + _hasMore = true; + await _loadItems(); + } } void _reload() { _reloadDebounce?.cancel(); _reloadDebounce = Timer(const Duration(milliseconds: 80), () { + if (_loading) { + _pendingReload = true; + return; + } _currentPage = 0; _hasMore = true; _loadItems(); diff --git a/core/lib/services/clipboard_service.dart b/core/lib/services/clipboard_service.dart index ab4c3da..5fd6a3c 100644 --- a/core/lib/services/clipboard_service.dart +++ b/core/lib/services/clipboard_service.dart @@ -27,23 +27,23 @@ class ClipboardService { int pasteIgnoreWindowMs = 450; - DateTime _lastPasteTime = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); + Stopwatch? _pasteStopwatch; String? _lastPastedContent; Future notifyPasteInitiated(String itemId) async { - _lastPasteTime = DateTime.now().toUtc(); + _pasteStopwatch = Stopwatch()..start(); final item = await _repository.getById(itemId); _lastPastedContent = item?.content; } bool _shouldIgnore(String? content) { - final elapsed = DateTime.now().toUtc().difference(_lastPasteTime); - if (elapsed.inMilliseconds < pasteIgnoreWindowMs) { - return true; - } + final sw = _pasteStopwatch; + if (sw == null) return false; + final elapsed = sw.elapsedMilliseconds; + if (elapsed < pasteIgnoreWindowMs) return true; if (content != null && content == _lastPastedContent && - elapsed.inMilliseconds < pasteIgnoreWindowMs * 2) { + elapsed < pasteIgnoreWindowMs * 2) { return true; } return false; @@ -212,7 +212,6 @@ class ClipboardService { modifiedAt: now, ); await _repository.update(updated); - await notifyPasteInitiated(itemId); return updated; } diff --git a/listener/windows/listener_plugin.cpp b/listener/windows/listener_plugin.cpp index 9dc49f3..465d1e5 100644 --- a/listener/windows/listener_plugin.cpp +++ b/listener/windows/listener_plugin.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -37,8 +38,6 @@ namespace listener { namespace { -static const UINT kWmClipboardUpdate = 0x031D; - std::vector ConvertDibToBmp(const std::vector& dib) { if (dib.size() < sizeof(BITMAPINFOHEADER)) return {}; @@ -261,7 +260,7 @@ void ListenerPlugin::StopListening() { std::optional ListenerPlugin::HandleWindowMessage( HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { - if (message == kWmClipboardUpdate) { + if (message == WM_CLIPBOARDUPDATE) { KillTimer(hwnd, kClipboardTimerId); SetTimer(hwnd, kClipboardTimerId, kClipboardTimerDelayMs, nullptr); } else if (message == WM_TIMER && wparam == kClipboardTimerId) { @@ -275,6 +274,10 @@ void ListenerPlugin::OnClipboardChanged() { HWND hwnd = registrar_->GetView() ? registrar_->GetView()->GetNativeWindow() : nullptr; if (!hwnd) return; + if (last_write_tick_ > 0 && + (GetTickCount64() - last_write_tick_) < kSelfWriteIgnoreMs) { + return; + } if (!OpenClipboard(hwnd)) return; flutter::EncodableMap event; @@ -393,12 +396,17 @@ bool ListenerPlugin::ShouldExclude() const { return true; } if (cf_can_include_ && IsClipboardFormatAvailable(cf_can_include_)) { - auto data = ExtractBytes(cf_can_include_); - if (data.size() >= 4) { - int val = static_cast(data[0]) | (static_cast(data[1]) << 8) | - (static_cast(data[2]) << 16) | - (static_cast(data[3]) << 24); - if (val == 0) return true; + HANDLE hData = GetClipboardData(cf_can_include_); + if (hData && GlobalSize(hData) >= 4) { + const void* ptr = GlobalLock(hData); + if (ptr) { + const auto* bytes = static_cast(ptr); + int val = static_cast(bytes[0]) | (static_cast(bytes[1]) << 8) | + (static_cast(bytes[2]) << 16) | + (static_cast(bytes[3]) << 24); + GlobalUnlock(hData); + if (val == 0) return true; + } } } return false; @@ -535,17 +543,20 @@ std::vector ListenerPlugin::ExtractFilePaths() { bool ListenerPlugin::IsUrl(const std::wstring& text) { if (text.size() < 5) return false; - auto lower = text; - std::transform(lower.begin(), lower.end(), lower.begin(), ::towlower); static const std::wstring kPrefixes[] = { L"https://", L"http://", L"ftp://", L"file:///", L"mailto:", }; + static constexpr size_t kMaxPrefix = 9; // longest prefix is "file:///" + const size_t checkLen = (std::min)(text.size(), kMaxPrefix); + std::wstring head(text.data(), checkLen); + std::transform(head.begin(), head.end(), head.begin(), ::towlower); + bool matched = false; for (const auto& prefix : kPrefixes) { - if (lower.size() >= prefix.size() && - lower.substr(0, prefix.size()) == prefix) { + if (head.size() >= prefix.size() && + head.compare(0, prefix.size(), prefix) == 0) { matched = true; break; } @@ -567,7 +578,7 @@ int ListenerPlugin::DetectFileType(const std::wstring& path) { std::wstring ext = p.extension().wstring(); std::transform(ext.begin(), ext.end(), ext.begin(), ::towupper); - static const std::map kExtMap = { + static const std::unordered_map kExtMap = { {L".MP3", 5}, {L".WAV", 5}, {L".FLAC", 5}, {L".AAC", 5}, {L".OGG", 5}, {L".WMA", 5}, {L".M4A", 5}, {L".MP4", 6}, {L".AVI", 6}, {L".MKV", 6}, {L".MOV", 6}, @@ -734,6 +745,10 @@ bool ListenerPlugin::SetTextToClipboard( bool ok = false; std::wstring wide = Utf8ToWide(text); + if (wide.empty()) { + CloseClipboard(); + return false; + } size_t sz = (wide.size() + 1) * sizeof(wchar_t); HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, sz); if (hMem) { @@ -786,6 +801,7 @@ bool ListenerPlugin::SetTextToClipboard( } CloseClipboard(); + if (ok) last_write_tick_ = GetTickCount64(); return ok; } @@ -861,6 +877,7 @@ bool ListenerPlugin::SetImageToClipboard(const std::string& imagePath) { if (!ok) GlobalFree(hMem); CloseClipboard(); + if (ok) last_write_tick_ = GetTickCount64(); return ok; } @@ -919,6 +936,7 @@ bool ListenerPlugin::SetFilesToClipboard( if (!ok) GlobalFree(hMem); CloseClipboard(); + if (ok) last_write_tick_ = GetTickCount64(); return ok; } diff --git a/listener/windows/listener_plugin.h b/listener/windows/listener_plugin.h index 7dae96b..b8b9ee7 100644 --- a/listener/windows/listener_plugin.h +++ b/listener/windows/listener_plugin.h @@ -54,6 +54,9 @@ class ListenerPlugin : public flutter::Plugin { static constexpr ULONGLONG kDebounceMs = 500; static constexpr UINT_PTR kClipboardTimerId = 1; static constexpr UINT kClipboardTimerDelayMs = 50; + static constexpr ULONGLONG kSelfWriteIgnoreMs = 700; + + ULONGLONG last_write_tick_ = 0; UINT cf_rtf_ = 0; UINT cf_html_ = 0;