Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ class _CopyPasteAppState extends State<CopyPasteApp>

Future<void> _initShell() async {
windowManager.addListener(this);
_startListening();
final isFirstRun = widget.storage.isFirstRun;
await _appWindow.init();
if (Platform.isWindows || Platform.isMacOS) {
Expand Down Expand Up @@ -210,7 +211,6 @@ class _CopyPasteAppState extends State<CopyPasteApp>
}
}

_startListening();
AutoUpdateService.onUpdateAvailable = _onUpdateAvailable;
unawaited(AutoUpdateService.initialize());
}
Expand Down
13 changes: 13 additions & 0 deletions app/lib/screens/main_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class MainScreenState extends State<MainScreen> {
ClipboardTab _currentTab = ClipboardTab.recent;
List<ClipboardItem> _items = [];
bool _loading = false;
bool _pendingReload = false;
int _selectedIndex = -1;
int _expandedIndex = -1;
Timer? _reloadDebounce;
Expand Down Expand Up @@ -131,6 +132,7 @@ class MainScreenState extends State<MainScreen> {

Future<void> _loadItems() async {
if (_loading) return;
_pendingReload = false;
setState(() => _loading = true);

try {
Expand Down Expand Up @@ -158,11 +160,22 @@ class MainScreenState extends State<MainScreen> {
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();
Expand Down
15 changes: 7 additions & 8 deletions core/lib/services/clipboard_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,23 @@ class ClipboardService {

int pasteIgnoreWindowMs = 450;

DateTime _lastPasteTime = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
Stopwatch? _pasteStopwatch;
String? _lastPastedContent;

Future<void> 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;
Expand Down Expand Up @@ -212,7 +212,6 @@ class ClipboardService {
modifiedAt: now,
);
await _repository.update(updated);
await notifyPasteInitiated(itemId);
return updated;
}

Expand Down
46 changes: 32 additions & 14 deletions listener/windows/listener_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include <filesystem>
#include <map>
#include <optional>
#include <unordered_map>
#include <sstream>
#include <string>
#include <vector>
Expand All @@ -37,8 +38,6 @@ namespace listener {

namespace {

static const UINT kWmClipboardUpdate = 0x031D;

std::vector<uint8_t> ConvertDibToBmp(const std::vector<uint8_t>& dib) {
if (dib.size() < sizeof(BITMAPINFOHEADER)) return {};

Expand Down Expand Up @@ -261,7 +260,7 @@ void ListenerPlugin::StopListening() {

std::optional<LRESULT> 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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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<int>(data[0]) | (static_cast<int>(data[1]) << 8) |
(static_cast<int>(data[2]) << 16) |
(static_cast<int>(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<const uint8_t*>(ptr);
int val = static_cast<int>(bytes[0]) | (static_cast<int>(bytes[1]) << 8) |
(static_cast<int>(bytes[2]) << 16) |
(static_cast<int>(bytes[3]) << 24);
GlobalUnlock(hData);
if (val == 0) return true;
}
}
}
return false;
Expand Down Expand Up @@ -535,17 +543,20 @@ std::vector<std::wstring> 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;
}
Expand All @@ -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<std::wstring, int> kExtMap = {
static const std::unordered_map<std::wstring, int> 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},
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -786,6 +801,7 @@ bool ListenerPlugin::SetTextToClipboard(
}

CloseClipboard();
if (ok) last_write_tick_ = GetTickCount64();
return ok;
}

Expand Down Expand Up @@ -861,6 +877,7 @@ bool ListenerPlugin::SetImageToClipboard(const std::string& imagePath) {
if (!ok) GlobalFree(hMem);

CloseClipboard();
if (ok) last_write_tick_ = GetTickCount64();
return ok;
}

Expand Down Expand Up @@ -919,6 +936,7 @@ bool ListenerPlugin::SetFilesToClipboard(
if (!ok) GlobalFree(hMem);

CloseClipboard();
if (ok) last_write_tick_ = GetTickCount64();
return ok;
}

Expand Down
3 changes: 3 additions & 0 deletions listener/windows/listener_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading