diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index 6782cbd..0000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-# These are supported funding model platforms
-
-github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
-patreon: Taxperia # Replace with a single Patreon username
-open_collective: # Replace with a single Open Collective username
-ko_fi: # Replace with a single Ko-fi username
-tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
-community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
-liberapay: # Replace with a single Liberapay username
-issuehunt: # Replace with a single IssueHunt username
-lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
-polar: # Replace with a single Polar username
-buy_me_a_coffee: taxperia # Replace with a single Buy Me a Coffee username
-thanks_dev: # Replace with a single thanks.dev username
-custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.gitignore b/.gitignore
index d9e6855..612d902 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,11 @@ releases/
*.zip
*.7z
*.rar
+
+# Local agent, planning, and work notes
+/AGENT.md
+/progress*.md
+/bak.md
+/*yapılacak*.md
+/memory-bank/
+/.release_notes_tmp/
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 84bbde5..f2da53e 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,32 +1,43 @@
-# TaxClip v1.2.2
-
-Security and quality maintenance release, plus refreshed English documentation.
-
-## Highlights
-
-- **Dependencies:** Pillow bumped to **12.3.0** (closes Dependabot CVE alerts)
-- **Code quality:** Clipboard dedupe fingerprints no longer use cryptographic hashes on sensitive clipboard text (fixes CodeQL `py/weak-sensitive-data-hashing`)
-- **Docs:** README, LICENSE, release notes, and GitHub / app description refreshed in English
-
-## Security & quality
-
-- Address all open Dependabot alerts for Pillow (< 12.3.0)
-- Replace SHA-256 / Blake2b clipboard fingerprints with a non-crypto content ID (`length` + Adler32 + CRC32) used only for short-lived deduplication — not for password storage or authentication
-- Re-enable / re-run CodeQL; Security and quality overview should show **0** open alerts on `main`
-
-## Documentation
-
-- Full English **README** with current feature set (smart cards, secret mode, compact panel, files, encryption, etc.)
-- English **Miyotu Software License** (same terms: personal / educational use; no commercial use; attribution required)
-- Updated product description for GitHub and the in-app About defaults
-
-## Install
-
-```bash
-pip install -r requirements.txt
-python main.py
-```
-
-Windows EXE packages may be attached to this release when available; running from source is fully supported.
-
-**Full Changelog:** https://github.com/Taxperia/TaxClip/compare/v1.2.1...v1.2.2
+# TaxClip v1.2.3
+
+Stability, responsiveness, and settings experience update.
+
+## Highlights
+
+- **Stable history layout:** fixed the ninth card and ninth skeleton briefly appearing over the first card when the window opens from the system tray. All nine placeholders now receive their final geometry before the first paint.
+- **Responsive database search:** fuzzy search now runs on a worker thread with its own SQLite connection, searches unloaded history items, and only loads full image payloads for the displayed results.
+- **Reliable Settings window:** prevented repeated miniature child windows, guarded against duplicate Settings instances, and fixed `QThread: Destroyed while thread is still running` shutdown crashes.
+- **Redesigned Settings UI:** added a modern navigation shell, consistent setting rows and controls, theme-aware styling, and a completely redesigned About page.
+- **Preview close fix:** the native close button in the preview window now closes the preview correctly.
+
+## User interface
+
+- Removed the sidebar Quick Actions section.
+- Fixed parent/visibility handling for history cards, loaders, and skeleton placeholders.
+- Removed the skeleton opacity effect that could leave stale paint artifacts on Windows.
+- Reworked the About page with product highlights, update status, and external links.
+- Removed the AI-development badge and authors section from About.
+- Improved toggle styling and visual consistency across Settings pages.
+
+## Performance and reliability
+
+- Added debounced, non-blocking fuzzy search with stale-result protection.
+- Reduced search memory usage by excluding image BLOBs from the initial candidate scan.
+- Added deterministic cleanup for skeleton widgets and stale FlowLayout entries.
+- Deferred FFmpeg and Google Drive checks until their Settings pages are opened.
+- Kept Settings background workers alive until they finish, preventing shutdown crashes.
+- Prepared and laid out history cards while the main window is hidden to eliminate first-frame card jumps.
+
+## Repository maintenance
+
+- Local agent instructions, progress notes, planning files, memory-bank content, build artifacts, and Python bytecode are excluded from Git.
+- Version metadata updated to **1.2.3**.
+
+## Install
+
+```bash
+pip install -r requirements.txt
+python main.py
+```
+
+**Full Changelog:** https://github.com/Taxperia/TaxClip/compare/v1.2.2...v1.2.3
diff --git a/SECURITY.md b/SECURITY.md
deleted file mode 100644
index d02b1f7..0000000
--- a/SECURITY.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# Security Policy
-
-## Supported Versions
-
-The following versions of this project are currently supported with security updates:
-
-| Version | Supported |
-|---------|------------|
-| Latest | ✅ Yes |
-| Older versions | ❌ No |
-
-Only the latest version receives security patches and updates.
-
----
-
-## Reporting a Vulnerability
-
-If you discover a security vulnerability, please **do not open a public issue**.
-
-Instead, report it privately by contacting:
-
-📧 taxperia@miyotu.com
-
-Please include:
-
-- A detailed description of the vulnerability
-- Steps to reproduce the issue
-- Potential impact
-- Suggested fix (if available)
-
-We will review the report as soon as possible and aim to respond within 72 hours.
-
----
-
-## Responsible Disclosure
-
-We kindly ask that you:
-
-- Do not publicly disclose the vulnerability before it has been resolved.
-- Allow reasonable time for investigation and patching.
-- Avoid exploiting the issue beyond proof-of-concept testing.
-
-We appreciate responsible disclosure and security contributions from the community.
diff --git a/assets/i18n/en.json b/assets/i18n/en.json
index 0027ec9..98ea7e3 100644
--- a/assets/i18n/en.json
+++ b/assets/i18n/en.json
@@ -90,10 +90,20 @@
"settings.buttons.save": "Save",
"settings.buttons.apply": "Apply",
- "about.title": "TaxClip – Modern Windows Clipboard Manager",
- "about.desc": "A powerful clipboard manager for text, images, files, snippets, reminders, lists, and drawings. Includes OCR, screen tools, sensitive-data protection, encryption, and Windows Hello support.",
- "about.authors": "Authors",
- "about.website": "Website",
+ "about.kicker": "CLIPBOARD WORKSPACE",
+ "about.desc": "A powerful clipboard manager for text, images, files, snippets, reminders, lists, and drawings. Includes OCR, screen tools, sensitive-data protection, encryption, and Windows Hello support.",
+ "about.feature.organize": "Everything in one place",
+ "about.feature.organize.desc": "Keep text, images, files, and notes organized.",
+ "about.feature.access": "Quick access",
+ "about.feature.access.desc": "Reach search, snippets, and lists from one window.",
+ "about.feature.privacy": "Privacy focused",
+ "about.feature.privacy.desc": "Sensitive-data protection and security controls.",
+ "about.updates": "Version and updates",
+ "about.updates.desc": "Check for TaxClip updates from here.",
+ "about.check_updates": "Check for updates",
+ "about.links": "Links",
+ "about.support": "Support",
+ "about.website": "Website",
"about.patreon": "Patreon",
"preview.title": "Preview",
diff --git a/assets/i18n/tr.json b/assets/i18n/tr.json
index e96e420..16709aa 100644
--- a/assets/i18n/tr.json
+++ b/assets/i18n/tr.json
@@ -93,11 +93,20 @@
"settings.buttons.save": "Kaydet",
"settings.buttons.apply": "Uygula",
- "about.title": "TaxClip – Gelişmiş Pano Yöneticisi",
- "about.desc": "Metin, resim, snippet, hatırlatma, liste ve çizimler için güçlü bir pano yöneticisi. Video kayıt, OCR, hassas veri koruması ve Windows Hello desteği ile tam donanımlı.",
- "about.ai_badge": "🤖 Bu proje yapay zeka (Claude AI) yardımıyla geliştirilmiştir.",
- "about.authors": "Yapımcılar",
- "about.website": "Website",
+ "about.kicker": "PANO ÇALIŞMA ALANI",
+ "about.desc": "Metin, resim, snippet, hatırlatma, liste ve çizimler için güçlü bir pano yöneticisi. Video kayıt, OCR, hassas veri koruması ve Windows Hello desteği ile tam donanımlı.",
+ "about.feature.organize": "Her şey tek yerde",
+ "about.feature.organize.desc": "Metin, görsel, dosya ve not geçmişini düzenli tut.",
+ "about.feature.access": "Hızlı erişim",
+ "about.feature.access.desc": "Arama, snippet ve listelere tek pencereden ulaş.",
+ "about.feature.privacy": "Gizlilik odaklı",
+ "about.feature.privacy.desc": "Hassas veri koruması ve güvenlik seçenekleri.",
+ "about.updates": "Sürüm ve güncellemeler",
+ "about.updates.desc": "TaxClip güncellemelerini buradan kontrol edebilirsin.",
+ "about.check_updates": "Güncellemeleri kontrol et",
+ "about.links": "Bağlantılar",
+ "about.support": "Destek ol",
+ "about.website": "Website",
"about.patreon": "Patreon",
"preview.title": "Önizleme",
diff --git a/assets/icons/alarm_add.svg b/assets/icons/alarm_add.svg
index 9ccfb05..0371042 100644
--- a/assets/icons/alarm_add.svg
+++ b/assets/icons/alarm_add.svg
@@ -1,15 +1 @@
-
-
\ No newline at end of file
+
diff --git a/assets/icons/app.svg b/assets/icons/app.svg
new file mode 100644
index 0000000..8dd1fd6
--- /dev/null
+++ b/assets/icons/app.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/chevron_down.svg b/assets/icons/chevron_down.svg
index 8a2e926..29c3c29 100644
--- a/assets/icons/chevron_down.svg
+++ b/assets/icons/chevron_down.svg
@@ -1,4 +1 @@
-
-
\ No newline at end of file
+
diff --git a/assets/icons/chevron_left.svg b/assets/icons/chevron_left.svg
new file mode 100644
index 0000000..27b9303
--- /dev/null
+++ b/assets/icons/chevron_left.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/chevron_right.svg b/assets/icons/chevron_right.svg
new file mode 100644
index 0000000..b6d5283
--- /dev/null
+++ b/assets/icons/chevron_right.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/clear.svg b/assets/icons/clear.svg
index e77bf8d..e416bd9 100644
--- a/assets/icons/clear.svg
+++ b/assets/icons/clear.svg
@@ -1,5 +1 @@
-
\ No newline at end of file
+
diff --git a/assets/icons/clipboard.svg b/assets/icons/clipboard.svg
index b8b50fd..bdab120 100644
--- a/assets/icons/clipboard.svg
+++ b/assets/icons/clipboard.svg
@@ -1,4 +1 @@
-
\ No newline at end of file
+
diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg
index 97bb740..310883c 100644
--- a/assets/icons/copy.svg
+++ b/assets/icons/copy.svg
@@ -1,4 +1 @@
-
\ No newline at end of file
+
diff --git a/assets/icons/delete.svg b/assets/icons/delete.svg
index 4ddb2a1..9f64504 100644
--- a/assets/icons/delete.svg
+++ b/assets/icons/delete.svg
@@ -1,7 +1 @@
-
\ No newline at end of file
+
diff --git a/assets/icons/delete_reminder.svg b/assets/icons/delete_reminder.svg
index a161e45..b0b968b 100644
--- a/assets/icons/delete_reminder.svg
+++ b/assets/icons/delete_reminder.svg
@@ -1,6 +1 @@
-
+
diff --git a/assets/icons/drawing_add.svg b/assets/icons/drawing_add.svg
index 492ab32..948f516 100644
--- a/assets/icons/drawing_add.svg
+++ b/assets/icons/drawing_add.svg
@@ -1,8 +1 @@
-
+
diff --git a/assets/icons/edit.svg b/assets/icons/edit.svg
index 9628c8a..c513e84 100644
--- a/assets/icons/edit.svg
+++ b/assets/icons/edit.svg
@@ -1,6 +1 @@
-
\ No newline at end of file
+
diff --git a/assets/icons/edit_reminder.svg b/assets/icons/edit_reminder.svg
index cb1a3d8..c513e84 100644
--- a/assets/icons/edit_reminder.svg
+++ b/assets/icons/edit_reminder.svg
@@ -1,4 +1 @@
-
+
diff --git a/assets/icons/expand.svg b/assets/icons/expand.svg
index 8a5a5d8..8c625ae 100644
--- a/assets/icons/expand.svg
+++ b/assets/icons/expand.svg
@@ -1,6 +1 @@
-
\ No newline at end of file
+
diff --git a/assets/icons/filter_all.svg b/assets/icons/filter_all.svg
new file mode 100644
index 0000000..7d2bdcd
--- /dev/null
+++ b/assets/icons/filter_all.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/filter_type.svg b/assets/icons/filter_type.svg
new file mode 100644
index 0000000..1da1589
--- /dev/null
+++ b/assets/icons/filter_type.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/gear.svg b/assets/icons/gear.svg
index 3d47855..6915cc8 100644
--- a/assets/icons/gear.svg
+++ b/assets/icons/gear.svg
@@ -1,17 +1 @@
-
-
\ No newline at end of file
+
diff --git a/assets/icons/lock.svg b/assets/icons/lock.svg
new file mode 100644
index 0000000..e7fa079
--- /dev/null
+++ b/assets/icons/lock.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/logo.ico b/assets/icons/logo.ico
index 3b7b62f..dbb6b94 100644
Binary files a/assets/icons/logo.ico and b/assets/icons/logo.ico differ
diff --git a/assets/icons/logo_256.png b/assets/icons/logo_256.png
index d6febe3..e8dabaa 100644
Binary files a/assets/icons/logo_256.png and b/assets/icons/logo_256.png differ
diff --git a/assets/icons/more_vert.svg b/assets/icons/more_vert.svg
new file mode 100644
index 0000000..18527a7
--- /dev/null
+++ b/assets/icons/more_vert.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/nav_all.svg b/assets/icons/nav_all.svg
new file mode 100644
index 0000000..d806376
--- /dev/null
+++ b/assets/icons/nav_all.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/nav_drawing.svg b/assets/icons/nav_drawing.svg
new file mode 100644
index 0000000..4c12485
--- /dev/null
+++ b/assets/icons/nav_drawing.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/nav_file.svg b/assets/icons/nav_file.svg
new file mode 100644
index 0000000..52e8765
--- /dev/null
+++ b/assets/icons/nav_file.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/nav_image.svg b/assets/icons/nav_image.svg
new file mode 100644
index 0000000..7cf3d71
--- /dev/null
+++ b/assets/icons/nav_image.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/nav_note.svg b/assets/icons/nav_note.svg
new file mode 100644
index 0000000..cc7d0a2
--- /dev/null
+++ b/assets/icons/nav_note.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/nav_reminder.svg b/assets/icons/nav_reminder.svg
new file mode 100644
index 0000000..ac14aa8
--- /dev/null
+++ b/assets/icons/nav_reminder.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/nav_snippet.svg b/assets/icons/nav_snippet.svg
new file mode 100644
index 0000000..569562e
--- /dev/null
+++ b/assets/icons/nav_snippet.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/nav_text.svg b/assets/icons/nav_text.svg
new file mode 100644
index 0000000..5545109
--- /dev/null
+++ b/assets/icons/nav_text.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/nav_todo.svg b/assets/icons/nav_todo.svg
new file mode 100644
index 0000000..49dc751
--- /dev/null
+++ b/assets/icons/nav_todo.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/note_add.svg b/assets/icons/note_add.svg
index f40834d..8450dd6 100644
--- a/assets/icons/note_add.svg
+++ b/assets/icons/note_add.svg
@@ -1,9 +1 @@
-
\ No newline at end of file
+
diff --git a/assets/icons/search.svg b/assets/icons/search.svg
new file mode 100644
index 0000000..bf2cd59
--- /dev/null
+++ b/assets/icons/search.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/share.svg b/assets/icons/share.svg
index f76dc2f..4d879c7 100644
--- a/assets/icons/share.svg
+++ b/assets/icons/share.svg
@@ -1,8 +1 @@
-
\ No newline at end of file
+
diff --git a/assets/icons/snippet_add.svg b/assets/icons/snippet_add.svg
index 516952e..60e634f 100644
--- a/assets/icons/snippet_add.svg
+++ b/assets/icons/snippet_add.svg
@@ -1,7 +1 @@
-
+
diff --git a/assets/icons/sort.svg b/assets/icons/sort.svg
new file mode 100644
index 0000000..3be3477
--- /dev/null
+++ b/assets/icons/sort.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/star.svg b/assets/icons/star.svg
index 3884bc6..4c15e98 100644
--- a/assets/icons/star.svg
+++ b/assets/icons/star.svg
@@ -1,3 +1 @@
-
\ No newline at end of file
+
diff --git a/assets/icons/star_off.svg b/assets/icons/star_off.svg
index bc68687..6561806 100644
--- a/assets/icons/star_off.svg
+++ b/assets/icons/star_off.svg
@@ -1,4 +1 @@
-
\ No newline at end of file
+
diff --git a/assets/icons/star_on.svg b/assets/icons/star_on.svg
index 8f48945..f6e6593 100644
--- a/assets/icons/star_on.svg
+++ b/assets/icons/star_on.svg
@@ -1,4 +1 @@
-
\ No newline at end of file
+
diff --git a/assets/icons/tag.svg b/assets/icons/tag.svg
new file mode 100644
index 0000000..c62fb8d
--- /dev/null
+++ b/assets/icons/tag.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/type_code.svg b/assets/icons/type_code.svg
new file mode 100644
index 0000000..8e62c3c
--- /dev/null
+++ b/assets/icons/type_code.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/type_text.svg b/assets/icons/type_text.svg
new file mode 100644
index 0000000..415779e
--- /dev/null
+++ b/assets/icons/type_text.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/video_camera.svg b/assets/icons/video_camera.svg
index 97bb669..eda21bb 100644
--- a/assets/icons/video_camera.svg
+++ b/assets/icons/video_camera.svg
@@ -1,8 +1 @@
-
+
diff --git a/assets/icons/video_mic.svg b/assets/icons/video_mic.svg
index c486e80..fe90c96 100644
--- a/assets/icons/video_mic.svg
+++ b/assets/icons/video_mic.svg
@@ -1,6 +1 @@
-
+
diff --git a/assets/icons/video_record.svg b/assets/icons/video_record.svg
index a3e1e5d..40bd5d6 100644
--- a/assets/icons/video_record.svg
+++ b/assets/icons/video_record.svg
@@ -1,4 +1 @@
-
+
diff --git a/assets/icons/video_replay.svg b/assets/icons/video_replay.svg
index 4d360fe..0104a5c 100644
--- a/assets/icons/video_replay.svg
+++ b/assets/icons/video_replay.svg
@@ -1,4 +1 @@
-
+
diff --git a/assets/icons/video_stop.svg b/assets/icons/video_stop.svg
index d93856e..de030da 100644
--- a/assets/icons/video_stop.svg
+++ b/assets/icons/video_stop.svg
@@ -1,4 +1 @@
-
+
diff --git a/assets/icons/view_grid.svg b/assets/icons/view_grid.svg
new file mode 100644
index 0000000..c64c193
--- /dev/null
+++ b/assets/icons/view_grid.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/view_list.svg b/assets/icons/view_list.svg
new file mode 100644
index 0000000..e09aee3
--- /dev/null
+++ b/assets/icons/view_list.svg
@@ -0,0 +1 @@
+
diff --git a/assets/sounds/chime.wav b/assets/sounds/chime.wav
deleted file mode 100644
index 73ecdcd..0000000
Binary files a/assets/sounds/chime.wav and /dev/null differ
diff --git a/assets/sounds/ding.wav b/assets/sounds/ding.wav
deleted file mode 100644
index b66cd86..0000000
Binary files a/assets/sounds/ding.wav and /dev/null differ
diff --git a/assets/sounds/notification1.wav b/assets/sounds/notification1.wav
deleted file mode 100644
index 4297392..0000000
Binary files a/assets/sounds/notification1.wav and /dev/null differ
diff --git a/assets/sounds/notification2.wav b/assets/sounds/notification2.wav
deleted file mode 100644
index a3cafc4..0000000
Binary files a/assets/sounds/notification2.wav and /dev/null differ
diff --git a/assets/sounds/notification3.wav b/assets/sounds/notification3.wav
deleted file mode 100644
index e2fbc3b..0000000
Binary files a/assets/sounds/notification3.wav and /dev/null differ
diff --git a/clipstack/__pycache__/__init__.cpython-313.pyc b/clipstack/__pycache__/__init__.cpython-313.pyc
deleted file mode 100644
index 9fafeea..0000000
Binary files a/clipstack/__pycache__/__init__.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/__pycache__/app.cpython-313.pyc b/clipstack/__pycache__/app.cpython-313.pyc
deleted file mode 100644
index 8a59cb1..0000000
Binary files a/clipstack/__pycache__/app.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/__pycache__/clipboard_watcher.cpython-313.pyc b/clipstack/__pycache__/clipboard_watcher.cpython-313.pyc
deleted file mode 100644
index b26bf23..0000000
Binary files a/clipstack/__pycache__/clipboard_watcher.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/__pycache__/hotkey.cpython-313.pyc b/clipstack/__pycache__/hotkey.cpython-313.pyc
deleted file mode 100644
index f050596..0000000
Binary files a/clipstack/__pycache__/hotkey.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/__pycache__/i18n.cpython-313.pyc b/clipstack/__pycache__/i18n.cpython-313.pyc
deleted file mode 100644
index 726ef84..0000000
Binary files a/clipstack/__pycache__/i18n.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/__pycache__/reminder_manager.cpython-313.pyc b/clipstack/__pycache__/reminder_manager.cpython-313.pyc
deleted file mode 100644
index 52f2efc..0000000
Binary files a/clipstack/__pycache__/reminder_manager.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/__pycache__/settings.cpython-313.pyc b/clipstack/__pycache__/settings.cpython-313.pyc
deleted file mode 100644
index 2029ef7..0000000
Binary files a/clipstack/__pycache__/settings.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/__pycache__/sound_player.cpython-313.pyc b/clipstack/__pycache__/sound_player.cpython-313.pyc
deleted file mode 100644
index 0c86fa4..0000000
Binary files a/clipstack/__pycache__/sound_player.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/__pycache__/startup.cpython-313.pyc b/clipstack/__pycache__/startup.cpython-313.pyc
deleted file mode 100644
index bff354c..0000000
Binary files a/clipstack/__pycache__/startup.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/__pycache__/storage.cpython-313.pyc b/clipstack/__pycache__/storage.cpython-313.pyc
deleted file mode 100644
index 543ca20..0000000
Binary files a/clipstack/__pycache__/storage.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/__pycache__/theme_manager.cpython-313.pyc b/clipstack/__pycache__/theme_manager.cpython-313.pyc
deleted file mode 100644
index 45e4533..0000000
Binary files a/clipstack/__pycache__/theme_manager.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/__pycache__/utils.cpython-313.pyc b/clipstack/__pycache__/utils.cpython-313.pyc
deleted file mode 100644
index c757a75..0000000
Binary files a/clipstack/__pycache__/utils.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/__pycache__/utils_crypto.cpython-313.pyc b/clipstack/__pycache__/utils_crypto.cpython-313.pyc
deleted file mode 100644
index b48c4f3..0000000
Binary files a/clipstack/__pycache__/utils_crypto.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/app.py b/clipstack/app.py
index 5e4ef28..625e735 100644
--- a/clipstack/app.py
+++ b/clipstack/app.py
@@ -4,9 +4,9 @@
import traceback
from pathlib import Path
-from PySide6.QtWidgets import QApplication, QSystemTrayIcon, QMenu, QMessageBox, QInputDialog, QLineEdit
+from PySide6.QtWidgets import QApplication, QSystemTrayIcon, QMenu, QMessageBox, QInputDialog, QLineEdit, QWidget
from PySide6.QtGui import QIcon, QAction, QScreen, QPixmap, QGuiApplication
-from PySide6.QtCore import Qt, QTimer, QObject, Signal, QElapsedTimer, QDateTime, QByteArray, QBuffer, QIODevice
+from PySide6.QtCore import Qt, QTimer, QObject, Signal, QElapsedTimer, QDateTime, QByteArray, QBuffer, QIODevice, QEvent
from .clipboard_watcher import ClipboardWatcher
from .ui.main_window import HistoryWindow
@@ -103,6 +103,66 @@ class HotkeyBridge(QObject):
snip = Signal()
+class _SettingsConstructionGuard(QObject):
+ """Prevent child controls from flashing as native windows during setup."""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self._active = False
+ self._owner = None
+ self._blocked = []
+
+ def begin(self, owner: QWidget):
+ self._owner = owner
+ self._blocked.clear()
+ self._active = True
+
+ def finish(self, dialog=None):
+ self._active = False
+ blocked = list(self._blocked)
+ self._blocked.clear()
+ self._owner = None
+
+ for widget in blocked:
+ try:
+ widget.hide()
+ parent = widget.parentWidget()
+ if parent is not None and widget is not dialog:
+ # QWidget.setParent(parent) preserves Qt.Window. Reset the
+ # type explicitly before the dialog is made visible.
+ if widget.isWindow():
+ widget.setParent(parent, Qt.WindowType.Widget)
+ widget.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen, False)
+ widget.show()
+ else:
+ widget.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen, False)
+ except RuntimeError:
+ # The temporary control may already have been deleted.
+ pass
+
+ def eventFilter(self, watched, event):
+ if not self._active or event.type() != QEvent.Type.Show:
+ return False
+ if not isinstance(watched, QWidget) or not watched.isWindow():
+ return False
+ if watched is self._owner or watched.objectName() == "SettingsDialog":
+ return False
+
+ try:
+ print(
+ "[WINDOW GUARD] Beklenmeyen üst-seviye widget engellendi: "
+ f"{type(watched).__name__} object={watched.objectName()!r} "
+ f"title={watched.windowTitle()!r}"
+ )
+ watched.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen, True)
+ watched.hide()
+ if watched not in self._blocked:
+ self._blocked.append(watched)
+ except RuntimeError:
+ pass
+ return True
+
+
class TrayApp:
def __init__(self):
@@ -113,6 +173,8 @@ def __init__(self):
self.app.setApplicationDisplayName("TaxClip")
self.app.setOrganizationName("Miyotu")
self.app.setQuitOnLastWindowClosed(False)
+ self._settings_construction_guard = _SettingsConstructionGuard(self.app)
+ self.app.installEventFilter(self._settings_construction_guard)
data_dir = Path.home() / "AppData" / "Roaming" / "TaxClip"
data_dir.mkdir(parents=True, exist_ok=True)
@@ -233,6 +295,9 @@ def __init__(self):
self._toggle_lock = False
self._toggle_timer = QElapsedTimer()
self._toggle_timer.start()
+ self._settings_dialog = None
+ self._settings_opening = False
+ self._retired_settings_dialogs = set()
self.hotkey = HotkeyManager()
self.hotkey_paste = HotkeyManager()
@@ -307,8 +372,9 @@ def __init__(self):
# Başlangıçta güncelleme kontrolü (sessiz)
self._check_updates_on_startup()
- # reboot pause kaldırma
- if self.settings.get("pause_until") == "reboot":
+ # Süreli/reboot duraklatmayı yeniden başlatmada doğru şekilde devam ettir.
+ pause_until = str(self.settings.get("pause_until", "") or "")
+ if pause_until == "reboot":
self.settings.set("pause_until", "")
self.settings.set("pause_recording", False)
self.settings.save()
@@ -317,6 +383,21 @@ def __init__(self):
self.clipboard_watcher.set_paused(False)
except Exception:
pass
+ elif pause_until:
+ resume_at = QDateTime.fromString(pause_until, Qt.ISODate)
+ remaining_ms = QDateTime.currentDateTime().msecsTo(resume_at) if resume_at.isValid() else 0
+ if remaining_ms > 0:
+ self.settings.set("pause_recording", True)
+ self.settings.save()
+ self.action_pause.setChecked(True)
+ self.clipboard_watcher.set_paused(True)
+ self._pause_resume_timer.start(remaining_ms)
+ else:
+ self.settings.set("pause_until", "")
+ self.settings.set("pause_recording", False)
+ self.settings.save()
+ self.action_pause.setChecked(False)
+ self.clipboard_watcher.set_paused(False)
def _tr(self, key: str, fallback: str, **fmt) -> str:
try:
@@ -834,14 +915,14 @@ def toggle_window(self):
if not self._request_unlock("Pano Geçmişi"):
return
self._last_activity = QDateTime.currentDateTime()
+ try:
+ self.window.prepare_for_show()
+ except Exception as exc:
+ print(f"[WARN] window pre-show refresh: {exc}")
try:
self.window.showCentered()
except Exception:
self.window.show()
- try:
- QTimer.singleShot(0, self.window.reload_items)
- except Exception:
- pass
self.window.activateWindow()
self.window.raise_()
finally:
@@ -962,23 +1043,87 @@ def quick_note_dialog(self):
pass
def open_settings(self):
- dlg = SettingsDialog(self.settings)
- if hasattr(dlg, "applied"):
- try:
- dlg.applied.connect(self._apply_runtime_settings)
- except Exception:
- pass
- if dlg.exec():
- self._apply_runtime_settings()
+ if self._settings_opening:
+ return
+ current = self._settings_dialog
+ if current is not None and current.isVisible():
+ current.raise_()
+ current.activateWindow()
+ return
+
+ self._settings_opening = True
+ # Keep the frameless modal owned by the main window. A parentless
+ # translucent dialog can briefly create separate native windows while
+ # Qt polishes its child controls on Windows.
+ dlg = None
+ try:
+ self._settings_construction_guard.begin(self.window)
try:
- self.action_startup.setChecked(bool(self.settings.get("launch_at_startup", True)))
+ dlg = SettingsDialog(
+ self.settings,
+ parent=self.window,
+ storage=self.storage,
+ )
except Exception:
- pass
- notify_tray(
- self.tray,
- self._tr("notify.settings_updated.title", "Settings updated"),
- self._tr("notify.settings_updated.body", "Changes have been applied."),
+ self._settings_construction_guard.finish(None)
+ raise
+ self._settings_dialog = dlg
+ # Keep the guard active through QDialog.exec()'s synchronous show
+ # phase. Child widgets receive Show events there, before the nested
+ # event loop begins. The zero timer releases them after that phase.
+ QTimer.singleShot(
+ 0,
+ lambda dialog=dlg: self._settings_construction_guard.finish(dialog),
)
+ if hasattr(dlg, "applied"):
+ try:
+ dlg.applied.connect(self._apply_runtime_settings)
+ except Exception:
+ pass
+ if dlg.exec():
+ self._apply_runtime_settings()
+ try:
+ self.action_startup.setChecked(bool(self.settings.get("launch_at_startup", True)))
+ except Exception:
+ pass
+ try:
+ if hasattr(self.window, "refresh_sidebar_from_settings"):
+ self.window.refresh_sidebar_from_settings()
+ if hasattr(self.window, "_refresh_sidebar_counts"):
+ self.window._refresh_sidebar_counts()
+ if dlg._bulk_deleted:
+ self.window.reload_items()
+ try:
+ self.window._load_snippets()
+ except Exception:
+ pass
+ try:
+ self.window._reload_todo_cards()
+ except Exception:
+ pass
+ try:
+ self.window._load_drawings()
+ except Exception:
+ pass
+ except Exception as e:
+ print(f"[WARN] settings post-apply UI: {e}")
+ notify_tray(
+ self.tray,
+ self._tr("notify.settings_updated.title", "Settings updated"),
+ self._tr("notify.settings_updated.body", "Changes have been applied."),
+ )
+ finally:
+ self._settings_construction_guard.finish(dlg)
+ self._settings_dialog = None
+ self._settings_opening = False
+ if dlg is not None:
+ # Settings owns background QThreads (FFmpeg/GDrive probes).
+ # Deleting it here aborts the whole Qt process while they run.
+ self._retired_settings_dialogs.add(dlg)
+ dlg.destroyed.connect(
+ lambda _obj=None, dialog=dlg: self._retired_settings_dialogs.discard(dialog)
+ )
+ dlg.request_dispose_when_idle()
def toggle_pause(self, checked: bool):
self.settings.set("pause_recording", checked)
@@ -1006,9 +1151,13 @@ def toggle_pause(self, checked: bool):
def pause_for_minutes(self, minutes: int):
self.toggle_pause(True)
+ duration_minutes = max(1, minutes)
+ resume_at = QDateTime.currentDateTime().addSecs(duration_minutes * 60)
+ self.settings.set("pause_until", resume_at.toString(Qt.ISODate))
+ self.settings.save()
try:
self._pause_resume_timer.stop()
- self._pause_resume_timer.start(max(1, minutes) * 60_000)
+ self._pause_resume_timer.start(duration_minutes * 60_000)
except Exception:
pass
notify_tray(self.tray, "Gizli Pano", f"Pano kaydı {minutes} dakika duraklatıldı.")
@@ -1036,6 +1185,10 @@ def show_compact_panel(self):
def _open_full_from_compact(self):
if self._compact_panel:
self._compact_panel.hide()
+ try:
+ self.window.prepare_for_show()
+ except Exception as exc:
+ print(f"[WARN] window pre-show refresh: {exc}")
try:
self.window.showCentered()
except Exception:
diff --git a/clipstack/clipboard_watcher.py b/clipstack/clipboard_watcher.py
index 9fe1529..3a84d76 100644
--- a/clipstack/clipboard_watcher.py
+++ b/clipstack/clipboard_watcher.py
@@ -113,6 +113,15 @@ def __init__(self, clipboard: QClipboard, storage: Storage, settings):
self.clipboard.dataChanged.connect(self._on_clip_changed)
self.sensitive_detector = get_sensitive_detector(settings)
+ # Windows'ta dataChanged sinyali bazen tetiklenmiyor — polling fallback
+ self._poll_timer = QTimer(self)
+ self._poll_timer.setInterval(1500)
+ self._poll_timer.timeout.connect(self._poll_clipboard)
+ self._poll_timer.start()
+ self._last_poll_text: str | None = None
+ self._last_poll_img_fp: str | None = None
+ print("[CLIPBOARD] Watcher başlatıldı (dataChanged + polling fallback)")
+
def set_paused(self, paused: bool):
self._paused = paused
@@ -239,8 +248,9 @@ def _on_clip_changed(self):
self.item_added.emit(row)
return
- # 1) Görsel (HTML yoksa)
- if self.clipboard.image() and not md.hasHtml():
+ # 1) Yalnızca gerçek görsel kopyası. Bazı Windows uygulamaları metinle
+ # birlikte eski bir bitmap formatı da taşıyabilir; metni yutma.
+ if md.hasImage() and not md.hasHtml() and not md.hasText():
img: QImage = self.clipboard.image()
if not img.isNull():
ba = QByteArray()
@@ -354,3 +364,36 @@ def _on_clip_changed(self):
if row is not None:
self.item_added.emit(row)
self._schedule_clipboard_clear(norm_text)
+
+ # ---- Polling fallback ----
+ def _poll_clipboard(self):
+ """dataChanged tetiklenmezse periyodik kontrol."""
+ if self._paused:
+ return
+ try:
+ md = self.clipboard.mimeData()
+ if md is None:
+ return
+
+ # Metin kontrolü
+ if md.hasText():
+ txt = (md.text() or "").strip()
+ if txt and txt != self._last_poll_text:
+ self._on_clip_changed()
+ self._last_poll_text = txt
+ return
+
+ # Görsel kontrolü
+ img = self.clipboard.image()
+ if img and not img.isNull():
+ ba = QByteArray()
+ buf = QBuffer(ba)
+ buf.open(QIODevice.WriteOnly)
+ img.save(buf, "PNG")
+ fp = fingerprint_bytes(bytes(ba))
+ if fp != self._last_poll_img_fp:
+ self._on_clip_changed()
+ self._last_poll_img_fp = fp
+ return
+ except Exception as e:
+ print(f"[CLIPBOARD POLL] Hata: {e}")
diff --git a/clipstack/settings.py b/clipstack/settings.py
index 19abe48..6a07e26 100644
--- a/clipstack/settings.py
+++ b/clipstack/settings.py
@@ -70,6 +70,7 @@ def __init__(self, path: Path):
"auto_clear_clipboard_seconds": 30, # 0=kapalı
"pause_until": "",
"compact_mode_default": False,
+ "sidebar_collapsed": False,
}
def load(self):
diff --git a/clipstack/storage.py b/clipstack/storage.py
index 3310640..9fa6193 100644
--- a/clipstack/storage.py
+++ b/clipstack/storage.py
@@ -631,9 +631,115 @@ def get_last_item(self):
return self._decrypt_clip_row(row_dict)
- def list_items(self, limit: int = 200, favorites_only: bool = False, offset: int = 0) -> List[dict]:
+ def count_summary(self) -> dict:
+ """Sidebar rozetleri için hızlı sayımlar."""
cur = self.conn.cursor()
- order = "ORDER BY pinned DESC, favorite DESC, id DESC"
+ def _one(sql: str, args=()) -> int:
+ try:
+ cur.execute(sql, args)
+ row = cur.fetchone()
+ return int(row[0] if row else 0)
+ except Exception:
+ return 0
+
+ all_n = _one("SELECT COUNT(*) FROM clip_items")
+ text_n = _one(
+ "SELECT COUNT(*) FROM clip_items WHERE item_type IN (?, ?)",
+ (int(ClipItemType.TEXT), int(ClipItemType.HTML)),
+ )
+ image_n = _one(
+ "SELECT COUNT(*) FROM clip_items WHERE item_type = ?",
+ (int(ClipItemType.IMAGE),),
+ )
+ files_n = _one(
+ "SELECT COUNT(*) FROM clip_items WHERE item_type = ?",
+ (int(ClipItemType.FILE),),
+ )
+ fav_n = _one("SELECT COUNT(*) FROM clip_items WHERE favorite = 1 OR pinned = 1")
+ notes_n = _one("SELECT COUNT(*) FROM notes")
+ reminders_n = _one("SELECT COUNT(*) FROM reminders")
+ snippets_n = _one("SELECT COUNT(*) FROM snippets")
+ todos_n = _one("SELECT COUNT(*) FROM todo_lists")
+ drawings_n = _one("SELECT COUNT(*) FROM drawings")
+ return {
+ "all": all_n,
+ "text": text_n,
+ "image": image_n,
+ "files": files_n,
+ "fav": fav_n,
+ "notes": notes_n,
+ "reminders": reminders_n,
+ "snippets": snippets_n,
+ "todos": todos_n,
+ "drawings": drawings_n,
+ "video": 0,
+ }
+
+ def count_items_by_types(self, item_types: List[int]) -> int:
+ if not item_types:
+ return 0
+ cur = self.conn.cursor()
+ placeholders = ",".join("?" for _ in item_types)
+ cur.execute(
+ f"SELECT COUNT(*) FROM clip_items WHERE item_type IN ({placeholders})",
+ tuple(item_types),
+ )
+ row = cur.fetchone()
+ return int(row[0] if row else 0)
+
+ def _clip_order_sql(self, sort: str = "newest") -> str:
+ """Clipboard liste sıralaması."""
+ s = (sort or "newest").lower()
+ if s == "oldest":
+ return "ORDER BY datetime(created_at) ASC, id ASC"
+ if s == "used":
+ return "ORDER BY COALESCE(use_count, 0) DESC, datetime(COALESCE(last_used_at, created_at)) DESC, id DESC"
+ # newest (varsayılan) — tarihe göre sırala, favori/pinned ayrımı yapma
+ return "ORDER BY datetime(created_at) DESC, id DESC"
+
+ def list_items_by_types(
+ self,
+ item_types: List[int],
+ limit: int = 9,
+ offset: int = 0,
+ sort: str = "newest",
+ ) -> List[dict]:
+ """Tip filtreli sayfalama — tüm tabloyu Python'da taramadan."""
+ if not item_types:
+ return []
+ cur = self.conn.cursor()
+ order = self._clip_order_sql(sort)
+ placeholders = ",".join("?" for _ in item_types)
+ cur.execute(
+ f"SELECT * FROM clip_items WHERE item_type IN ({placeholders}) {order} LIMIT ? OFFSET ?",
+ (*item_types, limit, offset),
+ )
+ rows = cur.fetchall()
+ result = []
+ for row in rows:
+ row_dict = dict(row)
+ if row_dict.get("item_type") == int(ClipItemType.IMAGE):
+ if not row_dict.get("image_blob") and row_dict.get("text_content"):
+ from pathlib import Path
+ try:
+ image_path = Path(row_dict["text_content"])
+ if image_path.exists():
+ row_dict["image_blob"] = image_path.read_bytes()
+ row_dict["text_content"] = None
+ except Exception as e:
+ print(f"[STORAGE list_items_by_types] Harici resim yükleme hatası: {e}")
+ result.append(self._decrypt_clip_row(row_dict))
+ return result
+
+ def list_items(
+ self,
+ limit: int = 200,
+ favorites_only: bool = False,
+ offset: int = 0,
+ sort: str = "newest",
+ ) -> List[dict]:
+ cur = self.conn.cursor()
+ order = self._clip_order_sql(sort)
if favorites_only:
cur.execute(
f"SELECT * FROM clip_items WHERE favorite = 1 OR pinned = 1 {order} LIMIT ? OFFSET ?",
@@ -753,6 +859,51 @@ def search_items(
date_to: Optional[str] = None,
fuzzy_threshold: int = 60,
limit: int = 100
+ ) -> List[dict]:
+ return self._search_items_with_connection(
+ self.conn,
+ query=query,
+ item_types=item_types,
+ date_from=date_from,
+ date_to=date_to,
+ fuzzy_threshold=fuzzy_threshold,
+ limit=limit,
+ )
+
+ def search_items_threadsafe(
+ self,
+ query: str = "",
+ item_types: Optional[List[ClipItemType]] = None,
+ date_from: Optional[str] = None,
+ date_to: Optional[str] = None,
+ fuzzy_threshold: int = 60,
+ limit: int = 100,
+ ) -> List[dict]:
+ """Run a read-only search on a connection owned by the calling thread."""
+ connection = sqlite3.connect(str(self.path))
+ connection.row_factory = sqlite3.Row
+ try:
+ return self._search_items_with_connection(
+ connection,
+ query=query,
+ item_types=item_types,
+ date_from=date_from,
+ date_to=date_to,
+ fuzzy_threshold=fuzzy_threshold,
+ limit=limit,
+ )
+ finally:
+ connection.close()
+
+ def _search_items_with_connection(
+ self,
+ connection: sqlite3.Connection,
+ query: str = "",
+ item_types: Optional[List[ClipItemType]] = None,
+ date_from: Optional[str] = None,
+ date_to: Optional[str] = None,
+ fuzzy_threshold: int = 60,
+ limit: int = 100,
) -> List[dict]:
"""
Gelişmiş arama fonksiyonu
@@ -763,10 +914,16 @@ def search_items(
- fuzzy_threshold: 0-100 arası benzerlik skoru (60 = %60 benzer)
- limit: Maksimum sonuç sayısı
"""
- cur = self.conn.cursor()
+ cur = connection.cursor()
- # SQL query oluştur
- sql = "SELECT * FROM clip_items WHERE 1=1"
+ # Search candidates without loading image BLOBs. Full payloads are
+ # fetched only for the final page of matches.
+ columns = [row[1] for row in cur.execute("PRAGMA table_info(clip_items)").fetchall()]
+ select_columns = ", ".join(
+ "NULL AS image_blob" if name == "image_blob" else f'"{name}"'
+ for name in columns
+ )
+ sql = f"SELECT {select_columns} FROM clip_items WHERE 1=1"
params = []
# Tip filtresi
@@ -792,18 +949,6 @@ def search_items(
result = []
for row in rows:
row_dict = dict(row)
-
- # Harici resim yükleme
- if row_dict.get("item_type") == int(ClipItemType.IMAGE):
- if not row_dict.get("image_blob") and row_dict.get("text_content"):
- try:
- image_path = Path(row_dict["text_content"])
- if image_path.exists():
- row_dict["image_blob"] = image_path.read_bytes()
- row_dict["text_content"] = None
- except Exception:
- pass
-
row_dict = self._decrypt_clip_row(row_dict)
# Query ile fuzzy match kontrolü
@@ -815,6 +960,12 @@ def search_items(
text_parts.append(_strip_html_tags(row_dict["html_content"]))
if row_dict.get("ocr_text"):
text_parts.append(_normalize_search_text(row_dict["ocr_text"]))
+ if row_dict.get("custom_title"):
+ text_parts.append(_normalize_search_text(row_dict["custom_title"]))
+ if row_dict.get("tags"):
+ text_parts.append(_normalize_search_text(row_dict["tags"]))
+ if row_dict.get("source_app"):
+ text_parts.append(_normalize_search_text(row_dict["source_app"]))
searchable_text = " ".join(part for part in text_parts if part).strip()
if searchable_text:
@@ -827,10 +978,38 @@ def search_items(
row_dict["_search_score"] = 100
result.append(row_dict)
- # Skora göre sırala
+ # Skora göre sırala ve yalnızca gösterilecek kayıtların tam
+ # resim verisini getir.
result.sort(key=lambda x: (x.get("_search_score", 0), x.get("id", 0)), reverse=True)
-
- return result[:limit]
+ selected = result[:limit]
+ if not selected:
+ return []
+
+ selected_ids = [int(row["id"]) for row in selected]
+ placeholders = ",".join("?" for _ in selected_ids)
+ full_rows = connection.execute(
+ f"SELECT * FROM clip_items WHERE id IN ({placeholders})",
+ selected_ids,
+ ).fetchall()
+ full_by_id = {int(row["id"]): dict(row) for row in full_rows}
+
+ hydrated = []
+ for match in selected:
+ row_dict = full_by_id.get(int(match["id"]), dict(match))
+ if row_dict.get("item_type") == int(ClipItemType.IMAGE):
+ if not row_dict.get("image_blob") and row_dict.get("text_content"):
+ try:
+ image_path = Path(row_dict["text_content"])
+ if image_path.exists():
+ row_dict["image_blob"] = image_path.read_bytes()
+ row_dict["text_content"] = None
+ except Exception:
+ pass
+ row_dict = self._decrypt_clip_row(row_dict)
+ row_dict["_search_score"] = match.get("_search_score", 0)
+ hydrated.append(row_dict)
+
+ return hydrated
def delete_item(self, item_id: int):
cur = self.conn.cursor()
@@ -1442,6 +1621,11 @@ def list_drawings(self, limit: int = 100) -> List[dict]:
self._decrypt_row_fields(dict(row), ("title", "image_data"))
for row in rows
]
+
+ def clear_all_drawings(self) -> None:
+ cur = self.conn.cursor()
+ cur.execute("DELETE FROM drawings")
+ self.conn.commit()
def get_drawing(self, drawing_id: int) -> dict | None:
"""Tek bir çizim getir (image_data ile)"""
diff --git a/clipstack/theme_manager.py b/clipstack/theme_manager.py
index 32eddcb..3c55a70 100644
--- a/clipstack/theme_manager.py
+++ b/clipstack/theme_manager.py
@@ -89,15 +89,20 @@ def apply(self, theme_key: str):
"default": "styles/theme_default.qss",
"dark": "styles/theme_dark.qss",
"light": "styles/theme_light.qss",
- "purple": "styles/theme_purple.qss",
- "cyberpunk": "styles/theme_cyberpunk.qss",
- "sunset": "styles/theme_sunset.qss",
- "matrix": "styles/theme_matrix.qss",
- "ocean": "styles/theme_ocean.qss",
- "retro": "styles/theme_retro.qss",
+ "purple": "styles/theme_purple.qss",
+ "cyberpunk": "styles/theme_cyberpunk.qss",
+ "sunset": "styles/theme_sunset.qss",
+ "matrix": "styles/theme_matrix.qss",
+ "ocean": "styles/theme_ocean.qss",
+ "retro": "styles/theme_retro.qss",
}.get(theme_key, "styles/theme_default.qss")
try:
qss = resource_path(qss_file).read_text("utf-8")
+ try:
+ shared = resource_path("styles/widgets/sidebar.qss").read_text("utf-8")
+ qss = f"{qss.rstrip()}\n\n{shared}\n"
+ except Exception:
+ pass
tooltip_qss = self._build_tooltip_qss(theme_key)
app.setStyleSheet(f"{qss.rstrip()}\n\n{tooltip_qss}\n")
self._tooltip_enforcer.set_tooltip_qss(tooltip_qss)
@@ -108,34 +113,34 @@ def apply(self, theme_key: str):
# Palette minimal tweak
pal = app.palette()
if theme_key == "light":
- pal.setColor(QPalette.Window, QColor("#f4f7fb"))
- pal.setColor(QPalette.Base, QColor("#ffffff"))
- pal.setColor(QPalette.Text, QColor("#111111"))
- pal.setColor(QPalette.WindowText, QColor("#111111"))
- elif theme_key == "retro":
- pal.setColor(QPalette.Window, QColor("#0078d7"))
- pal.setColor(QPalette.Base, QColor("#ffffff"))
- pal.setColor(QPalette.Text, QColor("#000000"))
- pal.setColor(QPalette.WindowText, QColor("#ffffff"))
- elif theme_key == "matrix":
- pal.setColor(QPalette.Window, QColor("#0d0d0d"))
- pal.setColor(QPalette.Base, QColor("#001a00"))
- pal.setColor(QPalette.Text, QColor("#00ff41"))
- pal.setColor(QPalette.WindowText, QColor("#00ff41"))
- elif theme_key == "cyberpunk":
- pal.setColor(QPalette.Window, QColor("#0d0221"))
- pal.setColor(QPalette.Base, QColor("#1a0933"))
- pal.setColor(QPalette.Text, QColor("#f0e7ff"))
- pal.setColor(QPalette.WindowText, QColor("#f0e7ff"))
- elif theme_key in ("sunset", "ocean"):
- pal.setColor(QPalette.Window, QColor("#1a0a2e"))
- pal.setColor(QPalette.Base, QColor("#1a0a2e"))
- pal.setColor(QPalette.Text, QColor("#ffe5ec"))
- pal.setColor(QPalette.WindowText, QColor("#ffe5ec"))
- else:
- pal.setColor(QPalette.Window, QColor("#0f172a"))
- pal.setColor(QPalette.Base, QColor("#0f172a"))
- pal.setColor(QPalette.Text, QColor("#e2e8f0"))
+ pal.setColor(QPalette.Window, QColor("#f4f7fb"))
+ pal.setColor(QPalette.Base, QColor("#ffffff"))
+ pal.setColor(QPalette.Text, QColor("#111111"))
+ pal.setColor(QPalette.WindowText, QColor("#111111"))
+ elif theme_key == "retro":
+ pal.setColor(QPalette.Window, QColor("#0078d7"))
+ pal.setColor(QPalette.Base, QColor("#ffffff"))
+ pal.setColor(QPalette.Text, QColor("#000000"))
+ pal.setColor(QPalette.WindowText, QColor("#ffffff"))
+ elif theme_key == "matrix":
+ pal.setColor(QPalette.Window, QColor("#0d0d0d"))
+ pal.setColor(QPalette.Base, QColor("#001a00"))
+ pal.setColor(QPalette.Text, QColor("#00ff41"))
+ pal.setColor(QPalette.WindowText, QColor("#00ff41"))
+ elif theme_key == "cyberpunk":
+ pal.setColor(QPalette.Window, QColor("#0d0221"))
+ pal.setColor(QPalette.Base, QColor("#1a0933"))
+ pal.setColor(QPalette.Text, QColor("#f0e7ff"))
+ pal.setColor(QPalette.WindowText, QColor("#f0e7ff"))
+ elif theme_key in ("sunset", "ocean"):
+ pal.setColor(QPalette.Window, QColor("#1a0a2e"))
+ pal.setColor(QPalette.Base, QColor("#1a0a2e"))
+ pal.setColor(QPalette.Text, QColor("#ffe5ec"))
+ pal.setColor(QPalette.WindowText, QColor("#ffe5ec"))
+ else:
+ pal.setColor(QPalette.Window, QColor("#0f172a"))
+ pal.setColor(QPalette.Base, QColor("#0f172a"))
+ pal.setColor(QPalette.Text, QColor("#e2e8f0"))
pal.setColor(QPalette.WindowText, QColor("#e2e8f0"))
app.setPalette(pal)
diff --git a/clipstack/ui/__pycache__/flow_layout.cpython-313.pyc b/clipstack/ui/__pycache__/flow_layout.cpython-313.pyc
deleted file mode 100644
index bf20725..0000000
Binary files a/clipstack/ui/__pycache__/flow_layout.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/ui/__pycache__/item_preview_dialog.cpython-313.pyc b/clipstack/ui/__pycache__/item_preview_dialog.cpython-313.pyc
deleted file mode 100644
index c0e40b4..0000000
Binary files a/clipstack/ui/__pycache__/item_preview_dialog.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/ui/__pycache__/item_widget.cpython-313.pyc b/clipstack/ui/__pycache__/item_widget.cpython-313.pyc
deleted file mode 100644
index cd4d263..0000000
Binary files a/clipstack/ui/__pycache__/item_widget.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/ui/__pycache__/main_window.cpython-313.pyc b/clipstack/ui/__pycache__/main_window.cpython-313.pyc
deleted file mode 100644
index e765886..0000000
Binary files a/clipstack/ui/__pycache__/main_window.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/ui/__pycache__/note_widget.cpython-313.pyc b/clipstack/ui/__pycache__/note_widget.cpython-313.pyc
deleted file mode 100644
index 8b54e5d..0000000
Binary files a/clipstack/ui/__pycache__/note_widget.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/ui/__pycache__/reminder_dialog.cpython-313.pyc b/clipstack/ui/__pycache__/reminder_dialog.cpython-313.pyc
deleted file mode 100644
index 479a0c4..0000000
Binary files a/clipstack/ui/__pycache__/reminder_dialog.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/ui/__pycache__/reminder_notification.cpython-313.pyc b/clipstack/ui/__pycache__/reminder_notification.cpython-313.pyc
deleted file mode 100644
index e69a8d3..0000000
Binary files a/clipstack/ui/__pycache__/reminder_notification.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/ui/__pycache__/reminder_widget.cpython-313.pyc b/clipstack/ui/__pycache__/reminder_widget.cpython-313.pyc
deleted file mode 100644
index e55fd28..0000000
Binary files a/clipstack/ui/__pycache__/reminder_widget.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/ui/__pycache__/settings_dialog.cpython-313.pyc b/clipstack/ui/__pycache__/settings_dialog.cpython-313.pyc
deleted file mode 100644
index 8dced7a..0000000
Binary files a/clipstack/ui/__pycache__/settings_dialog.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/ui/__pycache__/toast.cpython-313.pyc b/clipstack/ui/__pycache__/toast.cpython-313.pyc
deleted file mode 100644
index 80d37ae..0000000
Binary files a/clipstack/ui/__pycache__/toast.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/ui/bulk_delete_dialog.py b/clipstack/ui/bulk_delete_dialog.py
new file mode 100644
index 0000000..b25295f
--- /dev/null
+++ b/clipstack/ui/bulk_delete_dialog.py
@@ -0,0 +1,87 @@
+from __future__ import annotations
+
+from typing import Callable, Dict, List, Optional
+
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import (
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QCheckBox,
+ QPushButton, QMessageBox, QFrame,
+)
+
+
+class BulkDeleteDialog(QDialog):
+ """Ayarlar üzerinden toplu silme — checkbox ile hedef seçimi."""
+
+ TARGETS = [
+ ("clips", "Pano geçmişi (tüm içerikler)"),
+ ("notes", "Notlar"),
+ ("reminders", "Hatırlatmalar"),
+ ("snippets", "Snippet'ler"),
+ ("todos", "Listeler / görevler"),
+ ("drawings", "Çizimler"),
+ ]
+
+ def __init__(self, parent=None, on_delete: Optional[Callable[[List[str]], None]] = None):
+ super().__init__(parent)
+ self.setWindowTitle("Toplu silme")
+ self.setMinimumWidth(420)
+ self._on_delete = on_delete
+ self._checks: Dict[str, QCheckBox] = {}
+
+ lay = QVBoxLayout(self)
+ lay.setSpacing(12)
+
+ info = QLabel(
+ "Silmek istediğiniz içerik türlerini seçin. "
+ "Onayladıktan sonra işlem geri alınamaz."
+ )
+ info.setWordWrap(True)
+ info.setObjectName("BulkDeleteInfo")
+ lay.addWidget(info)
+
+ box = QFrame()
+ box.setObjectName("BulkDeleteBox")
+ box_lay = QVBoxLayout(box)
+ box_lay.setSpacing(8)
+ for key, label in self.TARGETS:
+ cb = QCheckBox(label)
+ cb.setChecked(False)
+ self._checks[key] = cb
+ box_lay.addWidget(cb)
+ lay.addWidget(box)
+
+ btns = QHBoxLayout()
+ btns.addStretch(1)
+ self.btn_cancel = QPushButton("İptal")
+ self.btn_cancel.clicked.connect(self.reject)
+ btns.addWidget(self.btn_cancel)
+ self.btn_delete = QPushButton("Sil")
+ self.btn_delete.setProperty("class", "accent")
+ self.btn_delete.clicked.connect(self._confirm_and_delete)
+ btns.addWidget(self.btn_delete)
+ lay.addLayout(btns)
+
+ def selected_targets(self) -> List[str]:
+ return [k for k, cb in self._checks.items() if cb.isChecked()]
+
+ def _confirm_and_delete(self):
+ targets = self.selected_targets()
+ if not targets:
+ QMessageBox.information(self, "Toplu silme", "En az bir içerik türü seçin.")
+ return
+ labels = [lbl for k, lbl in self.TARGETS if k in targets]
+ mb = QMessageBox(self)
+ mb.setIcon(QMessageBox.Warning)
+ mb.setWindowTitle("Emin misiniz?")
+ mb.setText(
+ "Aşağıdaki içerikler kalıcı olarak silinecek:\n\n• "
+ + "\n• ".join(labels)
+ + "\n\nBu işlem geri alınamaz. Devam edilsin mi?"
+ )
+ mb.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
+ mb.setDefaultButton(QMessageBox.No)
+ if mb.exec() != QMessageBox.Yes:
+ return
+ if callable(self._on_delete):
+ self._on_delete(targets)
+ self.accept()
diff --git a/clipstack/ui/card_common.py b/clipstack/ui/card_common.py
new file mode 100644
index 0000000..a98dfa2
--- /dev/null
+++ b/clipstack/ui/card_common.py
@@ -0,0 +1,135 @@
+"""Kart ortak yardımcılar — tip rozeti, tarih, kod vurgusu, görünüm modu."""
+from __future__ import annotations
+
+from PySide6.QtCore import Qt, QSize
+from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QToolButton, QWidget, QSizePolicy
+
+from ..utils import svg_icon
+
+
+CARD_W = 260
+CARD_H = 168
+LIST_H = 76
+
+
+def format_card_date(raw: str) -> str:
+ if not raw:
+ return ""
+ try:
+ date_part = str(raw).split(" ")[0]
+ y, m, d = date_part.split("-")
+ return f"{d}.{m}.{y}"
+ except Exception:
+ return str(raw)[:16]
+
+
+def highlight_code(text: str, max_chars: int = 400) -> str:
+ """QLabel RichText için renkli kod — span yerine (QSS color mirasını aşar)."""
+ import re
+
+ snippet = text if len(text) <= max_chars else text[: max_chars - 1] + "…"
+ try:
+ from pygments import highlight
+ from pygments.lexers import guess_lexer, TextLexer
+ from pygments.formatters import HtmlFormatter
+ try:
+ lexer = guess_lexer(snippet)
+ except Exception:
+ lexer = TextLexer()
+ formatter = HtmlFormatter(
+ noclasses=True,
+ nobackground=True,
+ style="monokai",
+ nowrap=True,
+ )
+ html = highlight(snippet, lexer, formatter)
+ html = re.sub(
+ r']*>',
+ r'',
+ html,
+ flags=re.IGNORECASE,
+ )
+ html = html.replace("", "")
+ return (
+ "
{html}
"
+ )
+ except Exception:
+ escaped = (
+ snippet.replace("&", "&").replace("<", "<").replace(">", ">")
+ )
+ return (
+ f""
+ f"{escaped}"
+ )
+
+
+def apply_card_view_mode(widget: QWidget, mode: str, preview: QLabel | None = None) -> None:
+ """grid | list — kart boyutunu ve önizleme yüksekliğini ayarla."""
+ mode = "list" if mode == "list" else "grid"
+ widget._view_mode = mode
+ if mode == "list":
+ widget.setMinimumSize(320, LIST_H)
+ widget.setMaximumSize(16777215, LIST_H)
+ try:
+ sp = widget.sizePolicy()
+ sp.setHorizontalPolicy(QSizePolicy.Expanding)
+ sp.setVerticalPolicy(QSizePolicy.Fixed)
+ widget.setSizePolicy(sp)
+ except Exception:
+ pass
+ if preview is not None:
+ preview.setMaximumHeight(28)
+ preview.setWordWrap(False)
+ else:
+ widget.setMinimumSize(CARD_W, CARD_H)
+ widget.setMaximumSize(CARD_W, CARD_H)
+ widget.setFixedSize(CARD_W, CARD_H)
+ try:
+ sp = widget.sizePolicy()
+ sp.setHorizontalPolicy(QSizePolicy.Fixed)
+ sp.setVerticalPolicy(QSizePolicy.Fixed)
+ widget.setSizePolicy(sp)
+ except Exception:
+ pass
+ if preview is not None:
+ preview.setMaximumHeight(16777215)
+ preview.setWordWrap(True)
+
+
+def make_type_badge(icon_path: str = "assets/icons/type_text.svg", glyph: str = "T") -> QFrame:
+ badge = QFrame()
+ badge.setObjectName("ItemTypeBadge")
+ badge.setFixedSize(28, 28)
+ badge.setAttribute(Qt.WA_StyledBackground, True)
+ lay = QHBoxLayout(badge)
+ lay.setContentsMargins(0, 0, 0, 0)
+ icon = QLabel()
+ icon.setAlignment(Qt.AlignCenter)
+ icon.setFixedSize(28, 28)
+ icon.setStyleSheet("background: transparent; border: none;")
+ try:
+ icon.setPixmap(svg_icon(icon_path).pixmap(QSize(16, 16)))
+ except Exception:
+ icon.setText(glyph)
+ lay.addWidget(icon)
+ return badge
+
+
+def make_tool_button(object_name: str, icon_path: str = "", tip: str = "", text: str = "") -> QToolButton:
+ btn = QToolButton()
+ btn.setObjectName(object_name)
+ btn.setAutoRaise(True)
+ btn.setStyleSheet("background: transparent; border: none;")
+ btn.setIconSize(QSize(16, 16))
+ if tip:
+ btn.setToolTip(tip)
+ if icon_path:
+ try:
+ btn.setIcon(svg_icon(icon_path))
+ except Exception:
+ if text:
+ btn.setText(text)
+ elif text:
+ btn.setText(text)
+ return btn
diff --git a/clipstack/ui/drawing_card_widget.py b/clipstack/ui/drawing_card_widget.py
index a990b55..609c2fd 100644
--- a/clipstack/ui/drawing_card_widget.py
+++ b/clipstack/ui/drawing_card_widget.py
@@ -1,132 +1,108 @@
-"""
-Drawing Card Widget - ItemWidget tarzı çizim kartı
-"""
-from PySide6.QtWidgets import (
- QFrame, QVBoxLayout, QHBoxLayout, QLabel, QPushButton
-)
-from PySide6.QtCore import Qt, Signal
-from PySide6.QtGui import QFont, QPixmap
-
-
-CARD_W = 260
-CARD_H = 160
-
-
-class DrawingCardWidget(QFrame):
- """Çizim kartı"""
-
- edit_requested = Signal(int) # drawing_id
- delete_requested = Signal(int) # drawing_id
-
+"""Çizim kartı — ItemCard düzeni."""
+from __future__ import annotations
+
+import base64
+
+from PySide6.QtCore import Qt, Signal, QByteArray
+from PySide6.QtGui import QPixmap, QImage
+from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QToolButton
+
+from .card_common import CARD_W, CARD_H, LIST_H, format_card_date, make_type_badge, make_tool_button, apply_card_view_mode
+
+
+class DrawingCardWidget(QWidget):
+ edit_requested = Signal(int)
+ delete_requested = Signal(int)
+ LIST_H = LIST_H
+
def __init__(self, drawing: dict, parent=None):
super().__init__(parent)
self.drawing = drawing
self.drawing_id = drawing["id"]
-
- self.setObjectName("DrawingCard")
- self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
+ self._view_mode = "grid"
+
+ self.setObjectName("ItemCard")
+ self.setAttribute(Qt.WA_StyledBackground, True)
self.setFixedSize(CARD_W, CARD_H)
-
+ self.setCursor(Qt.PointingHandCursor)
+
layout = QVBoxLayout(self)
- layout.setContentsMargins(10, 10, 10, 10)
+ layout.setContentsMargins(12, 12, 12, 10)
layout.setSpacing(8)
-
- # Thumbnail
- self.thumbnail = QLabel()
- self.thumbnail.setAlignment(Qt.AlignmentFlag.AlignCenter)
- self.thumbnail.setFixedSize(240, 90)
- self.thumbnail.setStyleSheet("""
- QLabel {
- background: white;
- border: 1px solid #dee2e6;
- border-radius: 4px;
- }
- """)
- layout.addWidget(self.thumbnail)
-
- # Tarih
- date_lbl = QLabel(self._format_date(drawing.get("created_at", "")))
- date_lbl.setFont(QFont("Segoe UI", 8))
- date_lbl.setStyleSheet("color: #6c757d;")
- layout.addWidget(date_lbl)
-
- # Butonlar
- btn_layout = QHBoxLayout()
- btn_layout.setSpacing(5)
-
- btn_edit = self._create_button("✏️ Düzenle")
- btn_edit.clicked.connect(lambda: self.edit_requested.emit(self.drawing_id))
- btn_layout.addWidget(btn_edit)
-
- btn_delete = self._create_button("🗑️ Sil")
- btn_delete.clicked.connect(lambda: self.delete_requested.emit(self.drawing_id))
- btn_layout.addWidget(btn_delete)
-
- layout.addLayout(btn_layout)
- layout.addStretch()
-
- # Thumbnail yükle
+
+ top = QHBoxLayout()
+ top.setSpacing(6)
+ top.addWidget(make_type_badge("assets/icons/nav_drawing.svg", "✎"))
+
+ title = drawing.get("title") or "Çizim"
+ self.lbl_title = QLabel(str(title)[:36])
+ self.lbl_title.setObjectName("ItemTitle")
+ self.lbl_title.setStyleSheet("font-weight: 600; font-size: 11px; background: transparent; border: none;")
+ top.addWidget(self.lbl_title, 1)
+
+ self.btn_more = make_tool_button("MoreButton", "assets/icons/more_vert.svg", "Diğer", "⋮")
+ self.btn_more.clicked.connect(lambda: self.edit_requested.emit(self.drawing_id))
+ top.addWidget(self.btn_more)
+ layout.addLayout(top)
+
+ self.preview = QLabel()
+ self.preview.setObjectName("ItemPreview")
+ self.preview.setAlignment(Qt.AlignCenter)
+ self.preview.setStyleSheet("background: transparent; border: none;")
+ layout.addWidget(self.preview, 1)
+
+ bottom = QHBoxLayout()
+ self.lbl_meta = QLabel(format_card_date(str(drawing.get("created_at", "") or "")))
+ self.lbl_meta.setObjectName("MetaLabel")
+ self.lbl_meta.setStyleSheet("font-size: 10px; color: #94a3b8; background: transparent; border: none;")
+ bottom.addWidget(self.lbl_meta, 1)
+
+ self.btn_copy = make_tool_button("CopyButton", "assets/icons/expand.svg", "Aç")
+ self.btn_copy.clicked.connect(lambda: self.edit_requested.emit(self.drawing_id))
+ bottom.addWidget(self.btn_copy)
+ layout.addLayout(bottom)
+
self._load_thumbnail()
-
- def _create_button(self, text: str) -> QPushButton:
- """Flat buton"""
- btn = QPushButton(text)
- btn.setFixedSize(115, 28)
- btn.setStyleSheet("""
- QPushButton {
- background: transparent;
- border: 1px solid #dee2e6;
- border-radius: 4px;
- color: white;
- font-size: 11px;
- padding: 4px 8px;
- }
- QPushButton:hover {
- background: #e9ecef;
- border-color: #adb5bd;
- }
- """)
- return btn
-
- def _format_date(self, date_str: str) -> str:
- """Tarih formatla"""
try:
- from datetime import datetime
- dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
- return dt.strftime("%d.%m.%Y %H:%M")
- except:
- return date_str
-
+ mode = getattr(getattr(self.window(), "chip_bar", None), "view_mode", "grid")
+ self.set_view_mode(mode)
+ except Exception:
+ pass
+
+ def set_view_mode(self, mode: str):
+ apply_card_view_mode(self, mode, getattr(self, "preview", None))
+ if mode == "list" and self.preview.pixmap() and not self.preview.pixmap().isNull():
+ thumb = self.preview.pixmap().scaled(48, 40, Qt.KeepAspectRatio, Qt.SmoothTransformation)
+ self.preview.setPixmap(thumb)
+ self.updateGeometry()
+
def _load_thumbnail(self):
- """Thumbnail yükle"""
try:
- import base64
- from PySide6.QtGui import QImage
-
- # image_data veya image key'ini kullan
img_key = "image_data" if "image_data" in self.drawing else "image"
- img_b64 = self.drawing[img_key]
-
- # Base64 padding düzelt
+ img_b64 = self.drawing.get(img_key)
+ if not img_b64:
+ self.preview.setText("Çizim yok")
+ return
if isinstance(img_b64, str):
padding = len(img_b64) % 4
if padding:
- img_b64 += '=' * (4 - padding)
-
- img_bytes = base64.b64decode(img_b64)
- img = QImage()
- img.loadFromData(img_bytes)
-
- # Ölçeklendir
- pixmap = QPixmap.fromImage(img)
- scaled = pixmap.scaled(
- 240, 90,
- Qt.AspectRatioMode.KeepAspectRatio,
- Qt.TransformationMode.SmoothTransformation
- )
-
- self.thumbnail.setPixmap(scaled)
-
- except Exception as e:
- self.thumbnail.setText("⚠️ Yüklenemedi")
- print(f"Thumbnail yükleme hatası: {e}")
+ img_b64 += "=" * (4 - padding)
+ img_bytes = base64.b64decode(img_b64)
+ else:
+ img_bytes = bytes(img_b64)
+ pm = QPixmap()
+ pm.loadFromData(QByteArray(img_bytes))
+ if pm.isNull():
+ self.preview.setText("Önizleme yok")
+ return
+ thumb = pm.scaled(230, 100, Qt.KeepAspectRatio, Qt.SmoothTransformation)
+ self.preview.setPixmap(thumb)
+ except Exception:
+ self.preview.setText("Önizleme hatası")
+
+ def mousePressEvent(self, event):
+ if event.button() == Qt.LeftButton:
+ child = self.childAt(event.pos())
+ if not isinstance(child, QToolButton):
+ self.edit_requested.emit(self.drawing_id)
+ super().mousePressEvent(event)
diff --git a/clipstack/ui/filter_chip_bar.py b/clipstack/ui/filter_chip_bar.py
new file mode 100644
index 0000000..9e94479
--- /dev/null
+++ b/clipstack/ui/filter_chip_bar.py
@@ -0,0 +1,175 @@
+"""İçerik filtre çubuğu — Tümü / En Yeni / Tür / Etiket / Uygulama + görünüm."""
+from __future__ import annotations
+
+from PySide6.QtCore import Qt, Signal, QSize
+from PySide6.QtWidgets import (
+ QHBoxLayout, QPushButton, QMenu, QFrame, QToolButton,
+)
+
+from ..utils import svg_icon
+
+
+class FilterChipBar(QFrame):
+ filter_changed = Signal()
+ view_mode_changed = Signal(str) # "grid" | "list"
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setObjectName("FilterChipBar")
+ self.setAttribute(Qt.WA_StyledBackground, True)
+
+ self.sort_mode = "newest"
+ self.type_filter = "all"
+ self.tag_filter = ""
+ self.app_filter = ""
+ self.view_mode = "grid"
+
+ lay = QHBoxLayout(self)
+ lay.setContentsMargins(10, 4, 10, 4)
+ lay.setSpacing(8)
+
+ self.btn_all = self._chip("Tümü", "assets/icons/filter_all.svg", checkable=True, checked=True)
+ self.btn_all.clicked.connect(lambda: self._set_type("all"))
+ lay.addWidget(self.btn_all)
+
+ self.btn_sort = self._chip("En Yeni", "assets/icons/sort.svg")
+ self.btn_sort.clicked.connect(self._open_sort_menu)
+ lay.addWidget(self.btn_sort)
+
+ self.btn_type = self._chip("Tür", "assets/icons/filter_type.svg")
+ self.btn_type.clicked.connect(self._open_type_menu)
+ lay.addWidget(self.btn_type)
+
+ self.btn_tag = self._chip("Etiket", "assets/icons/tag.svg")
+ self.btn_tag.clicked.connect(self._open_tag_menu)
+ lay.addWidget(self.btn_tag)
+
+ self.btn_app = self._chip("Uygulama", "assets/icons/app.svg")
+ self.btn_app.clicked.connect(self._open_app_menu)
+ lay.addWidget(self.btn_app)
+
+ lay.addStretch(1)
+
+ self.btn_grid = QToolButton()
+ self.btn_grid.setObjectName("ViewModeBtn")
+ self.btn_grid.setIcon(svg_icon("assets/icons/view_grid.svg"))
+ self.btn_grid.setIconSize(QSize(16, 16))
+ self.btn_grid.setCheckable(True)
+ self.btn_grid.setChecked(True)
+ self.btn_grid.setToolTip("Izgara görünümü")
+ self.btn_grid.setCursor(Qt.PointingHandCursor)
+ self.btn_grid.clicked.connect(lambda: self._set_view("grid"))
+ lay.addWidget(self.btn_grid)
+
+ self.btn_list = QToolButton()
+ self.btn_list.setObjectName("ViewModeBtn")
+ self.btn_list.setIcon(svg_icon("assets/icons/view_list.svg"))
+ self.btn_list.setIconSize(QSize(16, 16))
+ self.btn_list.setCheckable(True)
+ self.btn_list.setToolTip("Liste görünümü")
+ self.btn_list.setCursor(Qt.PointingHandCursor)
+ self.btn_list.clicked.connect(lambda: self._set_view("list"))
+ lay.addWidget(self.btn_list)
+
+ self._apps: list[str] = []
+ self._tags: list[str] = []
+
+ def _chip(self, text: str, icon_path: str = "", checkable: bool = False, checked: bool = False) -> QPushButton:
+ b = QPushButton(text)
+ b.setObjectName("FilterChip")
+ b.setCheckable(checkable)
+ b.setChecked(checked)
+ b.setCursor(Qt.PointingHandCursor)
+ b.setMinimumHeight(30)
+ b.setIconSize(QSize(14, 14))
+ if icon_path:
+ try:
+ b.setIcon(svg_icon(icon_path))
+ except Exception:
+ pass
+ return b
+
+ def set_available_apps(self, apps: list[str]):
+ self._apps = sorted({a for a in apps if a})
+
+ def set_available_tags(self, tags: list[str]):
+ self._tags = sorted({t for t in tags if t})
+
+ def _set_type(self, key: str):
+ self.type_filter = key
+ self.btn_all.setChecked(key == "all")
+ labels = {
+ "all": "Tür",
+ "text": "Metin",
+ "image": "Resim",
+ "file": "Dosya",
+ "code": "Kod",
+ }
+ self.btn_type.setText(labels.get(key, "Tür") if key != "all" else "Tür")
+ self.filter_changed.emit()
+
+ def _set_view(self, mode: str):
+ self.view_mode = mode
+ self.btn_grid.setChecked(mode == "grid")
+ self.btn_list.setChecked(mode == "list")
+ self.view_mode_changed.emit(mode)
+
+ def _open_sort_menu(self):
+ menu = QMenu(self)
+ for key, label in (
+ ("newest", "En Yeni"),
+ ("oldest", "En Eski"),
+ ("used", "En Çok Kullanılan"),
+ ):
+ act = menu.addAction(label)
+ act.setCheckable(True)
+ act.setChecked(self.sort_mode == key)
+ act.triggered.connect(lambda checked=False, k=key, l=label: self._pick_sort(k, l))
+ menu.exec(self.btn_sort.mapToGlobal(self.btn_sort.rect().bottomLeft()))
+
+ def _pick_sort(self, key: str, label: str):
+ self.sort_mode = key
+ self.btn_sort.setText(label)
+ self.filter_changed.emit()
+
+ def _open_type_menu(self):
+ menu = QMenu(self)
+ for key, label in (
+ ("all", "Tümü"),
+ ("text", "Metin"),
+ ("image", "Resim"),
+ ("file", "Dosya"),
+ ("code", "Kod"),
+ ):
+ act = menu.addAction(label)
+ act.triggered.connect(lambda checked=False, k=key: self._set_type(k))
+ menu.exec(self.btn_type.mapToGlobal(self.btn_type.rect().bottomLeft()))
+
+ def _open_tag_menu(self):
+ menu = QMenu(self)
+ menu.addAction("Tüm etiketler", lambda: self._pick_tag(""))
+ if not self._tags:
+ menu.addAction("(etiket yok)").setEnabled(False)
+ for t in self._tags[:30]:
+ menu.addAction(t, lambda checked=False, x=t: self._pick_tag(x))
+ menu.exec(self.btn_tag.mapToGlobal(self.btn_tag.rect().bottomLeft()))
+
+ def _pick_tag(self, tag: str):
+ self.tag_filter = tag
+ self.btn_tag.setText((tag[:14] + "…") if len(tag) > 14 else (tag or "Etiket"))
+ self.filter_changed.emit()
+
+ def _open_app_menu(self):
+ menu = QMenu(self)
+ menu.addAction("Tüm uygulamalar", lambda: self._pick_app(""))
+ if not self._apps:
+ menu.addAction("(uygulama yok)").setEnabled(False)
+ for a in self._apps[:30]:
+ menu.addAction(a, lambda checked=False, x=a: self._pick_app(x))
+ menu.exec(self.btn_app.mapToGlobal(self.btn_app.rect().bottomLeft()))
+
+ def _pick_app(self, app: str):
+ self.app_filter = app
+ short = app.replace(".exe", "") if app else "Uygulama"
+ self.btn_app.setText((short[:14] + "…") if len(short) > 14 else short)
+ self.filter_changed.emit()
diff --git a/clipstack/ui/flow_layout.py b/clipstack/ui/flow_layout.py
index 702a76a..faa941d 100644
--- a/clipstack/ui/flow_layout.py
+++ b/clipstack/ui/flow_layout.py
@@ -58,7 +58,11 @@ def minimumSize(self) -> QSize:
base = QSize(120, 80)
for it in self._items:
w = it.widget()
- if w is not None and not w.isVisible():
+ # isVisible() parent pencere gizliyken de False döner. Kartın
+ # kendisi gizlenmemişse pencere gösterilmeden önce de geometrisi
+ # hesaplanmalı; aksi hâlde son kart (0, 0)'da ilk kartın üstüne
+ # biner ve bir frame sonra yerine sıçrar.
+ if w is not None and w.isHidden():
continue
base = base.expandedTo(it.sizeHint())
return base + QSize(left + right, top + bottom)
@@ -79,12 +83,35 @@ def heightForWidth(self, width: int) -> int:
# ---- Kolaylık metotları ----
+ def _adopt_widget(self, w: QWidget) -> None:
+ """Adopt *w* as a child control, never as an owned native window."""
+ parent = self.parentWidget()
+ if parent is None:
+ return
+
+ # A parentless QWidget starts with a window type. The one-argument
+ # setParent() preserves that type, which leaves skeleton/loader cards
+ # as native Windows windows. Reset the type explicitly.
+ if w.parentWidget() is not parent or w.isWindow():
+ was_visible = w.isVisible()
+ if was_visible:
+ w.hide()
+ w.setParent(parent, Qt.WindowType.Widget)
+ if was_visible:
+ w.show()
+
+ # QLayout.addWidget() normally performs this registration, but this
+ # custom implementation replaces it.
+ self.addChildWidget(w)
+
def addWidget(self, w: QWidget) -> None:
+ self._adopt_widget(w)
self._items.append(QWidgetItem(w))
self.invalidate()
def insertWidget(self, index: int, w: QWidget) -> None:
index = max(0, min(index, len(self._items)))
+ self._adopt_widget(w)
self._items.insert(index, QWidgetItem(w))
self.invalidate()
@@ -130,13 +157,18 @@ def _doLayout(self, rect: QRect, testOnly: bool) -> int:
for it in self._items:
w = it.widget()
- if w is not None and not w.isVisible():
+ if w is not None and w.isHidden():
continue
hint = it.sizeHint()
if hint.width() <= 0 or hint.height() <= 0:
hint = hint.expandedTo(QSize(64, 48))
+ # Liste görünümü: satırın tamamını kapla
+ if w is not None and getattr(w, "_view_mode", "grid") == "list":
+ list_h = max(hint.height(), int(getattr(w, "LIST_H", 72) or 72))
+ hint = QSize(max(200, effective.width()), list_h)
+
nextX = x + hint.width() + hSpace
# Satır sonu kontrolü
@@ -154,4 +186,4 @@ def _doLayout(self, rect: QRect, testOnly: bool) -> int:
# Kullanılan toplam yükseklik
used = (y + lineHeight) - rect.y()
- return used + top + bottom
\ No newline at end of file
+ return used + top + bottom
diff --git a/clipstack/ui/item_preview_dialog.py b/clipstack/ui/item_preview_dialog.py
index d6e4c7b..dd588b2 100644
--- a/clipstack/ui/item_preview_dialog.py
+++ b/clipstack/ui/item_preview_dialog.py
@@ -1,263 +1,335 @@
-from __future__ import annotations
-
-from urllib.parse import urlparse
-
-from PySide6.QtCore import Qt, QByteArray, QSize
-from PySide6.QtGui import QPixmap
-from PySide6.QtWidgets import (
- QDialog, QVBoxLayout, QHBoxLayout, QLabel, QTextEdit, QPushButton,
- QFileDialog, QScrollArea, QMessageBox, QInputDialog, QLineEdit
-)
-import requests
-
-from ..sensitive_detector import ensure_sensitive_access, requires_sensitive_access
-from ..utils import resource_path, copy_to_clipboard_safely, svg_icon
-from ..storage import ClipItemType
-from ..i18n import i18n
-
-# Paylaşım için izin verilen sunucu host'ları
-_ALLOWED_SHARE_HOSTS = frozenset({
- "taxclip.com",
- "www.taxclip.com",
- "localhost",
- "127.0.0.1",
-})
-
-
-def validate_share_server_url(url: str) -> tuple[bool, str]:
- """Paylaşım sunucu URL'sini whitelist ile doğrula."""
- raw = (url or "").strip()
- if not raw:
- return False, "Paylaşım sunucu adresi boş."
- try:
- parsed = urlparse(raw)
- except Exception:
- return False, "Geçersiz paylaşım sunucu adresi."
-
- scheme = (parsed.scheme or "").lower()
- host = (parsed.hostname or "").lower()
-
- if scheme not in ("https", "http"):
- return False, "Paylaşım sunucusu yalnızca http/https olabilir."
-
- # localhost dışında HTTPS zorunlu
- if host not in ("localhost", "127.0.0.1") and scheme != "https":
- return False, "Uzak paylaşım sunucusu HTTPS olmalıdır."
-
- if host not in _ALLOWED_SHARE_HOSTS:
- return False, (
- f"İzin verilmeyen paylaşım sunucusu: {host}\n"
- f"İzin verilenler: {', '.join(sorted(_ALLOWED_SHARE_HOSTS))}"
- )
-
- # Path traversal / beklenmeyen path kontrolü
- base = f"{scheme}://{parsed.netloc}".rstrip("/")
- return True, base
-
-
-def row_val(row, key, default=None):
- try:
- return row[key]
- except Exception:
- try:
- return row.get(key, default) # dict ise
- except Exception:
- return default
-
-
-class ItemPreviewDialog(QDialog):
- def __init__(self, row, parent=None, settings=None):
- super().__init__(parent)
- self.setWindowTitle(self._tr("preview.title", "Preview"))
- try:
- self.setWindowIcon(svg_icon("assets/icons/expand.svg"))
- except Exception:
- pass
- self.resize(720, 520)
-
- self.row = row
- parent_window = parent.window() if parent is not None else None
- self.settings = settings or getattr(parent, "settings", None) or getattr(parent_window, "settings", None)
- self.item_type = ClipItemType(row["item_type"])
- self._sensitive_probe_text = self._build_sensitive_probe_text()
- self._requires_sensitive_access = requires_sensitive_access(self.settings, self._sensitive_probe_text)
- self._sensitive_access_granted = not self._requires_sensitive_access
- if self._requires_sensitive_access:
- self._sensitive_access_granted = ensure_sensitive_access(self.settings, self._sensitive_probe_text, self)
-
- v = QVBoxLayout(self)
-
- if not self._sensitive_access_granted:
- msg = QLabel("Bu içerik hassas veri içeriyor. Görüntülemek veya kopyalamak için doğrulama gerekli.")
- msg.setWordWrap(True)
- msg.setAlignment(Qt.AlignCenter)
- v.addWidget(msg, 1)
-
- elif self.item_type in (ClipItemType.TEXT, ClipItemType.HTML):
- text = row_val(row, "text_content", "") or ""
- if not text:
- html = row_val(row, "html_content", "") or ""
- if html:
- from PySide6.QtGui import QTextDocument
- doc = QTextDocument()
- doc.setHtml(html)
- text = doc.toPlainText()
-
- edit = QTextEdit()
- edit.setReadOnly(True)
- edit.setPlainText(text)
- v.addWidget(edit, 1)
-
- elif self.item_type == ClipItemType.IMAGE:
- blob = row_val(row, "image_blob")
- pm = QPixmap()
- lbl = QLabel()
- lbl.setAlignment(Qt.AlignCenter)
- if blob is not None:
- pm.loadFromData(QByteArray(blob))
- lbl.setPixmap(pm)
- else:
- lbl.setText(self._tr("item.unsupported", "(Unsupported)"))
-
- scroll = QScrollArea()
- scroll.setWidgetResizable(True)
- scroll.setWidget(lbl)
- v.addWidget(scroll, 1)
- else:
- lbl = QLabel(self._tr("item.unsupported", "(Unsupported)"))
- lbl.setAlignment(Qt.AlignCenter)
- v.addWidget(lbl, 1)
-
- # Buttons
- h = QHBoxLayout()
- h.addStretch(1)
-
- self.btn_copy = QPushButton(self._tr("preview.copy", "Copy"))
- self.btn_copy.clicked.connect(self._copy)
- h.addWidget(self.btn_copy)
-
- self.btn_save = QPushButton(self._tr("preview.save", "Save As…"))
- self.btn_save.clicked.connect(self._save)
- self.btn_save.setVisible(self.item_type == ClipItemType.IMAGE)
- h.addWidget(self.btn_save)
-
- self.btn_close = QPushButton(self._tr("preview.close", "Close"))
- self.btn_close.clicked.connect(self.accept)
- h.addWidget(self.btn_close)
-
- self.btn_share = QPushButton(self._tr("preview.share", "Paylaş"))
- self.btn_share.clicked.connect(self._share)
- h.addWidget(self.btn_share)
-
- v.addLayout(h)
-
- if not self._sensitive_access_granted:
- self.btn_copy.setEnabled(False)
- self.btn_share.setEnabled(False)
- self.btn_save.setEnabled(False)
-
- def _tr(self, key: str, fallback: str) -> str:
- try:
- s = i18n.t(key)
- except Exception:
- s = ""
- return s if s and s != key else fallback
-
- def _build_sensitive_probe_text(self) -> str:
- if self.item_type in (ClipItemType.TEXT, ClipItemType.HTML):
- text = row_val(self.row, "text_content", "") or ""
- if text:
- return text
- html = row_val(self.row, "html_content", "") or ""
- if html:
- from PySide6.QtGui import QTextDocument
- doc = QTextDocument()
- doc.setHtml(html)
- return doc.toPlainText()
- return ""
- if self.item_type == ClipItemType.IMAGE:
- return row_val(self.row, "ocr_text", "") or ""
- return ""
-
- def _copy(self):
- if not self._sensitive_access_granted:
- QMessageBox.warning(
- self,
- "Erişim Engellendi",
- "Bu içerik hassas veri içeriyor. Önce doğrulama gerekli."
- )
- return
- if self.item_type in (ClipItemType.TEXT, ClipItemType.HTML):
- payload = row_val(self.row, "text_content") or (row_val(self.row, "html_content") or "")
- elif self.item_type == ClipItemType.IMAGE:
- payload = row_val(self.row, "image_blob")
- else:
- payload = None
- copy_to_clipboard_safely(self, self.item_type, payload)
-
- def _save(self):
- if self.item_type != ClipItemType.IMAGE:
- return
- file, _ = QFileDialog.getSaveFileName(self, self._tr("preview.save", "Save As…"), "", "PNG (*.png);;JPEG (*.jpg *.jpeg);;BMP (*.bmp)")
- if not file:
- return
- blob = row_val(self.row, "image_blob")
- if blob is None:
- return
- pm = QPixmap()
- pm.loadFromData(QByteArray(blob))
- pm.save(file)
-
- def _share(self):
- if not self._sensitive_access_granted:
- QMessageBox.warning(
- self,
- "Erişim Engellendi",
- "Bu içerik hassas veri içeriyor. Önce doğrulama gerekli."
- )
- return
- # Sunucu ve API anahtarı ayarlardan
- share_server = self.settings.get("share_server_url", "https://taxclip.com") if self.settings else "https://taxclip.com"
- api_key = self.settings.get("share_api_key", "") if self.settings else ""
-
- ok_url, share_base = validate_share_server_url(share_server)
- if not ok_url:
- QMessageBox.warning(self, "Güvenlik", share_base)
- return
-
- # Süre seçimi
- durations = [("Sınırsız", 0), ("3 gün", 3), ("7 gün", 7), ("14 gün", 14), ("30 gün", 30)]
- duration_names = [d[0] for d in durations]
- duration_idx, ok = QInputDialog.getItem(self, "Paylaşım Süresi", "Ne kadar saklansın?", duration_names, 0, False)
- if not ok:
- return
- duration = durations[duration_names.index(duration_idx)][1]
-
- # Şifre iste (isteğe bağlı)
- ask_pw, ok = QInputDialog.getText(self, "Şifreli paylaş?", "Paylaşım şifresi (boş bırakılırsa şifresiz olur):", QLineEdit.Password)
- if not ok:
- return
- password = ask_pw.strip() if ask_pw else None
-
- content = row_val(self.row, "text_content") or row_val(self.row, "html_content") or ""
- payload = {
- "type": "note",
- "content": content,
- "duration": duration,
- "password": password if password else None,
- }
-
- headers = {}
- if api_key:
- headers["Authorization"] = f"Bearer {api_key}"
-
- try:
- resp = requests.post(f"{share_base}/api/share", json=payload, headers=headers, timeout=10)
- resp.raise_for_status()
- data = resp.json()
- url = data["url"]
- from PySide6.QtWidgets import QApplication
- QApplication.clipboard().setText(url)
- QMessageBox.information(self, "Paylaşıldı", f"Paylaşım linki panoya kopyalandı:\n{url}")
- except Exception as e:
- QMessageBox.warning(self, "Hata", f"Paylaşım başarısız:\n{e}")
+from __future__ import annotations
+
+from urllib.parse import urlparse
+
+from PySide6.QtCore import Qt, QByteArray, QSize, QPropertyAnimation, QEasingCurve, QParallelAnimationGroup, QPoint, QTimer
+from PySide6.QtGui import QCloseEvent, QPixmap
+from PySide6.QtWidgets import (
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QTextEdit, QPushButton,
+ QFileDialog, QScrollArea, QMessageBox, QInputDialog, QLineEdit,
+)
+import requests
+
+from ..sensitive_detector import ensure_sensitive_access, requires_sensitive_access
+from ..utils import resource_path, copy_to_clipboard_safely, svg_icon
+from ..storage import ClipItemType
+from ..i18n import i18n
+
+# Paylaşım için izin verilen sunucu host'ları
+_ALLOWED_SHARE_HOSTS = frozenset({
+ "taxclip.com",
+ "www.taxclip.com",
+ "localhost",
+ "127.0.0.1",
+})
+
+
+def validate_share_server_url(url: str) -> tuple[bool, str]:
+ """Paylaşım sunucu URL'sini whitelist ile doğrula."""
+ raw = (url or "").strip()
+ if not raw:
+ return False, "Paylaşım sunucu adresi boş."
+ try:
+ parsed = urlparse(raw)
+ except Exception:
+ return False, "Geçersiz paylaşım sunucu adresi."
+
+ scheme = (parsed.scheme or "").lower()
+ host = (parsed.hostname or "").lower()
+
+ if scheme not in ("https", "http"):
+ return False, "Paylaşım sunucusu yalnızca http/https olabilir."
+
+ # localhost dışında HTTPS zorunlu
+ if host not in ("localhost", "127.0.0.1") and scheme != "https":
+ return False, "Uzak paylaşım sunucusu HTTPS olmalıdır."
+
+ if host not in _ALLOWED_SHARE_HOSTS:
+ return False, (
+ f"İzin verilmeyen paylaşım sunucusu: {host}\n"
+ f"İzin verilenler: {', '.join(sorted(_ALLOWED_SHARE_HOSTS))}"
+ )
+
+ # Path traversal / beklenmeyen path kontrolü
+ base = f"{scheme}://{parsed.netloc}".rstrip("/")
+ return True, base
+
+
+def row_val(row, key, default=None):
+ try:
+ return row[key]
+ except Exception:
+ try:
+ return row.get(key, default) # dict ise
+ except Exception:
+ return default
+
+
+class ItemPreviewDialog(QDialog):
+ def __init__(self, row, parent=None, settings=None):
+ super().__init__(parent)
+ self.setWindowTitle(self._tr("preview.title", "Önizleme"))
+ self.setModal(True)
+ self.setWindowFlags(
+ (self.windowFlags() | Qt.WindowCloseButtonHint)
+ & ~Qt.WindowContextHelpButtonHint
+ )
+ try:
+ self.setWindowIcon(svg_icon("assets/icons/expand.svg"))
+ except Exception:
+ pass
+ self.resize(760, 520)
+ self.setMinimumSize(480, 320)
+
+ self.row = row
+ parent_window = parent.window() if parent is not None else None
+ self.settings = settings or getattr(parent, "settings", None) or getattr(parent_window, "settings", None)
+ self.item_type = ClipItemType(row["item_type"])
+ self._sensitive_probe_text = self._build_sensitive_probe_text()
+ self._requires_sensitive_access = requires_sensitive_access(self.settings, self._sensitive_probe_text)
+ self._sensitive_access_granted = not self._requires_sensitive_access
+ if self._requires_sensitive_access:
+ self._sensitive_access_granted = ensure_sensitive_access(self.settings, self._sensitive_probe_text, self)
+
+ v = QVBoxLayout(self)
+ v.setContentsMargins(16, 16, 16, 16)
+ v.setSpacing(12)
+
+ if not self._sensitive_access_granted:
+ msg = QLabel("Bu içerik hassas veri içeriyor. Görüntülemek veya kopyalamak için doğrulama gerekli.")
+ msg.setWordWrap(True)
+ msg.setAlignment(Qt.AlignCenter)
+ v.addWidget(msg, 1)
+
+ elif self.item_type in (ClipItemType.TEXT, ClipItemType.HTML):
+ text = row_val(row, "text_content", "") or ""
+ if not text:
+ html = row_val(row, "html_content", "") or ""
+ if html:
+ from PySide6.QtGui import QTextDocument
+ doc = QTextDocument()
+ doc.setHtml(html)
+ text = doc.toPlainText()
+
+ edit = QTextEdit()
+ edit.setReadOnly(True)
+ try:
+ from ..smart_content import analyze_text, ContentKind
+ from .card_common import highlight_code
+ smart = analyze_text(text)
+ if smart.kind in (ContentKind.CODE, ContentKind.JSON):
+ code = smart.meta.get("pretty") or smart.meta.get("code") or text
+ edit.setHtml(highlight_code(code, max_chars=12000))
+ else:
+ edit.setPlainText(text)
+ except Exception:
+ edit.setPlainText(text)
+ v.addWidget(edit, 1)
+
+ elif self.item_type == ClipItemType.IMAGE:
+ blob = row_val(row, "image_blob")
+ pm = QPixmap()
+ lbl = QLabel()
+ lbl.setAlignment(Qt.AlignCenter)
+ if blob is not None:
+ pm.loadFromData(QByteArray(blob))
+ lbl.setPixmap(pm)
+ else:
+ lbl.setText(self._tr("item.unsupported", "(Unsupported)"))
+
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setWidget(lbl)
+ v.addWidget(scroll, 1)
+ else:
+ lbl = QLabel(self._tr("item.unsupported", "(Unsupported)"))
+ lbl.setAlignment(Qt.AlignCenter)
+ v.addWidget(lbl, 1)
+
+ # Buttons
+ h = QHBoxLayout()
+ h.addStretch(1)
+
+ self.btn_copy = QPushButton(self._tr("preview.copy", "Copy"))
+ self.btn_copy.clicked.connect(self._copy)
+ h.addWidget(self.btn_copy)
+
+ self.btn_save = QPushButton(self._tr("preview.save", "Save As…"))
+ self.btn_save.clicked.connect(self._save)
+ self.btn_save.setVisible(self.item_type == ClipItemType.IMAGE)
+ h.addWidget(self.btn_save)
+
+ self.btn_close = QPushButton(self._tr("preview.close", "Close"))
+ self.btn_close.clicked.connect(self.accept)
+ h.addWidget(self.btn_close)
+
+ self.btn_share = QPushButton(self._tr("preview.share", "Paylaş"))
+ self.btn_share.clicked.connect(self._share)
+ h.addWidget(self.btn_share)
+
+ v.addLayout(h)
+
+ if not self._sensitive_access_granted:
+ self.btn_copy.setEnabled(False)
+ self.btn_share.setEnabled(False)
+ self.btn_save.setEnabled(False)
+
+ def exec_animated(self) -> int:
+ """Fade-in ile modal aç."""
+ animations = bool(True)
+ try:
+ if self.settings is not None:
+ animations = bool(self.settings.get("animations", True))
+ except Exception:
+ pass
+
+ if not animations:
+ self.setWindowOpacity(1.0)
+ return self.exec()
+
+ self.setWindowOpacity(0.0)
+ QTimer.singleShot(0, self._start_open_animation)
+ return self.exec()
+
+ def _start_open_animation(self):
+ if not self.isVisible():
+ return
+ self.raise_()
+ self.activateWindow()
+
+ end_pos = self.pos()
+ start_pos = end_pos + QPoint(0, 16)
+ self.move(start_pos)
+
+ fade_win = QPropertyAnimation(self, b"windowOpacity", self)
+ fade_win.setDuration(200)
+ fade_win.setStartValue(0.0)
+ fade_win.setEndValue(1.0)
+ fade_win.setEasingCurve(QEasingCurve.OutCubic)
+
+ slide = QPropertyAnimation(self, b"pos", self)
+ slide.setDuration(200)
+ slide.setStartValue(start_pos)
+ slide.setEndValue(end_pos)
+ slide.setEasingCurve(QEasingCurve.OutCubic)
+
+ group = QParallelAnimationGroup(self)
+ group.addAnimation(fade_win)
+ group.addAnimation(slide)
+ group.start()
+ self._open_anim = group
+
+ def closeEvent(self, event: QCloseEvent):
+ """Make the native title-bar X reliably end the modal event loop."""
+ animation = getattr(self, "_open_anim", None)
+ if animation is not None:
+ animation.stop()
+ self.setWindowOpacity(1.0)
+ self.reject()
+ event.accept()
+
+ def _tr(self, key: str, fallback: str) -> str:
+ try:
+ s = i18n.t(key)
+ except Exception:
+ s = ""
+ return s if s and s != key else fallback
+
+ def _build_sensitive_probe_text(self) -> str:
+ if self.item_type in (ClipItemType.TEXT, ClipItemType.HTML):
+ text = row_val(self.row, "text_content", "") or ""
+ if text:
+ return text
+ html = row_val(self.row, "html_content", "") or ""
+ if html:
+ from PySide6.QtGui import QTextDocument
+ doc = QTextDocument()
+ doc.setHtml(html)
+ return doc.toPlainText()
+ return ""
+ if self.item_type == ClipItemType.IMAGE:
+ return row_val(self.row, "ocr_text", "") or ""
+ return ""
+
+ def _copy(self):
+ if not self._sensitive_access_granted:
+ QMessageBox.warning(
+ self,
+ "Erişim Engellendi",
+ "Bu içerik hassas veri içeriyor. Önce doğrulama gerekli."
+ )
+ return
+ if self.item_type in (ClipItemType.TEXT, ClipItemType.HTML):
+ payload = row_val(self.row, "text_content") or (row_val(self.row, "html_content") or "")
+ elif self.item_type == ClipItemType.IMAGE:
+ payload = row_val(self.row, "image_blob")
+ else:
+ payload = None
+ copy_to_clipboard_safely(self, self.item_type, payload)
+
+ def _save(self):
+ if self.item_type != ClipItemType.IMAGE:
+ return
+ file, _ = QFileDialog.getSaveFileName(self, self._tr("preview.save", "Save As…"), "", "PNG (*.png);;JPEG (*.jpg *.jpeg);;BMP (*.bmp)")
+ if not file:
+ return
+ blob = row_val(self.row, "image_blob")
+ if blob is None:
+ return
+ pm = QPixmap()
+ pm.loadFromData(QByteArray(blob))
+ pm.save(file)
+
+ def _share(self):
+ if not self._sensitive_access_granted:
+ QMessageBox.warning(
+ self,
+ "Erişim Engellendi",
+ "Bu içerik hassas veri içeriyor. Önce doğrulama gerekli."
+ )
+ return
+ # Sunucu ve API anahtarı ayarlardan
+ share_server = self.settings.get("share_server_url", "https://taxclip.com") if self.settings else "https://taxclip.com"
+ api_key = self.settings.get("share_api_key", "") if self.settings else ""
+
+ ok_url, share_base = validate_share_server_url(share_server)
+ if not ok_url:
+ QMessageBox.warning(self, "Güvenlik", share_base)
+ return
+
+ # Süre seçimi
+ durations = [("Sınırsız", 0), ("3 gün", 3), ("7 gün", 7), ("14 gün", 14), ("30 gün", 30)]
+ duration_names = [d[0] for d in durations]
+ duration_idx, ok = QInputDialog.getItem(self, "Paylaşım Süresi", "Ne kadar saklansın?", duration_names, 0, False)
+ if not ok:
+ return
+ duration = durations[duration_names.index(duration_idx)][1]
+
+ # Şifre iste (isteğe bağlı)
+ ask_pw, ok = QInputDialog.getText(self, "Şifreli paylaş?", "Paylaşım şifresi (boş bırakılırsa şifresiz olur):", QLineEdit.Password)
+ if not ok:
+ return
+ password = ask_pw.strip() if ask_pw else None
+
+ content = row_val(self.row, "text_content") or row_val(self.row, "html_content") or ""
+ payload = {
+ "type": "note",
+ "content": content,
+ "duration": duration,
+ "password": password if password else None,
+ }
+
+ headers = {}
+ if api_key:
+ headers["Authorization"] = f"Bearer {api_key}"
+
+ try:
+ resp = requests.post(f"{share_base}/api/share", json=payload, headers=headers, timeout=10)
+ resp.raise_for_status()
+ data = resp.json()
+ url = data["url"]
+ from PySide6.QtWidgets import QApplication
+ QApplication.clipboard().setText(url)
+ QMessageBox.information(self, "Paylaşıldı", f"Paylaşım linki panoya kopyalandı:\n{url}")
+ except Exception as e:
+ QMessageBox.warning(self, "Hata", f"Paylaşım başarısız:\n{e}")
diff --git a/clipstack/ui/item_widget.py b/clipstack/ui/item_widget.py
index 3316493..d9d25f7 100644
--- a/clipstack/ui/item_widget.py
+++ b/clipstack/ui/item_widget.py
@@ -7,21 +7,25 @@
from pathlib import Path
from typing import Optional
-from PySide6.QtCore import Qt, QSize, QByteArray, Signal
+from PySide6.QtCore import Qt, QSize, QByteArray, Signal, QTimer
from PySide6.QtGui import QPixmap, QTextDocument, QColor, QDesktopServices, QAction
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QLabel, QHBoxLayout, QToolButton, QMenu,
- QApplication, QMessageBox, QInputDialog, QColorDialog,
+ QApplication, QMessageBox, QInputDialog, QColorDialog, QFrame,
)
from PySide6.QtCore import QUrl
from ..storage import ClipItemType
from ..sensitive_detector import ensure_sensitive_access, requires_sensitive_access
-from ..smart_content import analyze_text, describe_paths, ContentKind
+from ..smart_content import analyze_text, describe_paths, ContentKind, detect_language
+from .card_common import highlight_code as _highlight_code, apply_card_view_mode, CARD_W, CARD_H, LIST_H
from ..utils import resource_path, svg_icon
from ..i18n import i18n
+# _highlight_code card_common'dan geliyor
+
+
class ItemWidget(QWidget):
on_copy_requested = Signal(int, int, object) # (row_id, item_type, payload)
on_delete_requested = Signal(int) # row_id
@@ -30,8 +34,9 @@ class ItemWidget(QWidget):
on_save_snippet = Signal(int)
on_meta_changed = Signal(int)
- CARD_W = 260
- CARD_H = 160
+ CARD_W = CARD_W
+ CARD_H = CARD_H
+ LIST_H = LIST_H
def __init__(self, row, parent=None, settings=None, selected: bool = False):
super().__init__(parent)
@@ -44,131 +49,200 @@ def __init__(self, row, parent=None, settings=None, selected: bool = False):
self._selected = selected
self._file_paths: list[str] = []
self._smart = None
+ self._char_count = 0
+ self._view_mode = "grid"
self._sensitive_probe_text = self._build_sensitive_probe_text()
self._requires_sensitive_access = requires_sensitive_access(self.settings, self._sensitive_probe_text)
self.setObjectName("ItemCard")
self.setAttribute(Qt.WA_StyledBackground, True)
- self.setStyleSheet("border: none;")
self.setFixedSize(self.CARD_W, self.CARD_H)
self.setFocusPolicy(Qt.StrongFocus)
+ self.setCursor(Qt.PointingHandCursor)
+ # Global QWidget arka planının iç içe kutu oluşturmasını engelle
+ self.setAutoFillBackground(False)
self.v = QVBoxLayout(self)
- self.v.setContentsMargins(10, 10, 10, 10)
- self.v.setSpacing(4)
+ self.v.setContentsMargins(12, 12, 12, 10)
+ self.v.setSpacing(8)
+
+ # Üst satır: tip rozeti | stretch | favori | menü
+ top = QHBoxLayout()
+ top.setSpacing(6)
+
+ self.type_badge = QFrame()
+ self.type_badge.setObjectName("ItemTypeBadge")
+ self.type_badge.setFixedSize(28, 28)
+ self.type_badge.setAttribute(Qt.WA_StyledBackground, True)
+ self.type_badge.setAutoFillBackground(False)
+ badge_lay = QHBoxLayout(self.type_badge)
+ badge_lay.setContentsMargins(0, 0, 0, 0)
+ self.type_icon = QLabel()
+ self.type_icon.setAlignment(Qt.AlignCenter)
+ self.type_icon.setFixedSize(28, 28)
+ self.type_icon.setStyleSheet("background: transparent; border: none;")
+ badge_lay.addWidget(self.type_icon)
+ top.addWidget(self.type_badge)
- # Üst başlık (akıllı tür / özel isim)
self.lbl_title = QLabel()
self.lbl_title.setObjectName("ItemTitle")
- self.lbl_title.setStyleSheet("font-weight: 600; font-size: 11px;")
- self.v.addWidget(self.lbl_title)
+ self.lbl_title.setStyleSheet(
+ "font-weight: 600; font-size: 11px; background: transparent; border: none;"
+ )
+ top.addWidget(self.lbl_title, 1)
+
+ self.btn_fav = QToolButton()
+ self.btn_fav.setObjectName("FavButton")
+ self.btn_fav.setToolTip(self._tr("item.tooltip.favorite", "Add/Remove favorites"))
+ self.btn_fav.setCheckable(True)
+ self.btn_fav.setChecked(bool(self._row("favorite", False)))
+ self.btn_fav.setStyleSheet("background: transparent; border: none;")
+ self._apply_fav_icon()
+ self.btn_fav.toggled.connect(self._fav_toggled)
+ self.btn_fav.setAutoRaise(True)
+ top.addWidget(self.btn_fav)
+
+ self.btn_more = QToolButton()
+ self.btn_more.setObjectName("MoreButton")
+ self.btn_more.setToolTip("Diğer")
+ self.btn_more.setAutoRaise(True)
+ self.btn_more.setStyleSheet("background: transparent; border: none;")
+ try:
+ self.btn_more.setIcon(svg_icon("assets/icons/more_vert.svg"))
+ except Exception:
+ self.btn_more.setText("⋮")
+ self.btn_more.clicked.connect(lambda: self._show_context_menu(self.btn_more.mapToGlobal(self.btn_more.rect().bottomLeft())))
+ top.addWidget(self.btn_more)
+
+ self.v.addLayout(top)
+ # Önizleme
self.preview = QLabel()
self.preview.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.preview.setWordWrap(True)
- self.preview.setTextFormat(Qt.PlainText)
+ self.preview.setTextFormat(Qt.RichText)
+ self.preview.setObjectName("ItemPreview")
+ # Renkli kod için color set etme — QSS span renklerini ezer
+ self.preview.setStyleSheet("background: transparent; border: none;")
+ self.preview.setAutoFillBackground(False)
custom_title = self._row("custom_title") or ""
if self._requires_sensitive_access:
- self.lbl_title.setText("🔒 Hassas veri")
+ self.lbl_title.setText("Hassas veri")
+ self.preview.setTextFormat(Qt.PlainText)
self.preview.setText("Görüntülemek için açın")
+ self._set_type_icon("lock")
+ self._char_count = 0
elif self.item_type == ClipItemType.FILE:
self._render_file_card(custom_title)
elif self.item_type in (ClipItemType.TEXT, ClipItemType.HTML):
self._render_text_card(custom_title)
elif self.item_type == ClipItemType.IMAGE:
- self.lbl_title.setText(custom_title or "🖼 Görsel")
+ self.lbl_title.setText(custom_title or "Görsel")
+ self._set_type_icon("image")
blob = self._row("image_blob")
pm = QPixmap()
if blob is not None:
pm.loadFromData(QByteArray(blob))
- thumb = pm.scaled(self.CARD_W - 20, self.CARD_H - 60, Qt.KeepAspectRatio, Qt.SmoothTransformation)
+ thumb = pm.scaled(self.CARD_W - 24, self.CARD_H - 78, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.preview.setPixmap(thumb)
else:
+ self.preview.setTextFormat(Qt.PlainText)
self.preview.setText(self._tr("item.unsupported", "(Unsupported)"))
+ self._char_count = 0
else:
self.lbl_title.setText("?")
+ self._set_type_icon("text")
+ self.preview.setTextFormat(Qt.PlainText)
self.preview.setText(self._tr("item.unsupported", "(Unsupported)"))
self.v.addWidget(self.preview, 1)
- # Bottom bar: meta
- self.bottom = QHBoxLayout()
- meta_parts = [str(self._row("created_at", ""))]
+ # Alt bar: tarih + karakter | kopyala
+ bottom = QHBoxLayout()
+ bottom.setSpacing(6)
+
+ date_str = self._format_date(str(self._row("created_at", "") or ""))
+ meta_bits = [date_str]
+ if self._char_count > 0:
+ meta_bits.append(f"{self._char_count} kr")
source = self._row("source_app") or ""
if source:
- meta_parts.append(source.replace(".exe", ""))
- use_count = int(self._row("use_count") or 0)
- if use_count:
- meta_parts.append(f"×{use_count}")
- if self._row("collection"):
- meta_parts.append(str(self._row("collection")))
+ meta_bits.append(source.replace(".exe", "")[:12])
if self._row("is_sensitive"):
- meta_parts.append("🔒")
- self.lbl_meta = QLabel(" · ".join(p for p in meta_parts if p))
- self.lbl_meta.setObjectName("MetaLabel")
- self.lbl_meta.setStyleSheet("font-size: 10px; color: #888;")
- self.bottom.addWidget(self.lbl_meta, 1)
+ meta_bits.append("🔒")
- self.btn_fav = QToolButton()
- self.btn_fav.setObjectName("FavButton")
- self.btn_fav.setToolTip(self._tr("item.tooltip.favorite", "Add/Remove favorites"))
- self.btn_fav.setCheckable(True)
- self.btn_fav.setChecked(bool(self._row("favorite", False)))
- self._apply_fav_icon()
- self.btn_fav.toggled.connect(self._fav_toggled)
- self.btn_fav.setAutoRaise(True)
- self.bottom.addWidget(self.btn_fav)
-
- self.v.addLayout(self.bottom)
-
- # Hover highlight overlay
- self.hover_overlay = QWidget(self)
- self.hover_overlay.setObjectName("HoverHighlight")
- self.hover_overlay.setAttribute(Qt.WA_TransparentForMouseEvents, True)
- self.hover_overlay.setAttribute(Qt.WA_StyledBackground, True)
- self.hover_overlay.setStyleSheet("background-color: rgba(0,0,0,0.10); border-radius: 12px;")
- self.hover_overlay.hide()
-
- # Hover toolbar
- self.toolbar = QWidget(self)
- self.toolbar.setObjectName("HoverToolbar")
- self.toolbar.setAttribute(Qt.WA_StyledBackground, True)
- self.toolbar.setStyleSheet("background-color: rgba(0,0,0,0.22); border-top-left-radius: 12px; border-top-right-radius: 12px;")
- self.toolbar_layout = QHBoxLayout(self.toolbar)
- self.toolbar_layout.setContentsMargins(6, 4, 6, 4)
- self.toolbar_layout.setSpacing(6)
+ self.lbl_meta = QLabel(" · ".join(p for p in meta_bits if p))
+ self.lbl_meta.setObjectName("MetaLabel")
+ self.lbl_meta.setStyleSheet(
+ "font-size: 10px; color: #94a3b8; background: transparent; border: none;"
+ )
+ bottom.addWidget(self.lbl_meta, 1)
self.btn_copy = QToolButton()
+ self.btn_copy.setObjectName("CopyButton")
self.btn_copy.setIcon(svg_icon("assets/icons/copy.svg"))
self.btn_copy.setToolTip(self._tr("item.tooltip.copy", "Copy to clipboard"))
self.btn_copy.setAutoRaise(True)
+ self.btn_copy.setStyleSheet("background: transparent; border: none;")
self.btn_copy.clicked.connect(self._copy)
- self.toolbar_layout.addWidget(self.btn_copy)
-
- self.btn_expand = QToolButton()
- self.btn_expand.setIcon(svg_icon("assets/icons/expand.svg"))
- self.btn_expand.setToolTip(self._tr("item.tooltip.expand", "Expand"))
- self.btn_expand.setAutoRaise(True)
- self.btn_expand.clicked.connect(self._expand)
- self.toolbar_layout.addWidget(self.btn_expand)
-
- self.btn_delete = QToolButton()
- self.btn_delete.setIcon(svg_icon("assets/icons/delete.svg"))
- self.btn_delete.setToolTip(self._tr("item.tooltip.delete", "Delete"))
- self.btn_delete.setAutoRaise(True)
- self.btn_delete.clicked.connect(self._delete)
- self.toolbar_layout.addWidget(self.btn_delete)
-
- self.btn_share = QToolButton()
- self.btn_share.setIcon(svg_icon("assets/icons/share.svg"))
- self.btn_share.setToolTip(self._tr("item.tooltip.share", "Paylaş"))
- self.btn_share.setAutoRaise(True)
- self.btn_share.clicked.connect(self._share)
- self.toolbar_layout.addWidget(self.btn_share)
-
- self.toolbar.hide()
- self._sync_overlays()
+ bottom.addWidget(self.btn_copy)
+
+ self.v.addLayout(bottom)
self._apply_selection_style()
+ # Mevcut pencere görünüm modunu uygula
+ try:
+ win = self.window()
+ mode = getattr(getattr(win, "chip_bar", None), "view_mode", "grid")
+ self.set_view_mode(mode)
+ except Exception:
+ pass
+
+ def set_view_mode(self, mode: str):
+ apply_card_view_mode(self, mode, getattr(self, "preview", None))
+ self.updateGeometry()
+
+ def showEvent(self, event):
+ # Item cards are embedded controls, never native windows. Repair the
+ # window type too: setParent(parent) alone preserves Qt.Window.
+ parent = self.parentWidget()
+ if parent is None:
+ self.hide()
+ event.ignore()
+ return
+ if self.isWindow():
+ self.hide()
+ self.setParent(parent, Qt.WindowType.Widget)
+ QTimer.singleShot(0, self.show)
+ event.ignore()
+ return
+ super().showEvent(event)
+
+ def _format_date(self, raw: str) -> str:
+ if not raw:
+ return ""
+ # "2026-07-28 14:30:00" -> "28.07.2026"
+ try:
+ date_part = raw.split(" ")[0]
+ y, m, d = date_part.split("-")
+ return f"{d}.{m}.{y}"
+ except Exception:
+ return raw[:16]
+
+ def _set_type_icon(self, kind: str):
+ icon_map = {
+ "text": "assets/icons/type_text.svg",
+ "code": "assets/icons/type_code.svg",
+ "image": "assets/icons/nav_image.svg",
+ "file": "assets/icons/nav_file.svg",
+ "lock": "assets/icons/lock.svg",
+ }
+ path = icon_map.get(kind, "assets/icons/type_text.svg")
+ try:
+ self.type_icon.setPixmap(svg_icon(path).pixmap(QSize(16, 16)))
+ except Exception:
+ glyphs = {"text": "T", "code": ">", "image": "🖼", "file": "📎", "lock": "🔒"}
+ self.type_icon.setText(glyphs.get(kind, "T"))
+ self.type_icon.setAlignment(Qt.AlignCenter)
def _render_file_card(self, custom_title: str):
raw = self._row("text_content") or ""
@@ -178,9 +252,12 @@ def _render_file_card(self, custom_title: str):
except Exception:
self._file_paths = [raw] if raw else []
count = len(self._file_paths)
- self.lbl_title.setText(custom_title or (f"📎 {count} dosya" if count != 1 else f"📎 {Path(self._file_paths[0]).name}"))
+ self.lbl_title.setText(custom_title or (f"{count} dosya" if count != 1 else Path(self._file_paths[0]).name))
+ self._set_type_icon("file")
self.preview_text = "\n".join(self._file_paths)
+ self.preview.setTextFormat(Qt.PlainText)
self.preview.setText(describe_paths(self._file_paths))
+ self._char_count = len(self.preview_text or "")
def _render_text_card(self, custom_title: str):
text = self._row("text_content", "") or ""
@@ -191,30 +268,44 @@ def _render_text_card(self, custom_title: str):
doc.setHtml(html)
text = doc.toPlainText()
self.preview_text = text
+ self._char_count = len(text)
self._smart = analyze_text(text)
title = custom_title or self._smart.title
- kind_icons = {
- ContentKind.URL: "🔗",
- ContentKind.EMAIL: "✉",
- ContentKind.PHONE: "📞",
- ContentKind.HEX_COLOR: "🎨",
- ContentKind.JSON: "{ }",
- ContentKind.CODE: ">",
- ContentKind.MARKDOWN: "MD",
- ContentKind.FILE_PATH: "📂",
- ContentKind.LONG_TEXT: "📝",
- ContentKind.BASE64_IMAGE: "🖼",
- }
- icon = kind_icons.get(self._smart.kind, "")
- self.lbl_title.setText(f"{icon} {title}".strip())
- if self._smart.kind == ContentKind.HEX_COLOR:
+ self.lbl_title.setText(title)
+
+ if self._smart.kind == ContentKind.CODE or self._smart.kind == ContentKind.JSON:
+ self._set_type_icon("code")
+ self.preview.setTextFormat(Qt.RichText)
+ code = self._smart.meta.get("pretty") or self._smart.meta.get("code") or text
+ self.preview.setText(_highlight_code(code))
+ self.preview.setStyleSheet("background: transparent; border: none;")
+ elif self._smart.kind == ContentKind.HEX_COLOR:
+ self._set_type_icon("text")
hex_c = self._smart.meta.get("hex", "#000")
- self.preview.setText(f"{hex_c}\n{self._smart.summary}")
- self.preview.setStyleSheet(
- f"background-color: {hex_c}; color: #fff; padding: 6px; border-radius: 6px;"
+ self.preview.setTextFormat(Qt.RichText)
+ self.preview.setText(
+ f""
+ f"{hex_c}
{self._smart.summary}
"
)
else:
- self.preview.setText(self._shorten(self._smart.summary or text, 280))
+ self._set_type_icon("text")
+ # Kod benzeri satırlar varsa renklendir
+ if self._looks_like_code(text):
+ self._set_type_icon("code")
+ self.preview.setTextFormat(Qt.RichText)
+ self.preview.setText(_highlight_code(text))
+ self.preview.setStyleSheet("background: transparent; border: none;")
+ else:
+ self.preview.setTextFormat(Qt.PlainText)
+ self.preview.setStyleSheet("background: transparent; border: none;")
+ self.preview.setText(self._shorten(self._smart.summary or text, 280))
+
+ def _looks_like_code(self, text: str) -> bool:
+ if not text:
+ return False
+ hints = ("{", "}", "=>", "function", "def ", "import ", "class ", "const ", "let ", "#!/")
+ score = sum(1 for h in hints if h in text)
+ return score >= 2 or text.count("\n") >= 3 and ("{" in text or ";" in text)
def set_selected(self, selected: bool):
self._selected = selected
@@ -222,9 +313,14 @@ def set_selected(self, selected: bool):
def _apply_selection_style(self):
if self._selected:
- self.setStyleSheet("border: 2px solid #3b82f6; border-radius: 12px;")
+ self.setStyleSheet(
+ "#ItemCard { border: none; border-radius: 14px; "
+ "background: #1e3a5f; }"
+ "#ItemCard QLabel, #ItemCard QToolButton, #ItemCard QFrame {"
+ " background: transparent; border: none; }"
+ )
else:
- self.setStyleSheet("border: none;")
+ self.setStyleSheet("")
def _tr(self, key: str, fallback: str) -> str:
try:
@@ -268,34 +364,24 @@ def _ensure_sensitive_access(self) -> bool:
return False
def sizeHint(self) -> QSize:
+ if getattr(self, "_view_mode", "grid") == "list":
+ parent = self.parentWidget()
+ w = parent.width() - 24 if parent and parent.width() > 100 else 640
+ return QSize(max(320, w), self.LIST_H)
return QSize(self.CARD_W, self.CARD_H)
def minimumSizeHint(self) -> QSize:
return self.sizeHint()
- def resizeEvent(self, e):
- self._sync_overlays()
- return super().resizeEvent(e)
-
- def _sync_overlays(self):
- self.hover_overlay.setGeometry(0, 0, self.width(), self.height())
- self.toolbar.setGeometry(0, 0, self.width(), 32)
- self.hover_overlay.lower()
- self.toolbar.raise_()
-
- def enterEvent(self, event):
- self.hover_overlay.show()
- self.toolbar.show()
- super().enterEvent(event)
-
- def leaveEvent(self, event):
- self.toolbar.hide()
- self.hover_overlay.hide()
- super().leaveEvent(event)
-
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
- self._copy()
+ # Kart tıklanınca tam ekran / önizleme
+ child = self.childAt(event.pos())
+ # Butonlara basıldıysa kopyalama/menü kendi handler'ını kullanır
+ if isinstance(child, QToolButton) or (child and child.parent() and isinstance(child.parent(), QToolButton)):
+ super().mousePressEvent(event)
+ return
+ self._expand()
elif event.button() == Qt.RightButton:
self._show_context_menu(event.globalPos())
super().mousePressEvent(event)
@@ -321,7 +407,6 @@ def _show_context_menu(self, global_pos):
menu.addAction("Etiket ekle…", self._set_tags)
menu.addSeparator()
- # Akıllı eylemler
if self.item_type == ClipItemType.FILE and self._file_paths:
menu.addAction("Dosyayı aç", self._open_file)
menu.addAction("Klasörde göster", self._reveal_in_explorer)
@@ -424,9 +509,16 @@ def _delete(self):
self.on_delete_requested.emit(self.row_id)
def _expand(self):
+ if self._requires_sensitive_access and not self._ensure_sensitive_access():
+ return
from .item_preview_dialog import ItemPreviewDialog
- dlg = ItemPreviewDialog(self.row, self, settings=self.settings)
- dlg.exec()
+ owner = self.window()
+ dlg = ItemPreviewDialog(
+ self.row,
+ owner if owner is not self else None,
+ settings=self.settings,
+ )
+ dlg.exec_animated()
def _shorten(self, text: str, limit: int) -> str:
return text if len(text) <= limit else text[: limit - 1] + "…"
@@ -435,9 +527,13 @@ def _share(self):
if self._requires_sensitive_access and not self._ensure_sensitive_access():
return
from .item_preview_dialog import ItemPreviewDialog
- dlg = ItemPreviewDialog(self.row, self, settings=self.settings)
- dlg.exec()
- # Paylaşım dialog içinden yapılabilir; yoksa kopyala
+ owner = self.window()
+ dlg = ItemPreviewDialog(
+ self.row,
+ owner if owner is not self else None,
+ settings=self.settings,
+ )
+ dlg.exec_animated()
content = self._row("text_content") or self._row("html_content") or ""
if content:
QApplication.clipboard().setText(str(content))
diff --git a/clipstack/ui/main_window.py b/clipstack/ui/main_window.py
index 37c4427..f0424f2 100644
--- a/clipstack/ui/main_window.py
+++ b/clipstack/ui/main_window.py
@@ -2,7 +2,7 @@
from typing import Callable, List, Optional
-from PySide6.QtCore import QPoint, QRect, QSize, Qt, QTimer, Signal, QDateTime
+from PySide6.QtCore import QPoint, QRect, QSize, Qt, QTimer, Signal, QDateTime, QThread
from PySide6.QtGui import QCursor, QIcon, QKeySequence, QShortcut
from PySide6.QtWidgets import (
QApplication,
@@ -24,7 +24,9 @@
QMessageBox,
QComboBox,
QDateEdit,
- QCheckBox
+ QCheckBox,
+ QFrame,
+ QToolButton,
)
from ..i18n import i18n
@@ -38,11 +40,25 @@
from .toast import Toast
from .reminder_widget import ReminderWidget
from .reminder_dialog import ReminderDialog
+from .sidebar_widget import AppSidebar
+from .skeleton_card import SkeletonCard
+from .search_box import SearchBox
+from .filter_chip_bar import FilterChipBar
PRIME_COUNT = 9
-PAGE_SIZE = 30
-LOADER_DELAY_MS = 300
+PAGE_SIZE = 9
+ITEMS_PER_PAGE = 9
+LOADER_DELAY_MS = 120
+CARD_W = 260
+CARD_H = 168
+CARD_GAP = 12
+
+# Sidebar key -> tab index sırası
+NAV_TAB_ORDER = [
+ "all", "text", "image", "files", "fav",
+ "notes", "reminders", "snippets", "todos", "drawings", "video",
+]
def row_val(row, key, default=None):
@@ -80,6 +96,26 @@ def __init__(self, text: str = "Yükleniyor…", parent: Optional[QWidget] = Non
lay.addWidget(self.lbl)
+class SearchWorker(QThread):
+ """Runs the expensive fuzzy database scan away from the GUI thread."""
+
+ completed = Signal(int, object)
+ failed = Signal(int, str)
+
+ def __init__(self, storage: Storage, token: int, parameters: dict, parent=None):
+ super().__init__(parent)
+ self.storage = storage
+ self.token = token
+ self.parameters = parameters
+
+ def run(self):
+ try:
+ results = self.storage.search_items_threadsafe(**self.parameters)
+ self.completed.emit(self.token, results)
+ except Exception as exc:
+ self.failed.emit(self.token, str(exc))
+
+
class HistoryWindow(QWidget):
open_settings_requested = Signal()
@@ -91,12 +127,16 @@ def __init__(self, storage: Storage, settings: Settings):
self._open_settings_handler: Optional[Callable[[], None]] = None
self._notifier: Optional[Callable[[str, str], None]] = None
- self.resize(900, 560)
+ self.setObjectName("HistoryWindow")
+ self.setAttribute(Qt.WA_StyledBackground, True)
+ # Sidebar (~228) + 3×260 kart + boşluklar → 3x3 grid için
+ self.resize(1120, 720)
+ self.setMinimumWidth(1080)
+ self.setMinimumHeight(640)
# Üst bar - Arama
- self.search = QLineEdit()
- self.search.setMinimumHeight(32)
- self.search.setPlaceholderText("Ara... (Fuzzy search destekli)")
+ self.search = SearchBox()
+ self.search.setMinimumHeight(36)
self.search.textChanged.connect(self._on_search_changed)
# Gelişmiş filtreler
@@ -149,9 +189,15 @@ def __init__(self, storage: Storage, settings: Settings):
# Filtre toggle butonu
self.btn_toggle_filters = QPushButton("🔍 Gelişmiş")
+ self.btn_toggle_filters.setObjectName("FilterToggleBtn")
self.btn_toggle_filters.setMinimumHeight(32)
self.btn_toggle_filters.setCheckable(True)
self.btn_toggle_filters.clicked.connect(self._toggle_filters)
+ self.btn_toggle_filters.setVisible(False)
+
+ self.chip_bar = FilterChipBar()
+ self.chip_bar.filter_changed.connect(self._on_chip_filter_changed)
+ self.chip_bar.view_mode_changed.connect(self._on_view_mode_changed)
# Filtre paneli layout
filter_layout = QHBoxLayout(self.filter_panel)
@@ -168,7 +214,7 @@ def __init__(self, storage: Storage, settings: Settings):
filter_layout.addStretch()
# Not Ekle (yalnız Notlar sekmesinde görünür)
- self.btn_add_note = QPushButton()
+ self.btn_add_note = QPushButton(self)
self.btn_add_note.setMinimumHeight(32)
self.btn_add_note.clicked.connect(self._add_note_dialog)
self.btn_add_note.setVisible(False)
@@ -179,7 +225,7 @@ def __init__(self, storage: Storage, settings: Settings):
except Exception:
pass
- self.btn_clear_reminders = QPushButton()
+ self.btn_clear_reminders = QPushButton(self)
self.btn_clear_reminders.setMinimumHeight(32)
self.btn_clear_reminders.clicked.connect(self._clear_all_reminders)
self.btn_clear_reminders.setVisible(False)
@@ -190,7 +236,7 @@ def __init__(self, storage: Storage, settings: Settings):
except Exception:
pass
- self.btn_add_reminder = QPushButton()
+ self.btn_add_reminder = QPushButton(self)
self.btn_add_reminder.setMinimumHeight(32)
self.btn_add_reminder.clicked.connect(self._add_reminder_dialog)
self.btn_add_reminder.setVisible(False)
@@ -202,7 +248,7 @@ def __init__(self, storage: Storage, settings: Settings):
pass
# Tüm notları temizle (yalnız Notlar sekmesinde görünür)
- self.btn_clear_notes = QPushButton()
+ self.btn_clear_notes = QPushButton(self)
self.btn_clear_notes.setMinimumHeight(32)
self.btn_clear_notes.clicked.connect(self._clear_all_notes)
self.btn_clear_notes.setVisible(False)
@@ -214,7 +260,7 @@ def __init__(self, storage: Storage, settings: Settings):
pass
# Yeni snippet ekle
- self.btn_add_snippet = QPushButton()
+ self.btn_add_snippet = QPushButton(self)
self.btn_add_snippet.setMinimumHeight(32)
self.btn_add_snippet.clicked.connect(self._add_new_snippet)
self.btn_add_snippet.setVisible(False)
@@ -226,7 +272,7 @@ def __init__(self, storage: Storage, settings: Settings):
pass
# Tüm snippet'leri temizle
- self.btn_clear_snippets = QPushButton()
+ self.btn_clear_snippets = QPushButton(self)
self.btn_clear_snippets.setMinimumHeight(32)
self.btn_clear_snippets.clicked.connect(self._clear_all_snippets)
self.btn_clear_snippets.setVisible(False)
@@ -238,7 +284,7 @@ def __init__(self, storage: Storage, settings: Settings):
pass
# Yeni Liste butonu
- self.btn_add_todo = QPushButton()
+ self.btn_add_todo = QPushButton(self)
self.btn_add_todo.setMinimumHeight(32)
self.btn_add_todo.clicked.connect(self._create_new_todo_list)
try:
@@ -246,7 +292,7 @@ def __init__(self, storage: Storage, settings: Settings):
except Exception:
pass
- self.btn_clear_todos = QPushButton()
+ self.btn_clear_todos = QPushButton(self)
self.btn_clear_todos.setMinimumHeight(32)
self.btn_clear_todos.setVisible(False)
self.btn_clear_todos.clicked.connect(self._clear_all_todo_lists)
@@ -256,7 +302,7 @@ def __init__(self, storage: Storage, settings: Settings):
pass
# Yeni çizim
- self.btn_add_drawing = QPushButton()
+ self.btn_add_drawing = QPushButton(self)
self.btn_add_drawing.setMinimumHeight(32)
self.btn_add_drawing.clicked.connect(self._create_new_drawing)
try:
@@ -265,7 +311,7 @@ def __init__(self, storage: Storage, settings: Settings):
pass
# Çizimleri temizle
- self.btn_clear_drawings = QPushButton()
+ self.btn_clear_drawings = QPushButton(self)
self.btn_clear_drawings.setMinimumHeight(32)
self.btn_clear_drawings.clicked.connect(self._clear_all_drawings)
try:
@@ -274,7 +320,7 @@ def __init__(self, storage: Storage, settings: Settings):
pass
# Ayarlar
- self.btn_settings = QPushButton()
+ self.btn_settings = QPushButton(self)
self.btn_settings.setMinimumHeight(32)
self.btn_settings.clicked.connect(self._on_open_settings_clicked)
try:
@@ -283,7 +329,7 @@ def __init__(self, storage: Storage, settings: Settings):
pass
# Geçmişi temizle (tüm öğeler)
- self.btn_clear = QPushButton()
+ self.btn_clear = QPushButton(self)
self.btn_clear.setMinimumHeight(32)
self.btn_clear.clicked.connect(self.clear_history)
try:
@@ -292,21 +338,23 @@ def __init__(self, storage: Storage, settings: Settings):
pass
top = QHBoxLayout()
+ top.setContentsMargins(10, 8, 10, 4)
+ top.setSpacing(8)
top.addWidget(self.search, 1)
- top.addWidget(self.btn_toggle_filters)
top.addStretch(1)
top.addWidget(self.btn_add_todo)
top.addWidget(self.btn_add_note)
top.addWidget(self.btn_add_reminder)
top.addWidget(self.btn_add_snippet)
- top.addWidget(self.btn_add_drawing) # Yeni Çizim butonu
- top.addWidget(self.btn_clear_reminders) # Tümünü Sil (Hatırlatmalar)
- top.addWidget(self.btn_clear_notes) # Tümünü Sil (Notlar)
- top.addWidget(self.btn_clear_snippets) # Tümünü Sil (Snippets)
- top.addWidget(self.btn_clear_todos) # Tümünü Sil (Listeler)
- top.addWidget(self.btn_clear_drawings) # Tümünü Sil (Çizimler)
- top.addWidget(self.btn_clear) # Tümünü Sil (Geçmiş) - solda
- top.addWidget(self.btn_settings) # Ayarlar - sağda
+ top.addWidget(self.btn_add_drawing)
+ # Tümünü sil / ayarlar sidebara taşındı
+ self.btn_settings.setVisible(False)
+ self.btn_clear.setVisible(False)
+ self.btn_clear_notes.setVisible(False)
+ self.btn_clear_reminders.setVisible(False)
+ self.btn_clear_snippets.setVisible(False)
+ self.btn_clear_todos.setVisible(False)
+ self.btn_clear_drawings.setVisible(False)
# Sekmeler
self.tabs = QTabWidget()
@@ -317,14 +365,18 @@ def __init__(self, storage: Storage, settings: Settings):
self._no_results_widget = QWidget()
no_results_layout = QVBoxLayout(self._no_results_widget)
no_results_layout.setAlignment(Qt.AlignCenter)
- self._no_results_icon = QLabel("🔍")
- self._no_results_icon.setStyleSheet("font-size: 48px;")
+ self._no_results_icon = QLabel()
self._no_results_icon.setAlignment(Qt.AlignCenter)
+ try:
+ self._no_results_icon.setPixmap(svg_icon("assets/icons/search.svg").pixmap(QSize(48, 48)))
+ except Exception:
+ self._no_results_icon.setText("🔍")
+ self._no_results_icon.setStyleSheet("font-size: 48px;")
self._no_results_label = QLabel("Sonuç bulunamadı")
- self._no_results_label.setStyleSheet("font-size: 16px; color: #888;")
+ self._no_results_label.setStyleSheet("font-size: 16px; color: #94a3b8;")
self._no_results_label.setAlignment(Qt.AlignCenter)
self._no_results_hint = QLabel("Farklı bir arama terimi deneyin")
- self._no_results_hint.setStyleSheet("font-size: 12px; color: #666;")
+ self._no_results_hint.setStyleSheet("font-size: 12px; color: #64748b;")
self._no_results_hint.setAlignment(Qt.AlignCenter)
no_results_layout.addWidget(self._no_results_icon)
no_results_layout.addWidget(self._no_results_label)
@@ -535,12 +587,70 @@ def __init__(self, storage: Storage, settings: Settings):
# Sekme değişimi (tablar eklendikten sonra bağla)
self.tabs.currentChanged.connect(self._on_tab_changed)
+ # Sekme çubuğunu gizle — navigasyon sidebar üzerinden
+ try:
+ self.tabs.tabBar().hide()
+ except Exception:
+ pass
- # Kök
- root = QVBoxLayout(self)
- root.addLayout(top)
- root.addWidget(self.filter_panel) # Filtre panelini ekle
- root.addWidget(self.tabs, 1)
+ # Pagination bar
+ self.pagination_bar = QWidget()
+ self.pagination_bar.setObjectName("PaginationBar")
+ pag_lay = QHBoxLayout(self.pagination_bar)
+ pag_lay.setContentsMargins(8, 6, 8, 6)
+ self.btn_page_prev = QPushButton(" Önceki")
+ self.btn_page_prev.setMinimumHeight(30)
+ try:
+ self.btn_page_prev.setIcon(svg_icon("assets/icons/chevron_left.svg"))
+ self.btn_page_prev.setIconSize(QSize(16, 16))
+ except Exception:
+ self.btn_page_prev.setText("‹ Önceki")
+ self.btn_page_prev.clicked.connect(lambda: self._goto_page(-1))
+ self.lbl_page = QLabel("Sayfa 1")
+ self.lbl_page.setAlignment(Qt.AlignCenter)
+ self.btn_page_next = QPushButton("Sonraki ")
+ self.btn_page_next.setMinimumHeight(30)
+ try:
+ self.btn_page_next.setIcon(svg_icon("assets/icons/chevron_right.svg"))
+ self.btn_page_next.setIconSize(QSize(16, 16))
+ # İkon sağda olsun
+ self.btn_page_next.setLayoutDirection(Qt.RightToLeft)
+ except Exception:
+ self.btn_page_next.setText("Sonraki ›")
+ self.btn_page_next.clicked.connect(lambda: self._goto_page(1))
+ pag_lay.addWidget(self.btn_page_prev)
+ pag_lay.addStretch(1)
+ pag_lay.addWidget(self.lbl_page)
+ pag_lay.addStretch(1)
+ pag_lay.addWidget(self.btn_page_next)
+
+ # Sidebar
+ self.sidebar = AppSidebar(self)
+ self.sidebar.nav_changed.connect(self._on_sidebar_nav)
+ self.sidebar.settings_clicked.connect(self._on_open_settings_clicked)
+ if bool(self.settings.get("sidebar_collapsed", False)):
+ self.sidebar.set_collapsed(True)
+
+ # Ana içerik sütunu
+ content_col = QVBoxLayout()
+ content_col.setContentsMargins(0, 0, 0, 0)
+ content_col.setSpacing(4)
+ content_col.addLayout(top)
+ content_col.addWidget(self.chip_bar)
+ content_col.addWidget(self.filter_panel)
+ content_col.addWidget(self.tabs, 1)
+ content_col.addWidget(self.pagination_bar)
+
+ # Kök: sidebar + içerik
+ root = QHBoxLayout(self)
+ root.setContentsMargins(0, 0, 0, 0)
+ root.setSpacing(0)
+ root.addWidget(self.sidebar)
+ root.addLayout(content_col, 1)
+
+ # Üst arama her zaman görünür
+ self.search.setVisible(True)
+ self.btn_toggle_filters.setVisible(False)
# Toast
self._toast = Toast(self)
@@ -557,6 +667,23 @@ def __init__(self, storage: Storage, settings: Settings):
self._reminder_cards: List[ReminderWidget] = []
self._snippet_cards = []
self._first_show = True # İlk açılış kontrolü
+ self._page_index = 0
+ self._page_size_dynamic = ITEMS_PER_PAGE
+ self._total_for_page = 0
+ self._skeleton_widgets: list = []
+ self._nav_key = "all"
+ self._nav_load_token = 0
+ self._nav_load_timer: Optional[QTimer] = None
+ self._search_generation = 0
+ self._search_thread: Optional[SearchWorker] = None
+ self._queued_search = None
+ self._search_timer = QTimer(self)
+ self._search_timer.setSingleShot(True)
+ self._search_timer.timeout.connect(self._perform_search)
+ # Geriye uyumluluk: eski sağ arama paneli yok
+ self.search_panel = QWidget(self)
+ self.search_panel.hide()
+ self.search_side = self.search
# Sayfalama durumları
self._offset_all = 0
@@ -605,28 +732,21 @@ def __init__(self, storage: Storage, settings: Settings):
self.refresh_texts()
# ESC ile gizle
- QShortcut(QKeySequence("Escape"), self, activated=self.hide)
+ QShortcut(QKeySequence("Escape"), self, activated=self._on_escape)
# Klavye ile hızlı kullanım
- QShortcut(QKeySequence("Ctrl+F"), self, activated=lambda: self.search.setFocus())
+ QShortcut(QKeySequence("Ctrl+F"), self, activated=self._focus_search)
+ QShortcut(QKeySequence("Ctrl+K"), self, activated=self._focus_search)
QShortcut(QKeySequence("Return"), self, activated=self._kb_copy_selected)
QShortcut(QKeySequence("Ctrl+Return"), self, activated=self._kb_paste_selected)
QShortcut(QKeySequence("Delete"), self, activated=self._kb_delete_selected)
QShortcut(QKeySequence("Ctrl+P"), self, activated=self._kb_pin_selected)
QShortcut(QKeySequence("Up"), self, activated=lambda: self._kb_move_selection(-1))
QShortcut(QKeySequence("Down"), self, activated=lambda: self._kb_move_selection(1))
- QShortcut(QKeySequence("Tab"), self, activated=lambda: self.tabs.setCurrentIndex((self.tabs.currentIndex() + 1) % self.tabs.count()))
+ QShortcut(QKeySequence("Tab"), self, activated=self._kb_next_nav)
for i in range(1, 10):
QShortcut(QKeySequence(f"Alt+{i}"), self, activated=lambda n=i: self._kb_paste_nth(n - 1))
- # Scroll izleme
- self.scroll_all.verticalScrollBar().valueChanged.connect(lambda _: self._maybe_load_more("all"))
- self.scroll_text.verticalScrollBar().valueChanged.connect(lambda _: self._maybe_load_more("text"))
- self.scroll_image.verticalScrollBar().valueChanged.connect(lambda _: self._maybe_load_more("image"))
- self.scroll_files.verticalScrollBar().valueChanged.connect(lambda _: self._maybe_load_more("files"))
- self.scroll_fav.verticalScrollBar().valueChanged.connect(lambda _: self._maybe_load_more("fav"))
- self.scroll_notes.verticalScrollBar().valueChanged.connect(lambda _: self._maybe_load_more("notes"))
- self.scroll_reminders.verticalScrollBar().valueChanged.connect(lambda _: self._maybe_load_more("reminders"))
-
+ # Sonsuz scroll kaldırıldı — pagination kullanılıyor
# Başlangıçta buton görünürlüğünü ayarla
self._on_tab_changed(self.tabs.currentIndex())
@@ -637,9 +757,12 @@ def __init__(self, storage: Storage, settings: Settings):
def _initial_load(self):
"""Constructor'dan çağrılır - tüm verileri ilk kez yükle"""
if self._first_show: # Sadece ilk seferde
+ # Mark it before any nested processEvents() call can dispatch the
+ # constructor's delayed callback and enter this method again.
+ self._first_show = False
print("[DEBUG] _initial_load: Tüm veriler yükleniyor...")
- # Clip items (all, text, image, fav)
+ # Clip items — pagination
self.reload_items()
# Notlar
@@ -648,22 +771,25 @@ def _initial_load(self):
self._loading_notes = False
self._load_page("notes", first=True)
- # Hatırlatmalar
+ # Hatırlatmalar — sekme açılınca yüklenecek (çift eklemeyi önle)
self._offset_reminders = 0
self._no_more_reminders = False
self._loading_reminders = False
- self._load_page("reminders", first=True)
+ self._reminders_loaded = False
# Snippet'ler
self._load_snippets()
+
+ self._refresh_chip_meta()
# Todo listeleri
self._load_todo_lists()
# Çizimler
self._load_drawings()
+
+ self._refresh_sidebar_counts()
- self._first_show = False
print("[DEBUG] _initial_load: Yükleme tamamlandı!")
def _tr(self, key: str, fallback: str) -> str:
@@ -673,6 +799,508 @@ def _tr(self, key: str, fallback: str) -> str:
v = ""
return v if v and v != key else fallback
+ # ---------- Sidebar / Arama / Pagination ----------
+
+ def _on_escape(self):
+ if self.search.hasFocus() and self.search.text():
+ self.search.clear()
+ return
+ self.hide()
+
+ def _on_chip_filter_changed(self):
+ """Chip bar değişince sayfayı yenile / tip filtrelerini uygula."""
+ tf = getattr(self.chip_bar, "type_filter", "all")
+ # Sidebar ile senkron (metin/resim/dosya)
+ nav_map = {"text": "text", "image": "image", "file": "files", "all": None, "code": "text"}
+ target = nav_map.get(tf)
+ if target and self._nav_key != target:
+ self._on_sidebar_nav(target)
+ return
+ self._page_index = 0
+ self._schedule_page_reload()
+ self._refresh_chip_meta()
+
+ def _on_view_mode_changed(self, mode: str):
+ """Izgara / liste görünümünü tüm kartlara uygula."""
+ self._apply_view_mode_to_cards(mode)
+ self._refresh_layouts()
+
+ def _apply_view_mode_to_cards(self, mode: str | None = None):
+ mode = mode or getattr(self.chip_bar, "view_mode", "grid")
+ groups = [
+ getattr(self, "_items_all", []),
+ getattr(self, "_items_text", []),
+ getattr(self, "_items_image", []),
+ getattr(self, "_items_files", []),
+ getattr(self, "_items_fav", []),
+ getattr(self, "_note_cards", []),
+ getattr(self, "_snippet_cards", []),
+ getattr(self, "_todo_cards", []),
+ getattr(self, "_drawing_cards", []),
+ ]
+ for group in groups:
+ for w in list(group or []):
+ try:
+ if hasattr(w, "set_view_mode"):
+ w.set_view_mode(mode)
+ else:
+ w._view_mode = mode
+ except Exception:
+ pass
+ # Skeleton kartlar
+ for which, sk in list(getattr(self, "_skeleton_widgets", []) or []):
+ try:
+ from .card_common import CARD_W, CARD_H, LIST_H
+ if mode == "list":
+ sk._view_mode = "list"
+ sk.setFixedHeight(LIST_H)
+ sk.setMinimumWidth(320)
+ sk.setMaximumWidth(16777215)
+ else:
+ sk._view_mode = "grid"
+ sk.setFixedSize(CARD_W, CARD_H)
+ except Exception:
+ pass
+
+ def _refresh_chip_meta(self):
+ """Chip menülerine uygulama / etiket listesi doldur."""
+ try:
+ rows = self.storage.list_items(limit=300, favorites_only=False, offset=0)
+ apps = []
+ tags = []
+ for r in rows:
+ src = row_val(r, "source_app") or ""
+ if src:
+ apps.append(str(src).replace(".exe", ""))
+ t = row_val(r, "tags") or ""
+ if t:
+ tags.extend([x.strip() for x in str(t).split(",") if x.strip()])
+ self.chip_bar.set_available_apps(apps)
+ self.chip_bar.set_available_tags(tags)
+ except Exception:
+ pass
+
+ def _clear_reminder_cards(self):
+ """Hatırlatma kartlarını tamamen temizle (layout orphan'ları dahil)."""
+ self._reminder_cards = []
+ lay = getattr(self, "flow_reminders", None)
+ if lay is None:
+ return
+ while lay.count():
+ item = lay.takeAt(0)
+ w = item.widget() if item is not None else None
+ if w is not None:
+ w.hide()
+ w.deleteLater()
+ lay.addStretch()
+
+ def _clear_snippet_cards(self):
+ for w in list(getattr(self, "_snippet_cards", []) or []):
+ try:
+ self.flow_snippets.removeWidget(w)
+ except Exception:
+ pass
+ w.hide()
+ w.deleteLater()
+ self._snippet_cards = []
+
+ def _focus_search(self):
+ self.search.setFocus()
+ self.search.selectAll()
+
+ def _open_search_panel(self):
+ # Geriye uyumluluk — üst aramaya odaklan
+ self._focus_search()
+
+ def _close_search_panel(self):
+ pass
+
+ def _on_side_search_changed(self, text: str):
+ pass
+
+ def _kb_next_nav(self):
+ keys = NAV_TAB_ORDER
+ try:
+ idx = keys.index(self._nav_key)
+ except ValueError:
+ idx = 0
+ nxt = keys[(idx + 1) % len(keys)]
+ self._on_sidebar_nav(nxt)
+
+ def _on_sidebar_nav(self, key: str):
+ self._nav_key = key
+ self.sidebar.set_current(key)
+ try:
+ tab_idx = NAV_TAB_ORDER.index(key)
+ except ValueError:
+ tab_idx = 0
+ self.tabs.blockSignals(True)
+ self.tabs.setCurrentIndex(tab_idx)
+ self.tabs.blockSignals(False)
+ self._page_index = 0
+ self._on_tab_changed(tab_idx)
+ if (self.search.text() or "").strip() or self.filter_panel.isVisible():
+ self._search_timer.stop()
+ self._perform_search()
+ return
+ # Önce skeleton, sonra gecikmeli yükleme — UI takılmasın
+ self._schedule_page_reload()
+
+ def _schedule_page_reload(self):
+ key = self._nav_key
+ self._nav_load_token += 1
+ token = self._nav_load_token
+ if key in ("all", "text", "image", "files", "fav"):
+ self._clear_clip_flow(key)
+ self._show_skeletons(key, ITEMS_PER_PAGE)
+ if self._nav_load_timer is None:
+ self._nav_load_timer = QTimer(self)
+ self._nav_load_timer.setSingleShot(True)
+ try:
+ self._nav_load_timer.timeout.disconnect()
+ except Exception:
+ pass
+ self._nav_load_timer.timeout.connect(lambda t=token: self._deferred_reload(t))
+ self._nav_load_timer.start(16)
+
+ # Beklenmeyen bir Qt nesne hatası yükleme kartlarını ekranda sonsuza
+ # kadar bırakmasın. Yalnızca hâlâ geçerli olan isteği temizler.
+ QTimer.singleShot(5000, lambda t=token, k=key: self._expire_skeletons(t, k))
+
+ def _expire_skeletons(self, token: int, which: str):
+ if token != self._nav_load_token:
+ return
+ self._clear_skeletons(which)
+ self._reflow_now(which)
+
+ def _deferred_reload(self, token: int):
+ if token != self._nav_load_token:
+ return
+ key = self._nav_key
+ try:
+ self._reload_current_page()
+ except Exception as exc:
+ print(f"[ERROR] deferred page reload ({key}): {exc}")
+ finally:
+ # Kart oluşturma/layout aşamasında hata olsa bile yükleme ekranı
+ # kalıcı hâle gelmemeli.
+ if token == self._nav_load_token:
+ self._clear_skeletons(key)
+ self._reflow_now(key)
+
+ def _calc_page_size(self) -> int:
+ """Sabit 9'lu grid."""
+ return ITEMS_PER_PAGE
+
+ def _current_scroll(self):
+ key = self._nav_key
+ return {
+ "all": getattr(self, "scroll_all", None),
+ "text": getattr(self, "scroll_text", None),
+ "image": getattr(self, "scroll_image", None),
+ "files": getattr(self, "scroll_files", None),
+ "fav": getattr(self, "scroll_fav", None),
+ "notes": getattr(self, "scroll_notes", None),
+ "reminders": getattr(self, "scroll_reminders", None),
+ }.get(key)
+
+ def _current_clip_items(self):
+ key = self._nav_key
+ return {
+ "all": self._items_all,
+ "text": self._items_text,
+ "image": self._items_image,
+ "files": self._items_files,
+ "fav": self._items_fav,
+ }.get(key, self._items_all)
+
+ def _update_pagination_label(self):
+ size = max(1, self._page_size_dynamic)
+ total = max(0, self._total_for_page)
+ pages = max(1, (total + size - 1) // size) if total else 1
+ page = min(self._page_index + 1, pages)
+ self.lbl_page.setText(f"Sayfa {page} / {pages} · {total} öğe")
+ self.btn_page_prev.setEnabled(self._page_index > 0)
+ self.btn_page_next.setEnabled(self._page_index + 1 < pages)
+
+ def _goto_page(self, delta: int):
+ size = max(1, self._page_size_dynamic)
+ total = max(0, self._total_for_page)
+ pages = max(1, (total + size - 1) // size) if total else 1
+ new_idx = max(0, min(pages - 1, self._page_index + delta))
+ if new_idx == self._page_index:
+ return
+ self._page_index = new_idx
+ self._schedule_page_reload()
+
+ def _purge_flow(self, flow) -> None:
+ """FlowLayout içindeki tüm widget'ları anında temizle (orphan kalmasın)."""
+ if flow is None:
+ return
+ while flow.count():
+ try:
+ item = flow.takeAt(0)
+ except Exception as exc:
+ print(f"[WARN] _purge_flow takeAt: {exc}")
+ break
+ if item is None:
+ break
+ w = item.widget()
+ if w is not None:
+ try:
+ w.hide()
+ w.deleteLater()
+ except RuntimeError:
+ # Layout öğesi alınmış durumda; C++ nesnesi önceden
+ # silindiyse kalan öğeleri temizlemeye devam et.
+ pass
+
+ def _show_skeletons(self, which: str, count: int = 9):
+ self._clear_skeletons(which)
+ flow = self._flow_for(which)
+ if flow is None:
+ return
+ for _ in range(count):
+ # Doğrudan layout sahibine bağlamak, kısa süreli bile olsa native
+ # pencere olarak oluşmasını önler.
+ sk = SkeletonCard(flow.parentWidget())
+ try:
+ mode = getattr(self.chip_bar, "view_mode", "grid")
+ from .card_common import CARD_W, CARD_H, LIST_H
+ if mode == "list":
+ sk._view_mode = "list"
+ sk.setFixedHeight(LIST_H)
+ sk.setMinimumWidth(320)
+ sk.setMaximumWidth(16777215)
+ except Exception:
+ pass
+ flow.addWidget(sk)
+ # Yeni QWidget varsayılan olarak explicit hidden başlar. Reflow'dan
+ # önce görünür işaretlenmezse FlowLayout onu atlar ve widget ilk
+ # paint'te (0, 0)'da 1. skeleton'ın üzerine çizilir.
+ sk.show()
+ self._skeleton_widgets.append((which, sk))
+ self._reflow_now(which if which in ("all", "text", "image", "files", "fav", "notes", "reminders") else "all")
+
+ def _clear_skeletons(self, which: str = None):
+ # Registry'yi önce ayır. deleteLater() ile C++ tarafında önceden
+ # silinmiş tek bir nesne diğer skeleton'ların temizliğini kesmemeli.
+ registered = list(self._skeleton_widgets)
+ self._skeleton_widgets = [
+ (w_which, sk)
+ for w_which, sk in registered
+ if which is not None and w_which != which
+ ]
+ affected = set()
+ for w_which, sk in registered:
+ if which is None or w_which == which:
+ affected.add(w_which)
+ flow = self._flow_for(w_which)
+ try:
+ if flow:
+ flow.removeWidget(sk)
+ except Exception:
+ pass
+ try:
+ sk.hide()
+ sk.deleteLater()
+ except RuntimeError:
+ pass
+ for w_which in affected:
+ flow = self._flow_for(w_which)
+ try:
+ if flow is not None:
+ flow.invalidate()
+ except Exception:
+ pass
+
+ def _flow_for(self, which: str):
+ return {
+ "all": getattr(self, "flow_all", None),
+ "text": getattr(self, "flow_text", None),
+ "image": getattr(self, "flow_image", None),
+ "files": getattr(self, "flow_files", None),
+ "fav": getattr(self, "flow_fav", None),
+ "notes": getattr(self, "flow_notes", None),
+ "reminders": getattr(self, "flow_reminders", None),
+ }.get(which)
+
+ def _reload_current_page(self):
+ key = self._nav_key
+ if key in ("all", "text", "image", "files", "fav"):
+ self._load_paginated_clips(key)
+ elif key == "notes":
+ self._offset_notes = self._page_index * ITEMS_PER_PAGE
+ self._no_more_notes = False
+ self._clear_notes_flow()
+ self._show_skeletons("notes", ITEMS_PER_PAGE)
+ self._load_page("notes", first=True)
+ self._clear_skeletons("notes")
+ elif key == "reminders":
+ self._offset_reminders = self._page_index * ITEMS_PER_PAGE
+ self._no_more_reminders = False
+ self._loading_reminders = False
+ self._clear_reminder_cards()
+ self._load_page("reminders", first=True)
+ self._reminders_loaded = True
+ self._refresh_sidebar_counts()
+
+ def _clear_notes_flow(self):
+ for w in list(self._note_cards):
+ try:
+ self.flow_notes.removeWidget(w)
+ except Exception:
+ pass
+ w.hide()
+ w.deleteLater()
+ self._note_cards.clear()
+ self._purge_flow(self.flow_notes)
+
+ def _load_paginated_clips(self, which: str):
+ """Sabit tam 9'lu sayfa."""
+ self._page_size_dynamic = ITEMS_PER_PAGE
+ limit = ITEMS_PER_PAGE
+ offset = self._page_index * limit
+
+ # _schedule_page_reload skeleton'ları zaten kurdu. Burada ikinci bir
+ # takım oluşturmak deleteLater yarışına ve ekranda takılı kalmalarına
+ # neden oluyordu.
+ rows = []
+ sort_m = getattr(self.chip_bar, "sort_mode", "newest")
+ try:
+ if which == "text":
+ types = [int(ClipItemType.TEXT), int(ClipItemType.HTML)]
+ self._total_for_page = self.storage.count_items_by_types(types)
+ rows = self.storage.list_items_by_types(types, limit=limit, offset=offset, sort=sort_m)
+ elif which == "image":
+ types = [int(ClipItemType.IMAGE)]
+ self._total_for_page = self.storage.count_items_by_types(types)
+ rows = self.storage.list_items_by_types(types, limit=limit, offset=offset, sort=sort_m)
+ elif which == "files":
+ types = [int(ClipItemType.FILE)]
+ self._total_for_page = self.storage.count_items_by_types(types)
+ rows = self.storage.list_items_by_types(types, limit=limit, offset=offset, sort=sort_m)
+ elif which == "fav":
+ self._total_for_page = int(self.storage.count_summary().get("fav", 0))
+ rows = self.storage.list_items(limit=limit, favorites_only=True, offset=offset, sort=sort_m)
+ else:
+ self._total_for_page = int(self.storage.count_summary().get("all", 0))
+ rows = self.storage.list_items(limit=limit, favorites_only=False, offset=offset, sort=sort_m)
+ except Exception as e:
+ print(f"[ERROR] _load_paginated_clips: {e}")
+ rows = []
+ finally:
+ self._clear_skeletons(which)
+
+ # Orphan/eski kart temizliği — sonra sadece max 9 kart
+ self._clear_clip_flow(which)
+ rows = list(rows or [])[:ITEMS_PER_PAGE]
+
+ # Chip bar: uygulama / etiket / kod tipi (sıralama DB'de yapıldı)
+ try:
+ app_f = (getattr(self.chip_bar, "app_filter", "") or "").lower()
+ tag_f = (getattr(self.chip_bar, "tag_filter", "") or "").lower()
+ type_f = getattr(self.chip_bar, "type_filter", "all")
+ if type_f == "code":
+ from ..smart_content import analyze_text, ContentKind
+ filtered = []
+ for r in rows:
+ txt = row_val(r, "text_content") or ""
+ if analyze_text(txt).kind in (ContentKind.CODE, ContentKind.JSON):
+ filtered.append(r)
+ rows = filtered
+ if app_f:
+ rows = [r for r in rows if app_f in str(row_val(r, "source_app") or "").lower()]
+ if tag_f:
+ rows = [r for r in rows if tag_f in str(row_val(r, "tags") or "").lower()]
+ except Exception:
+ pass
+ rows = rows[:ITEMS_PER_PAGE]
+
+ for i, row in enumerate(rows):
+ try:
+ self._add_row_widget(which, row, immediate_layout=(i == len(rows) - 1))
+ except Exception as exc:
+ # Bozuk tek bir geçmiş kaydı tüm sayfanın yüklenmesini
+ # engellemesin.
+ print(f"[WARN] card could not be created ({which}): {exc}")
+
+ # Emniyet: 9'dan fazla görünür kart kalmasın
+ self._enforce_page_cap(which)
+ self._apply_view_mode_to_cards()
+ self._reflow_now(which)
+
+ self._update_pagination_label()
+ QTimer.singleShot(0, self._refresh_layouts)
+
+ def _enforce_page_cap(self, which: str):
+ mapping = {
+ "all": self._items_all,
+ "text": self._items_text,
+ "image": self._items_image,
+ "files": self._items_files,
+ "fav": self._items_fav,
+ }
+ items = mapping.get(which)
+ flow = self._flow_for(which)
+ if items is None or flow is None:
+ return
+ while len(items) > ITEMS_PER_PAGE:
+ w = items.pop()
+ try:
+ flow.removeWidget(w)
+ except Exception:
+ pass
+ w.hide()
+ w.deleteLater()
+
+ def _clear_clip_flow(self, which: str):
+ mapping = {
+ "all": (self._items_all, self.flow_all),
+ "text": (self._items_text, self.flow_text),
+ "image": (self._items_image, self.flow_image),
+ "files": (self._items_files, self.flow_files),
+ "fav": (self._items_fav, self.flow_fav),
+ }
+ if which not in mapping:
+ return
+ items, flow = mapping[which]
+ self._clear_skeletons(which)
+ for w in list(items):
+ try:
+ flow.removeWidget(w)
+ except Exception:
+ pass
+ try:
+ w.hide()
+ w.deleteLater()
+ except RuntimeError:
+ pass
+ items.clear()
+ # Listede olmayan orphan widget'ları da sil (skeleton/loader kalıntısı)
+ self._purge_flow(flow)
+ self._skeleton_widgets = [(k, sk) for k, sk in self._skeleton_widgets if k != which]
+
+ def _refresh_sidebar_counts(self):
+ try:
+ counts = self.storage.count_summary()
+ self.sidebar.set_counts(counts)
+ except Exception as e:
+ print(f"[WARN] sidebar counts: {e}")
+
+ def refresh_sidebar_from_settings(self):
+ """Ayarlar kapandıktan sonra çağrılır."""
+ collapsed = bool(self.settings.get("sidebar_collapsed", False))
+ if collapsed != self.sidebar._collapsed:
+ self.sidebar.set_collapsed(collapsed)
+
+ def resizeEvent(self, e):
+ super().resizeEvent(e)
+ # Sayfa boyutu sabit 9 — resize'da yeniden sayfalama yok
+
+ def _on_resize_repage(self):
+ pass
+
# ---------- Gelişmiş Arama Fonksiyonları ----------
def _toggle_filters(self):
@@ -681,15 +1309,18 @@ def _toggle_filters(self):
self.filter_panel.setVisible(not is_visible)
self.btn_toggle_filters.setText("🔍 Gelişmiş ▲" if not is_visible else "🔍 Gelişmiş ▼")
- def _on_search_changed(self):
+ def _on_search_changed(self, _text: str = ""):
"""Arama metni değiştiğinde"""
- # Debounce için timer kullan
- if not hasattr(self, '_search_timer'):
- self._search_timer = QTimer()
- self._search_timer.setSingleShot(True)
- self._search_timer.timeout.connect(self._perform_search)
+ # Invalidate an in-flight result immediately. Only the latest query is
+ # allowed to update the cards when its worker finishes.
+ self._search_generation += 1
+ self._queued_search = None
self._search_timer.stop()
- self._search_timer.start(300) # 300ms bekle
+ # Boşaltınca hemen temizle — "sonuç yok" ekranı takılı kalmasın
+ if not (self.search.text() or "").strip():
+ self._exit_search_mode()
+ return
+ self._search_timer.start(220)
def _on_filter_changed(self):
"""Filtre değiştiğinde"""
@@ -699,32 +1330,58 @@ def _on_filter_changed(self):
self.lbl_date_to.setVisible(is_custom)
self.date_to.setVisible(is_custom)
+ self._search_generation += 1
+ self._queued_search = None
+ self._search_timer.stop()
# Arama yap
self._perform_search()
+
+ def _exit_search_mode(self):
+ """Arama temizlenince boş sonuç ekranını kapat ve içeriği geri getir."""
+ self._search_generation += 1
+ self._queued_search = None
+ try:
+ self._no_results_widget.setVisible(False)
+ self._search_loading_widget.setVisible(False)
+ self.scroll_all.setVisible(True)
+ except Exception:
+ pass
+ # Sayfayı temiz yeniden yükle (arama sırasında eklenen/gizlenen kartlar düzelir)
+ if self._nav_key in ("all", "text", "image", "files", "fav", "notes"):
+ self._schedule_page_reload()
+ else:
+ for w in self._items_all:
+ w.setVisible(True)
+ for w in self._items_text:
+ w.setVisible(True)
+ for w in self._items_image:
+ w.setVisible(True)
+ for w in getattr(self, "_items_files", []):
+ w.setVisible(True)
+ for w in self._items_fav:
+ w.setVisible(True)
+ for card in self._note_cards:
+ card.setVisible(True)
+ for card in self._reminder_cards:
+ card.setVisible(True)
+ self._refresh_layouts()
def _perform_search(self):
"""Gelişmiş arama yap"""
query = self.search.text().strip()
-
+
+ if not query and not self.filter_panel.isVisible():
+ self._exit_search_mode()
+ return
+
# Filtre paneli açık değilse normal arama yap
if not self.filter_panel.isVisible():
- # Arama boşsa tüm widget'ları göster
- if not query:
- for w in self._items_all:
- w.setVisible(True)
- for w in self._items_text:
- w.setVisible(True)
- for w in self._items_image:
- w.setVisible(True)
- for w in self._items_fav:
- w.setVisible(True)
- for card in self._note_cards:
- card.setVisible(True)
- for card in self._reminder_cards:
- card.setVisible(True)
- self._refresh_layouts()
- else:
- self.apply_filter(query)
+ self.apply_filter(query)
+ return
+
+ # Filtre paneli açık ama arama boş ve ekstra filtre yoksa çık
+ if not query and self.cmb_type_filter.currentData() is None and self.cmb_date_filter.currentData() == "all":
+ self._exit_search_mode()
return
# Tip filtresi
@@ -752,122 +1409,61 @@ def _perform_search(self):
# Fuzzy threshold
fuzzy_threshold = self.cmb_fuzzy.currentData()
- # Aramayı yap
- try:
- results = self.storage.search_items(
- query=query,
- item_types=item_types,
- date_from=date_from,
- date_to=date_to,
- fuzzy_threshold=fuzzy_threshold,
- limit=500
- )
-
- # Sonuçları göster
- self._display_search_results(results)
-
- except Exception as e:
- print(f"[ERROR] Arama hatası: {e}")
- import traceback
- traceback.print_exc()
+ self._search_in_database(
+ query,
+ item_types=item_types,
+ date_from=date_from,
+ date_to=date_to,
+ fuzzy_threshold=fuzzy_threshold,
+ )
def _display_search_results(self, results: List[dict]):
- """Arama sonuçlarını göster - yüklenmemiş içerikler için widget oluştur"""
- # Önce tüm widget'ları gizle
- for w in self._items_all:
- w.setVisible(False)
- for w in self._items_text:
- w.setVisible(False)
- for w in self._items_image:
- w.setVisible(False)
- for w in self._items_fav:
- w.setVisible(False)
-
- # Mevcut widget ID'lerini topla
- existing_ids_all = {w.row_id for w in self._items_all}
- existing_ids_text = {w.row_id for w in self._items_text}
- existing_ids_image = {w.row_id for w in self._items_image}
- existing_ids_fav = {w.row_id for w in self._items_fav}
-
- # Sonuçları göster veya oluştur
- for row in results:
- row_id = row["id"]
+ """Replace old search cards with at most one page of fresh results."""
+ for which in ("all", "text", "image", "files", "fav"):
+ self._clear_clip_flow(which)
+
+ def add_result(which: str, row: dict):
+ containers = {
+ "all": self.container_all,
+ "text": self.container_text,
+ "image": self.container_image,
+ "files": self.container_files,
+ "fav": self.container_fav,
+ }
+ flows = {
+ "all": self.flow_all,
+ "text": self.flow_text,
+ "image": self.flow_image,
+ "files": self.flow_files,
+ "fav": self.flow_fav,
+ }
+ groups = {
+ "all": self._items_all,
+ "text": self._items_text,
+ "image": self._items_image,
+ "files": self._items_files,
+ "fav": self._items_fav,
+ }
+ widget = self._wire_clip_widget(
+ ItemWidget(row, containers[which], settings=self.settings)
+ )
+ flows[which].addWidget(widget)
+ groups[which].append(widget)
+ widget.show()
+
+ for row in list(results or [])[:ITEMS_PER_PAGE]:
item_type = int(row.get("item_type", 0))
- is_favorite = bool(row.get("favorite", False))
-
- # ALL sekmesi
- if row_id in existing_ids_all:
- # Widget zaten var, sadece göster
- for w in self._items_all:
- if w.row_id == row_id:
- w.setVisible(True)
- break
- else:
- # Widget yok, oluştur
- w = ItemWidget(row, self.container_all)
- w.on_copy_requested.connect(self.on_copy_requested)
- w.on_delete_requested.connect(self.on_delete_requested)
- w.on_favorite_toggled.connect(self.on_favorite_toggled)
- self.flow_all.addWidget(w)
- self._items_all.append(w)
- w.setVisible(True)
-
- # TEXT sekmesi
+ add_result("all", row)
if item_type in (int(ClipItemType.TEXT), int(ClipItemType.HTML)):
- if row_id in existing_ids_text:
- for w in self._items_text:
- if w.row_id == row_id:
- w.setVisible(True)
- break
- else:
- w_text = ItemWidget(row, self.container_text)
- w_text.on_copy_requested.connect(self.on_copy_requested)
- w_text.on_delete_requested.connect(self.on_delete_requested)
- w_text.on_favorite_toggled.connect(self.on_favorite_toggled)
- self.flow_text.addWidget(w_text)
- self._items_text.append(w_text)
- w_text.setVisible(True)
-
- # IMAGE sekmesi
- if item_type == int(ClipItemType.IMAGE):
- if row_id in existing_ids_image:
- for w in self._items_image:
- if w.row_id == row_id:
- w.setVisible(True)
- break
- else:
- w_image = self._wire_clip_widget(ItemWidget(row, self.container_image))
- self.flow_image.addWidget(w_image)
- self._items_image.append(w_image)
- w_image.setVisible(True)
-
- # FILES sekmesi
- if item_type == int(ClipItemType.FILE):
- existing_ids_files = {w.row_id for w in self._items_files}
- if row_id in existing_ids_files:
- for w in self._items_files:
- if w.row_id == row_id:
- w.setVisible(True)
- break
- else:
- w_file = self._wire_clip_widget(ItemWidget(row, self.container_files))
- self.flow_files.addWidget(w_file)
- self._items_files.append(w_file)
- w_file.setVisible(True)
-
- # FAVORITES sekmesi
- if is_favorite or bool(row.get("pinned", False)):
- if row_id in existing_ids_fav:
- for w in self._items_fav:
- if w.row_id == row_id:
- w.setVisible(True)
- break
- else:
- w_fav = self._wire_clip_widget(ItemWidget(row, self.container_fav))
- self.flow_fav.addWidget(w_fav)
- self._items_fav.append(w_fav)
- w_fav.setVisible(True)
-
+ add_result("text", row)
+ elif item_type == int(ClipItemType.IMAGE):
+ add_result("image", row)
+ elif item_type == int(ClipItemType.FILE):
+ add_result("files", row)
+ if bool(row.get("favorite", False)) or bool(row.get("pinned", False)):
+ add_result("fav", row)
+
+ self._apply_view_mode_to_cards()
self._refresh_layouts()
# ---------- End Gelişmiş Arama ----------
@@ -934,19 +1530,25 @@ def _on_tab_changed(self, idx: int):
only_todos = (idx == todos_idx)
only_drawings = (idx == drawings_idx)
- # Buton görünürlükleri
+ # Buton görünürlükleri — silme butonları her zaman gizli (ayarlara taşındı)
self.btn_add_note.setVisible(only_notes)
- self.btn_clear_notes.setVisible(only_notes)
+ self.btn_clear_notes.setVisible(False)
self.btn_add_reminder.setVisible(only_reminders)
- self.btn_clear_reminders.setVisible(only_reminders)
+ self.btn_clear_reminders.setVisible(False)
self.btn_add_snippet.setVisible(only_snippets)
- self.btn_clear_snippets.setVisible(only_snippets)
+ self.btn_clear_snippets.setVisible(False)
self.btn_add_todo.setVisible(only_todos)
- self.btn_clear_todos.setVisible(only_todos)
+ self.btn_clear_todos.setVisible(False)
self.btn_add_drawing.setVisible(only_drawings)
- self.btn_clear_drawings.setVisible(only_drawings)
- self.btn_clear.setVisible(not only_notes and not only_reminders and not only_snippets and not only_todos and not only_drawings)
+ self.btn_clear_drawings.setVisible(False)
+ self.btn_clear.setVisible(False)
+ # Sidebar senkron
+ if 0 <= idx < len(NAV_TAB_ORDER):
+ self._nav_key = NAV_TAB_ORDER[idx]
+ if hasattr(self, "sidebar"):
+ self.sidebar.set_current(self._nav_key)
+
# Snippet sekmesinde yükle
if only_snippets and not self._snippet_cards:
self._load_snippets()
@@ -956,25 +1558,46 @@ def _on_tab_changed(self, idx: int):
QTimer.singleShot(10, self._refresh_drawings_layout)
QTimer.singleShot(100, self._refresh_drawings_layout)
+ # Pagination bar: video/snippet/todo/drawing için gizle
+ show_pag = self._nav_key in ("all", "text", "image", "files", "fav", "notes", "reminders")
+ if hasattr(self, "pagination_bar"):
+ self.pagination_bar.setVisible(show_pag)
+
QTimer.singleShot(0, self._refresh_layouts)
QTimer.singleShot(50, self._refresh_layouts)
def showCentered(self):
+ """Ekranın ortasında aç."""
screen = QApplication.primaryScreen().availableGeometry()
w, h = self.width(), self.height()
- cursor_pos = QCursor.pos()
- x = max(0, min(cursor_pos.x() - w // 2, screen.width() - w))
- y = max(0, min(cursor_pos.y() - 100, screen.height() - h))
+ x = screen.x() + max(0, (screen.width() - w) // 2)
+ y = screen.y() + max(0, (screen.height() - h) // 2)
self.setGeometry(QRect(QPoint(x, y), QSize(w, h)))
self.show()
+ self.raise_()
+ self.activateWindow()
+
+ def prepare_for_show(self):
+ """Refresh the hidden window before it is exposed to the desktop."""
+ if self._first_show:
+ # This also makes the constructor's delayed initial-load callback a
+ # no-op if the tray icon is used during application startup.
+ self._initial_load()
+ else:
+ self.reload_items()
+ self._refresh_sidebar_counts()
+
+ # Do the final geometry pass synchronously while the window is still
+ # hidden. Otherwise newly-created cards briefly paint at (0, 0).
+ self._refresh_layouts()
def showEvent(self, e):
if self._toast:
self._toast.dismiss()
-
- # Layout'u yenile (kartlar zaten constructor'da yüklendi)
+
+ # Data is refreshed by prepare_for_show() before the native window is
+ # shown. showEvent must not rebuild the cards a second time.
QTimer.singleShot(0, self._refresh_layouts)
- QTimer.singleShot(50, self._refresh_layouts)
return super().showEvent(e)
def hideEvent(self, e):
@@ -1042,7 +1665,43 @@ def _reflow_now(self, which: str):
print(f"[DEBUG] _reflow_now: {len(self._reminder_cards) if hasattr(self, '_reminder_cards') else 0} hatırlatma kartı güncelleniyor")
def _refresh_layouts(self):
- for which in ("all", "text", "image", "fav", "notes", "reminders"):
+ # Cards are never legitimate top-level windows. Reparenting must also
+ # reset their type; setParent(parent) preserves Qt.Window on Windows.
+ card_parents = (
+ (getattr(self, "_items_all", []), self.container_all),
+ (getattr(self, "_items_text", []), self.container_text),
+ (getattr(self, "_items_image", []), self.container_image),
+ (getattr(self, "_items_files", []), self.container_files),
+ (getattr(self, "_items_fav", []), self.container_fav),
+ )
+ for group, expected_parent in card_parents:
+ for widget in list(group):
+ if widget.parentWidget() is expected_parent and not widget.isWindow():
+ continue
+ was_visible = widget.isVisible()
+ widget.hide()
+ widget.setParent(expected_parent, Qt.WindowType.Widget)
+ if was_visible:
+ widget.show()
+
+ # Parentless skeletons/loaders from an earlier layout pass are not in
+ # the item lists. They must never survive as native windows.
+ for widget in list(QApplication.topLevelWidgets()):
+ if isinstance(widget, ItemWidget):
+ expected_parent = next(
+ (container for group, container in card_parents if widget in group),
+ None,
+ )
+ widget.hide()
+ if expected_parent is not None:
+ widget.setParent(expected_parent, Qt.WindowType.Widget)
+ widget.show()
+ else:
+ widget.deleteLater()
+ elif isinstance(widget, (SkeletonCard, LoaderWidget)):
+ widget.hide()
+ widget.deleteLater()
+ for which in ("all", "text", "image", "files", "fav", "notes", "reminders"):
self._reflow_now(which)
# Çizimler için de yenile
if hasattr(self, 'flow_drawings') and hasattr(self, '_drawing_cards'):
@@ -1056,16 +1715,18 @@ def reload_items(self):
self._offset_all = self._offset_text = self._offset_image = self._offset_files = self._offset_fav = 0
self._no_more_all = self._no_more_text = self._no_more_image = self._no_more_files = self._no_more_fav = False
self._loading_all = self._loading_text = self._loading_image = self._loading_files = self._loading_fav = False
-
- # İlk 9 - clip items
- self._load_page("all", first=True)
- self._load_page("text", first=True)
- self._load_page("image", first=True)
- self._load_page("files", first=True)
- self._load_page("fav", first=True)
-
- # Notlar ve hatırlatmalar ilk açılışta yüklendiler, tekrar yükleme!
-
+ self._page_index = 0
+
+ # Pagination ile mevcut sekmeyi yükle; diğer sekmeleri de hazırla
+ for key in ("all", "text", "image", "files", "fav"):
+ if key == self._nav_key or self._nav_key not in ("all", "text", "image", "files", "fav"):
+ continue
+ # arka planda hafif hazırlık gerekmez; yalnızca aktif sayfa
+ if self._nav_key in ("all", "text", "image", "files", "fav"):
+ self._load_paginated_clips(self._nav_key)
+ else:
+ self._load_paginated_clips("all")
+ self._refresh_sidebar_counts()
def _maybe_load_more(self, which: str):
# Arama aktifken lazy load devre dışı (basitlik)
@@ -1187,7 +1848,7 @@ def _hide_loader(self, which: str):
layout.removeWidget(w)
except Exception:
pass
- w.setParent(None)
+ w.hide()
w.deleteLater()
self._reflow_now(which)
@@ -1294,9 +1955,22 @@ def _load_page(self, which: str, first: bool): # <- 4 BOŞLUK GİRİNTİ (sın
if not rows:
self._no_more_reminders = True
else:
+ seen_ids = {
+ int(getattr(c, "reminder_id", -1) or -1)
+ for c in self._reminder_cards
+ }
+ added = 0
for r in rows:
+ rid = int(row_val(r, "id", -1))
+ if rid >= 0 and rid in seen_ids:
+ continue
self._add_reminder_card(r)
+ if rid >= 0:
+ seen_ids.add(rid)
+ added += 1
self._offset_reminders += len(rows)
+ if added == 0 and rows:
+ self._no_more_reminders = True
finally:
self._hide_loader(which)
if which == "all":
@@ -1347,7 +2021,7 @@ def do_delete(note_id_to_delete: int):
pass
if hasattr(self, "_note_cards") and w in self._note_cards:
self._note_cards.remove(w)
- w.setParent(None)
+ w.hide()
w.deleteLater()
self._reflow_now("notes")
@@ -1449,42 +2123,45 @@ def _add_note_dialog(self):
def _add_row_widget(self, kind: str, row, immediate_layout: bool = False):
if kind == "all":
- w = self._wire_clip_widget(ItemWidget(row, self.container_all))
+ w = self._wire_clip_widget(ItemWidget(row, self.container_all, settings=self.settings))
w.setVisible(False)
self.flow_all.addWidget(w)
self._items_all.append(w)
which = "all"
elif kind == "text":
- w = self._wire_clip_widget(ItemWidget(row, self.container_text))
+ w = self._wire_clip_widget(ItemWidget(row, self.container_text, settings=self.settings))
w.setVisible(False)
self.flow_text.addWidget(w)
self._items_text.append(w)
which = "text"
elif kind == "image":
- w = self._wire_clip_widget(ItemWidget(row, self.container_image))
+ w = self._wire_clip_widget(ItemWidget(row, self.container_image, settings=self.settings))
w.setVisible(False)
self.flow_image.addWidget(w)
self._items_image.append(w)
which = "image"
elif kind == "files":
- w = self._wire_clip_widget(ItemWidget(row, self.container_files))
+ w = self._wire_clip_widget(ItemWidget(row, self.container_files, settings=self.settings))
w.setVisible(False)
self.flow_files.addWidget(w)
self._items_files.append(w)
which = "files"
else: # fav
- w = self._wire_clip_widget(ItemWidget(row, self.container_fav))
+ w = self._wire_clip_widget(ItemWidget(row, self.container_fav, settings=self.settings))
w.setVisible(False)
self.flow_fav.addWidget(w)
self._items_fav.append(w)
which = "fav"
- if immediate_layout:
- self._reflow_now(which)
-
q = (self.search.text() or "").lower().strip()
w.setVisible(self._match_row_text(w, q))
- QTimer.singleShot(0, lambda: self._reflow_now(which))
+ if immediate_layout:
+ # FlowLayout ignores hidden widgets. The card must become visible
+ # before the final synchronous pass, or the ninth card remains at
+ # QWidget's default origin until the next event-loop tick.
+ self._reflow_now(which)
+ else:
+ QTimer.singleShot(0, lambda: self._reflow_now(which))
def _clear_flows(self):
for w in self._items_all:
@@ -1492,7 +2169,7 @@ def _clear_flows(self):
self.flow_all.removeWidget(w)
except Exception:
pass
- w.setParent(None)
+ w.hide()
w.deleteLater()
self._items_all.clear()
@@ -1501,7 +2178,7 @@ def _clear_flows(self):
self.flow_text.removeWidget(w)
except Exception:
pass
- w.setParent(None)
+ w.hide()
w.deleteLater()
self._items_text.clear()
@@ -1510,7 +2187,7 @@ def _clear_flows(self):
self.flow_image.removeWidget(w)
except Exception:
pass
- w.setParent(None)
+ w.hide()
w.deleteLater()
self._items_image.clear()
@@ -1519,7 +2196,7 @@ def _clear_flows(self):
self.flow_files.removeWidget(w)
except Exception:
pass
- w.setParent(None)
+ w.hide()
w.deleteLater()
self._items_files.clear()
@@ -1528,7 +2205,7 @@ def _clear_flows(self):
self.flow_fav.removeWidget(w)
except Exception:
pass
- w.setParent(None)
+ w.hide()
w.deleteLater()
self._items_fav.clear()
@@ -1599,38 +2276,81 @@ def apply_filter(self, text: str):
self._refresh_layouts()
- def _search_in_database(self, query: str):
- """Veritabanında arama yap ve sonuçları yükle"""
+ def _search_in_database(
+ self,
+ query: str,
+ *,
+ item_types=None,
+ date_from=None,
+ date_to=None,
+ fuzzy_threshold=None,
+ ):
+ """Queue a non-blocking database search; keep only the newest query."""
+ self._search_generation += 1
+ token = self._search_generation
self._search_loading_widget.setVisible(True)
self._no_results_widget.setVisible(False)
- QApplication.processEvents() # UI'ı güncelle
-
- try:
- # Veritabanında ara (fuzzy search ile)
- threshold = self._default_search_threshold(query)
- results = self.storage.search_items(
- query=query,
- fuzzy_threshold=threshold,
- limit=100
- )
-
- self._search_loading_widget.setVisible(False)
-
- if not results:
- # Sonuç bulunamadı
- self._no_results_widget.setVisible(True)
- self._no_results_label.setText(f"'{query}' için sonuç bulunamadı")
- return
-
- # Sonuçları göster
- self._display_search_results(results)
- self._no_results_widget.setVisible(False)
-
- except Exception as e:
- print(f"[ERROR] Veritabanı araması hatası: {e}")
- self._search_loading_widget.setVisible(False)
+
+ parameters = {
+ "query": query,
+ "item_types": item_types,
+ "date_from": date_from,
+ "date_to": date_to,
+ "fuzzy_threshold": (
+ self._default_search_threshold(query)
+ if fuzzy_threshold is None else fuzzy_threshold
+ ),
+ "limit": ITEMS_PER_PAGE,
+ }
+ request = (token, query, parameters)
+ if self._search_thread is not None and self._search_thread.isRunning():
+ self._queued_search = request
+ return
+ self._start_search_worker(request)
+
+ def _start_search_worker(self, request):
+ token, _query, parameters = request
+ worker = SearchWorker(self.storage, token, parameters, self)
+ self._search_thread = worker
+ worker.completed.connect(self._on_search_results)
+ worker.failed.connect(self._on_search_failed)
+ worker.finished.connect(lambda w=worker: self._on_search_worker_finished(w))
+ worker.start()
+
+ def _on_search_results(self, token: int, results):
+ if token != self._search_generation:
+ return
+ query = (self.search.text() or "").strip()
+ self._search_loading_widget.setVisible(False)
+ if not results:
+ for which in ("all", "text", "image", "files", "fav"):
+ self._clear_clip_flow(which)
self._no_results_widget.setVisible(True)
- self._no_results_label.setText("Arama sırasında bir hata oluştu")
+ self._no_results_label.setText(f"'{query}' için sonuç bulunamadı")
+ self.scroll_all.setVisible(False)
+ self._refresh_layouts()
+ return
+
+ self.scroll_all.setVisible(True)
+ self._display_search_results(results)
+ self._no_results_widget.setVisible(False)
+
+ def _on_search_failed(self, token: int, message: str):
+ if token != self._search_generation:
+ return
+ print(f"[ERROR] Veritabanı araması hatası: {message}")
+ self._search_loading_widget.setVisible(False)
+ self._no_results_widget.setVisible(True)
+ self._no_results_label.setText("Arama sırasında bir hata oluştu")
+
+ def _on_search_worker_finished(self, worker: SearchWorker):
+ if self._search_thread is worker:
+ self._search_thread = None
+ worker.deleteLater()
+ request = self._queued_search
+ self._queued_search = None
+ if request is not None and request[0] == self._search_generation:
+ self._start_search_worker(request)
# ------------------ Anlık olaylar ------------------
@@ -1741,7 +2461,14 @@ def on_item_meta_changed(self, row_id: int):
self.reload_items()
def on_item_added(self, row):
+ # Pencere kapalıyken de kartı ekle; showEvent'te reload gelecek ama
+ # anlık güncelleme için pencere açıksa hemen göster
if not self.isVisible():
+ # Sidebar sayısını güncelle (arka planda bile)
+ try:
+ self._refresh_sidebar_counts()
+ except Exception:
+ pass
return
# Tümü sekmesine ekle
@@ -1793,6 +2520,9 @@ def on_item_added(self, row):
if bool(row_val(row, "favorite", False)) or bool(row_val(row, "pinned", False)):
self._add_to_favorites_ui(row)
+ # Sayfa başına 9 sınırı
+ for key in ("all", "text", "image", "files", "fav"):
+ self._enforce_page_cap(key)
QTimer.singleShot(0, lambda: self._reflow_now("all"))
def on_copy_requested(self, row_id: int, data_kind: ClipItemType, payload):
@@ -1944,7 +2674,7 @@ def _remove_from_favorites_ui(self, row_id: int):
self.flow_fav.removeWidget(w)
except Exception:
pass
- w.setParent(None)
+ w.hide()
w.deleteLater()
self._reflow_now("fav")
@@ -1956,7 +2686,7 @@ def _remove_item_from_ui(self, row_id: int):
self.flow_all.removeWidget(w)
except Exception:
pass
- w.setParent(None)
+ w.hide()
w.deleteLater()
self._remove_from_favorites_ui(row_id)
self._reflow_now("all")
@@ -1993,7 +2723,7 @@ def _clear_all_notes(self):
self.flow_notes.removeWidget(w)
except Exception:
pass
- w.setParent(None)
+ w.hide()
w.deleteLater()
self._note_cards.clear()
self._offset_notes = 0
@@ -2067,41 +2797,32 @@ def _add_reminder_dialog(self):
def _add_reminder_card(self, row):
"""Hatırlatma kartı ekle - VBoxLayout için"""
+ if not hasattr(self, "_reminder_cards"):
+ self._reminder_cards = []
+
+ rid = int(row_val(row, "id", -1))
+ if rid >= 0 and any(int(getattr(c, "reminder_id", -1) or -1) == rid for c in self._reminder_cards):
+ return # tekrar ekleme
+
w = ReminderWidget(row, self.container_reminders)
-
- # Sinyaller
+ w.reminder_id = rid if rid >= 0 else int(getattr(w, "reminder_id", -1) or -1)
+
w.on_edit_requested.connect(self._edit_reminder)
w.on_delete_requested.connect(self._delete_reminder)
w.on_toggle_requested.connect(self._toggle_reminder)
-
- # Liste başlat
- if not hasattr(self, "_reminder_cards"):
- self._reminder_cards = []
-
- # VBoxLayout'a ekle - Stretch'den ÖNCE (son eleman stretch)
- # Layout'taki eleman sayısını kontrol et
+
layout_count = self.flow_reminders.count()
-
- # Son eleman stretch ise ondan önce ekle, değilse sona ekle
if layout_count > 0:
- # Stretch genellikle son elemandır, ondan önce ekle
insert_at = max(0, layout_count - 1)
self.flow_reminders.insertWidget(insert_at, w)
else:
- # Layout boşsa direkt ekle
self.flow_reminders.addWidget(w)
-
- # Listeye ekle
+
self._reminder_cards.append(w)
-
- # Widget'ı aktif et
w.setAttribute(Qt.WA_StyledBackground, True)
w.setVisible(True)
w.show()
-
- print(f"[DEBUG] Hatırlatma kartı eklendi: ID={row.get('id')}, is_active={row.get('is_active')}, layout_index={self.flow_reminders.indexOf(w)}")
-
- # Görünürlük filtreleme
+
q = (self.search.text() or "").lower().strip()
if q:
try:
@@ -2109,8 +2830,8 @@ def _add_reminder_card(self, row):
full_text = " ".join([(l.text() or "") for l in lbls]).lower() if lbls else ""
if q not in full_text:
w.setVisible(False)
- except Exception as e:
- print(f"[DEBUG] Filtre hatası: {e}")
+ except Exception:
+ pass
def _edit_reminder(self, reminder_id: int):
"""Hatırlatmayı düzenle"""
@@ -2143,7 +2864,7 @@ def _edit_reminder(self, reminder_id: int):
layout_idx = self.flow_reminders.indexOf(w)
self.flow_reminders.removeWidget(w)
- w.setParent(None)
+ w.hide()
w.deleteLater()
self._reminder_cards.remove(w)
@@ -2193,7 +2914,7 @@ def _delete_reminder(self, reminder_id: int):
pass
if w in self._reminder_cards:
self._reminder_cards.remove(w)
- w.setParent(None)
+ w.hide()
w.deleteLater()
self._reflow_now("reminders")
@@ -2233,7 +2954,7 @@ def _clear_all_reminders(self):
self.flow_reminders.removeWidget(w)
except Exception:
pass
- w.setParent(None)
+ w.hide()
w.deleteLater()
self._reminder_cards.clear()
self._offset_reminders = 0
@@ -2277,6 +2998,7 @@ def on_reminder_time_updated(self, reminder: dict):
def _load_snippets(self):
"""Snippet'leri yükle"""
try:
+ self._clear_snippet_cards()
snippets = self.storage.list_snippets()
for snippet in snippets:
self._add_snippet_card(snippet)
@@ -2330,7 +3052,7 @@ def _delete_snippet(self, snippet_id: int):
if w:
self.flow_snippets.removeWidget(w)
self._snippet_cards.remove(w)
- w.setParent(None)
+ w.hide()
w.deleteLater()
def _toggle_snippet_favorite(self, snippet_id: int):
@@ -2439,7 +3161,7 @@ def _clear_all_snippets(self):
# UI'ı temizle
for w in list(self._snippet_cards):
self.flow_snippets.removeWidget(w)
- w.setParent(None)
+ w.hide()
w.deleteLater()
self._snippet_cards.clear()
@@ -2478,7 +3200,7 @@ def _reload_todo_cards(self):
self.flow_todos.removeWidget(card)
except Exception:
pass
- card.setParent(None)
+ card.hide()
card.deleteLater()
self._todo_cards.clear()
self._load_todo_lists()
@@ -2766,7 +3488,7 @@ def _clear_all_drawings(self):
for w in self._drawing_cards:
self.flow_drawings.removeWidget(w)
- w.setParent(None)
+ w.hide()
w.deleteLater()
self._drawing_cards.clear()
@@ -2791,6 +3513,6 @@ def _delete_drawing(self, drawing_id: int):
if w:
self.flow_drawings.removeWidget(w)
self._drawing_cards.remove(w)
- w.setParent(None)
+ w.hide()
w.deleteLater()
self._toast.show_message("🗑️ Çizim silindi")
diff --git a/clipstack/ui/note_widget.py b/clipstack/ui/note_widget.py
index 0269682..8b65399 100644
--- a/clipstack/ui/note_widget.py
+++ b/clipstack/ui/note_widget.py
@@ -1,211 +1,153 @@
-from __future__ import annotations
-
-from PySide6.QtCore import Qt, QSize, Signal
-from PySide6.QtWidgets import (
- QWidget,
- QVBoxLayout,
- QLabel,
- QHBoxLayout,
- QToolButton,
- QDialog,
- QTextEdit,
- QPushButton,
- QInputDialog,
-)
-
-from ..utils import resource_path, svg_icon
-from ..i18n import i18n
-
-
-class NoteWidget(QWidget):
- on_copy_requested = Signal(int, object, object) # (note_id, item_type_unused, payload=str)
- on_delete_requested = Signal(int) # (note_id,)
- on_edit_requested = Signal(int, str) # (note_id, new_content)
-
- CARD_W = 260
- CARD_H = 160
-
- def __init__(self, row, parent=None):
- super().__init__(parent)
- self.row = row
- self.note_id = int(self._row("id", -1))
- self.content: str = str(self._row("content", ""))
- self.created_at: str = str(self._row("created_at", ""))
-
- # ItemWidget ile aynı kart stili
- self.setObjectName("ItemCard")
- self.setFixedSize(self.CARD_W, self.CARD_H)
- self.setAutoFillBackground(True)
-
- self.v = QVBoxLayout(self)
- self.v.setContentsMargins(10, 10, 10, 10)
- self.v.setSpacing(6)
-
- # Önizleme metni
- self.preview = QLabel()
- self.preview.setAlignment(Qt.AlignLeft | Qt.AlignTop)
- self.preview.setWordWrap(True)
- self.preview.setTextFormat(Qt.PlainText)
- self.preview.setText(self._shorten(self.content, 300))
- self.v.addWidget(self.preview, 1)
-
- # Alt bar: tarih
- self.bottom = QHBoxLayout()
- self.bottom.setContentsMargins(0, 0, 0, 0)
- self.bottom.setSpacing(6)
- self.lbl_meta = QLabel(self.created_at)
- self.lbl_meta.setObjectName("MetaLabel")
- self.bottom.addWidget(self.lbl_meta, 1)
- self.v.addLayout(self.bottom)
-
- # Hover highlight
- self.hover_overlay = QWidget(self)
- self.hover_overlay.setObjectName("HoverHighlight")
- self.hover_overlay.setAttribute(Qt.WA_TransparentForMouseEvents, True)
- self.hover_overlay.setAttribute(Qt.WA_StyledBackground, True)
- self.hover_overlay.setStyleSheet("background-color: rgba(0,0,0,0.10); border-radius: 12px;")
- self.hover_overlay.hide()
-
- # Hover toolbar: copy, expand, edit, delete
- self.toolbar = QWidget(self)
- self.toolbar.setObjectName("HoverToolbar")
- self.toolbar.setAttribute(Qt.WA_StyledBackground, True)
- self.toolbar.setStyleSheet("background-color: rgba(0,0,0,0.22); border-top-left-radius: 12px; border-top-right-radius: 12px; color: white;")
- self.toolbar_layout = QHBoxLayout(self.toolbar)
- self.toolbar_layout.setContentsMargins(6, 4, 6, 4)
- self.toolbar_layout.setSpacing(6)
-
- self.btn_copy = QToolButton()
- try:
- self.btn_copy.setIcon(svg_icon("assets/icons/copy.svg"))
- except Exception:
- pass
- self.btn_copy.setToolTip(self._tr("item.tooltip.copy", "Copy to clipboard"))
- self.btn_copy.setAutoRaise(True)
- self.btn_copy.clicked.connect(self._copy)
- self.toolbar_layout.addWidget(self.btn_copy)
-
- self.btn_expand = QToolButton()
- try:
- self.btn_expand.setIcon(svg_icon("assets/icons/expand.svg"))
- except Exception:
- pass
- self.btn_expand.setToolTip(self._tr("item.tooltip.expand", "Expand"))
- self.btn_expand.setAutoRaise(True)
- self.btn_expand.clicked.connect(self._expand)
- self.toolbar_layout.addWidget(self.btn_expand)
-
- self.btn_edit = QToolButton()
- try:
- # Beyaz ikon
- self.btn_edit.setIcon(svg_icon("assets/icons/edit.svg"))
- except Exception:
- pass
- self.btn_edit.setToolTip(self._tr("notes.edit", "Edit"))
- self.btn_edit.setAutoRaise(True)
- self.btn_edit.clicked.connect(self._edit)
- self.toolbar_layout.addWidget(self.btn_edit)
-
- self.btn_delete = QToolButton()
- try:
- self.btn_delete.setIcon(svg_icon("assets/icons/delete.svg"))
- except Exception:
- pass
- self.btn_delete.setToolTip(self._tr("item.tooltip.delete", "Delete"))
- self.btn_delete.setAutoRaise(True)
- self.btn_delete.clicked.connect(self._delete)
- self.toolbar_layout.addWidget(self.btn_delete)
-
- self.toolbar.hide()
- self._sync_overlays()
-
- def _tr(self, key: str, fallback: str) -> str:
- try:
- v = i18n.t(key)
- except Exception:
- v = ""
- return v if v and v != key else fallback
-
- def _row(self, key: str, default=None):
- try:
- return self.row[key]
- except Exception:
- try:
- return self.row.get(key, default)
- except Exception:
- return default
-
- def _shorten(self, text: str, limit: int) -> str:
- return text if len(text) <= limit else text[: limit - 1] + "…"
-
- def sizeHint(self) -> QSize:
- return QSize(self.CARD_W, self.CARD_H)
-
- def minimumSizeHint(self) -> QSize:
- return self.sizeHint()
-
- def resizeEvent(self, e):
- self._sync_overlays()
- return super().resizeEvent(e)
-
- def _sync_overlays(self):
- self.hover_overlay.setGeometry(0, 0, self.width(), self.height())
- self.toolbar.setGeometry(0, 0, self.width(), 32)
- self.hover_overlay.lower()
- self.toolbar.raise_()
-
- def enterEvent(self, event):
- self.hover_overlay.show()
- self.toolbar.show()
- super().enterEvent(event)
-
- def leaveEvent(self, event):
- self.toolbar.hide()
- self.hover_overlay.hide()
- super().leaveEvent(event)
-
- def mousePressEvent(self, event):
- if event.button() == Qt.LeftButton:
- self._copy()
- super().mousePressEvent(event)
-
- # Aksiyonlar
- def _copy(self):
- self.on_copy_requested.emit(self.note_id, None, self.content)
-
- def _delete(self):
- self.on_delete_requested.emit(self.note_id)
-
- def _expand(self):
- dlg = QDialog(self)
- dlg.setWindowTitle(self._tr("notes.dialog.title", "Note"))
- v = QVBoxLayout(dlg)
- te = QTextEdit()
- te.setReadOnly(True)
- te.setPlainText(self.content)
- v.addWidget(te, 1)
- btn_close = QPushButton(self._tr("common.close", "Close"))
- btn_close.clicked.connect(dlg.accept)
- v.addWidget(btn_close, alignment=Qt.AlignRight)
- dlg.resize(560, 420)
- dlg.exec()
-
- def _edit(self):
- # Çok satırlı düzenleme – mevcut içerikle aç
- new_text, ok = QInputDialog.getMultiLineText(
- self,
- self._tr("notes.edit.title", "Edit Note"),
- self._tr("notes.edit.prompt", "Content:"),
- self.content,
- )
- if not ok:
- return
- new_text = (new_text or "").strip()
- if new_text == self.content:
- return
- self.on_edit_requested.emit(self.note_id, new_text)
-
- # UI içeriğini dışarıdan güncellemek için
- def set_content(self, new_text: str):
- self.content = new_text or ""
- self.preview.setText(self._shorten(self.content, 300))
\ No newline at end of file
+from __future__ import annotations
+
+from PySide6.QtCore import Qt, Signal
+from PySide6.QtWidgets import (
+ QWidget, QVBoxLayout, QLabel, QHBoxLayout, QToolButton,
+ QDialog, QTextEdit, QPushButton, QInputDialog,
+)
+
+from .card_common import (
+ CARD_W, CARD_H, LIST_H, format_card_date, make_type_badge, make_tool_button, highlight_code,
+ apply_card_view_mode,
+)
+from ..utils import svg_icon
+from ..i18n import i18n
+
+
+class NoteWidget(QWidget):
+ on_copy_requested = Signal(int, object, object)
+ on_delete_requested = Signal(int)
+ on_edit_requested = Signal(int, str)
+
+ CARD_W = CARD_W
+ CARD_H = CARD_H
+ LIST_H = LIST_H
+
+ def __init__(self, row, parent=None):
+ super().__init__(parent)
+ self.row = row
+ self.note_id = int(self._row("id", -1))
+ self.content: str = str(self._row("content", ""))
+ self.created_at: str = str(self._row("created_at", ""))
+ self._view_mode = "grid"
+
+ self.setObjectName("ItemCard")
+ self.setAttribute(Qt.WA_StyledBackground, True)
+ self.setFixedSize(self.CARD_W, self.CARD_H)
+ self.setCursor(Qt.PointingHandCursor)
+ self.setAutoFillBackground(False)
+
+ self.v = QVBoxLayout(self)
+ self.v.setContentsMargins(12, 12, 12, 10)
+ self.v.setSpacing(8)
+
+ top = QHBoxLayout()
+ top.setSpacing(6)
+ top.addWidget(make_type_badge("assets/icons/nav_note.svg", "N"))
+
+ self.lbl_title = QLabel("Not")
+ self.lbl_title.setObjectName("ItemTitle")
+ self.lbl_title.setStyleSheet("font-weight: 600; font-size: 11px; background: transparent; border: none;")
+ top.addWidget(self.lbl_title, 1)
+
+ self.btn_more = make_tool_button("MoreButton", "assets/icons/more_vert.svg", "Diğer", "⋮")
+ self.btn_more.clicked.connect(self._expand)
+ top.addWidget(self.btn_more)
+ self.v.addLayout(top)
+
+ self.preview = QLabel()
+ self.preview.setObjectName("ItemPreview")
+ self.preview.setAlignment(Qt.AlignLeft | Qt.AlignTop)
+ self.preview.setWordWrap(True)
+ self.preview.setTextFormat(Qt.RichText if self._looks_like_code(self.content) else Qt.PlainText)
+ self.preview.setStyleSheet("background: transparent; border: none;")
+ if self._looks_like_code(self.content):
+ self.preview.setText(highlight_code(self.content))
+ else:
+ self.preview.setText(self._shorten(self.content, 280))
+ self.v.addWidget(self.preview, 1)
+
+ bottom = QHBoxLayout()
+ meta = format_card_date(self.created_at)
+ if self.content:
+ meta = f"{meta} · {len(self.content)} kr" if meta else f"{len(self.content)} kr"
+ self.lbl_meta = QLabel(meta)
+ self.lbl_meta.setObjectName("MetaLabel")
+ self.lbl_meta.setStyleSheet("font-size: 10px; color: #94a3b8; background: transparent; border: none;")
+ bottom.addWidget(self.lbl_meta, 1)
+
+ self.btn_copy = make_tool_button("CopyButton", "assets/icons/copy.svg", "Kopyala")
+ self.btn_copy.clicked.connect(self._copy)
+ bottom.addWidget(self.btn_copy)
+ self.v.addLayout(bottom)
+ try:
+ mode = getattr(getattr(self.window(), "chip_bar", None), "view_mode", "grid")
+ self.set_view_mode(mode)
+ except Exception:
+ pass
+
+ def set_view_mode(self, mode: str):
+ apply_card_view_mode(self, mode, getattr(self, "preview", None))
+ self.updateGeometry()
+
+ def _looks_like_code(self, text: str) -> bool:
+ if not text:
+ return False
+ hints = ("{", "}", "=>", "function", "def ", "import ", "class ", "const ", "let ")
+ return sum(1 for h in hints if h in text) >= 2
+
+ def _row(self, key, default=None):
+ try:
+ return self.row[key]
+ except Exception:
+ try:
+ return self.row.get(key, default)
+ except Exception:
+ return default
+
+ def _shorten(self, text: str, limit: int) -> str:
+ return text if len(text) <= limit else text[: limit - 1] + "…"
+
+ def _copy(self):
+ self.on_copy_requested.emit(self.note_id, None, self.content)
+
+ def _expand(self):
+ dlg = QDialog(self)
+ dlg.setWindowTitle("Not")
+ dlg.resize(560, 420)
+ lay = QVBoxLayout(dlg)
+ edit = QTextEdit()
+ edit.setPlainText(self.content)
+ lay.addWidget(edit, 1)
+ row = QHBoxLayout()
+ btn_save = QPushButton("Kaydet")
+ btn_del = QPushButton("Sil")
+ btn_close = QPushButton("Kapat")
+ row.addStretch(1)
+ row.addWidget(btn_del)
+ row.addWidget(btn_save)
+ row.addWidget(btn_close)
+ lay.addLayout(row)
+
+ def save():
+ self.content = edit.toPlainText()
+ self.on_edit_requested.emit(self.note_id, self.content)
+ if self._looks_like_code(self.content):
+ self.preview.setTextFormat(Qt.RichText)
+ self.preview.setText(highlight_code(self.content))
+ else:
+ self.preview.setTextFormat(Qt.PlainText)
+ self.preview.setText(self._shorten(self.content, 280))
+ dlg.accept()
+
+ btn_save.clicked.connect(save)
+ btn_del.clicked.connect(lambda: (self.on_delete_requested.emit(self.note_id), dlg.reject()))
+ btn_close.clicked.connect(dlg.reject)
+ dlg.exec()
+
+ def mousePressEvent(self, event):
+ if event.button() == Qt.LeftButton:
+ child = self.childAt(event.pos())
+ if not isinstance(child, QToolButton):
+ self._expand()
+ super().mousePressEvent(event)
diff --git a/clipstack/ui/reminder_widget.py b/clipstack/ui/reminder_widget.py
index 4e4540e..77ef671 100644
--- a/clipstack/ui/reminder_widget.py
+++ b/clipstack/ui/reminder_widget.py
@@ -1,268 +1,268 @@
-from __future__ import annotations
-from datetime import datetime
-from PySide6.QtCore import Qt, Signal, QSize, QTimer
-from PySide6.QtGui import QFont
-from PySide6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QFrame, QSizePolicy
-)
-from ..ui.widgets.toggle_switch import ToggleSwitch
-from ..utils import resource_path, svg_icon
-from ..i18n import i18n
-
-
-class ReminderWidget(QWidget):
- """Hatırlatma kartı widget'ı - Modern uzunlamasına tasarım"""
-
- on_edit_requested = Signal(int)
- on_delete_requested = Signal(int)
- on_toggle_requested = Signal(int, bool)
-
- def __init__(self, reminder: dict, parent=None):
- super().__init__(parent)
- self.reminder = reminder
- self.reminder_id = reminder["id"]
-
- self.setObjectName("ReminderCard")
- self.setAttribute(Qt.WA_StyledBackground, True)
- # Uzunlamasına kart - tam genişlik, kompakt yükseklik
- self.setMinimumHeight(68)
- self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
-
- # Ana layout - Horizontal (sol taraf bilgi, sağ taraf butonlar)
- main_layout = QHBoxLayout(self)
- main_layout.setContentsMargins(16, 12, 16, 12)
- main_layout.setSpacing(16)
-
- # SOL TARAF: Bilgiler
- left_layout = QVBoxLayout()
- left_layout.setSpacing(6)
-
- # Başlık ve zaman aynı satırda
- title_time_layout = QHBoxLayout()
- title_time_layout.setSpacing(6)
-
- self.lbl_title = QLabel()
- self.lbl_title.setObjectName("ReminderTitle")
- self.lbl_title.setWordWrap(False)
- title_font = QFont()
- title_font.setPointSize(12)
- title_font.setBold(True)
- self.lbl_title.setFont(title_font)
-
- self.lbl_time = QLabel()
- self.lbl_time.setObjectName("ReminderTime")
- time_font = QFont()
- time_font.setPointSize(9)
- self.lbl_time.setFont(time_font)
- self.lbl_time.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
-
- title_time_layout.addWidget(self.lbl_title)
- title_time_layout.addWidget(self.lbl_time)
- title_time_layout.addStretch()
-
- left_layout.addLayout(title_time_layout)
-
- # Açıklama (varsa)
- self.lbl_description = QLabel()
- self.lbl_description.setObjectName("ReminderDescription")
- self.lbl_description.setWordWrap(True)
- self.lbl_description.setContentsMargins(0, 2, 0, 0)
- self.lbl_description.setMaximumHeight(60)
- desc_font = QFont()
- desc_font.setPointSize(9)
- self.lbl_description.setFont(desc_font)
- left_layout.addWidget(self.lbl_description)
-
- # Tekrar bilgisi
- self.lbl_repeat = QLabel()
- self.lbl_repeat.setObjectName("ReminderRepeat")
- repeat_font = QFont()
- repeat_font.setPointSize(9)
- self.lbl_repeat.setFont(repeat_font)
- self.lbl_repeat.setContentsMargins(0, 2, 0, 0)
- left_layout.addWidget(self.lbl_repeat)
-
- left_layout.addStretch()
- main_layout.addLayout(left_layout, 1) # Sol taraf esnek
-
- # SAĞ TARAF: Kontroller
- right_layout = QHBoxLayout()
- right_layout.setSpacing(8)
- right_layout.setAlignment(Qt.AlignVCenter)
- right_layout.setContentsMargins(0, 0, 0, 0)
-
- # Düzenle butonu
- self.btn_edit = QPushButton()
- self.btn_edit.setObjectName("ReminderEditBtn")
- self.btn_edit.setFixedSize(32, 32)
- self.btn_edit.setToolTip(self._tr("reminder.edit", "Düzenle"))
- self.btn_edit.clicked.connect(lambda: self.on_edit_requested.emit(self.reminder_id))
- try:
- icon_path = resource_path("assets/icons/edit_reminder.svg")
- if icon_path.exists():
- self.btn_edit.setIcon(svg_icon(icon_path))
- self.btn_edit.setIconSize(QSize(18, 18))
- else:
- self.btn_edit.setText("✏️")
- edit_font = QFont()
- edit_font.setPointSize(13)
- self.btn_edit.setFont(edit_font)
- except Exception:
- self.btn_edit.setText("✏️")
- edit_font = QFont()
- edit_font.setPointSize(13)
- self.btn_edit.setFont(edit_font)
-
- # Sil butonu
- self.btn_delete = QPushButton()
- self.btn_delete.setObjectName("ReminderDeleteBtn")
- self.btn_delete.setFixedSize(32, 32)
- self.btn_delete.setToolTip(self._tr("reminder.delete", "Sil"))
- self.btn_delete.clicked.connect(lambda: self.on_delete_requested.emit(self.reminder_id))
- try:
- icon_path = resource_path("assets/icons/delete_reminder.svg")
- if icon_path.exists():
- self.btn_delete.setIcon(svg_icon(icon_path))
- self.btn_delete.setIconSize(QSize(18, 18))
- else:
- self.btn_delete.setText("🗑️")
- delete_font = QFont()
- delete_font.setPointSize(13)
- self.btn_delete.setFont(delete_font)
- except Exception:
- self.btn_delete.setText("🗑️")
- delete_font = QFont()
- delete_font.setPointSize(13)
- self.btn_delete.setFont(delete_font)
-
- # Switch EN SAĞDA
- self.switch = ToggleSwitch(checked=bool(reminder.get("is_active", 1)))
- self.switch.onToggled(self._on_switch_toggled)
- self.switch.setToolTip(self._tr("reminder.active", "Aktif/Pasif"))
-
- right_layout.addWidget(self.btn_edit)
- right_layout.addWidget(self.btn_delete)
- right_layout.addWidget(self.switch)
-
- main_layout.addLayout(right_layout)
-
- # İçeriği güncelle
- self._update_content()
-
- # Dil değişimini dinle
- i18n.languageChanged.connect(self._update_content)
-
- # Kalan süreyi canlı güncellemek için timer (30 saniyede bir)
- self._time_update_timer = QTimer(self)
- self._time_update_timer.timeout.connect(self._update_time_only)
- self._time_update_timer.start(30000) # 30 saniye
-
- # Widget'ı görünür yap
- self.setVisible(True)
- self.show()
-
- def _tr(self, key: str, fallback: str) -> str:
- try:
- v = i18n.t(key)
- except Exception:
- v = ""
- return v if v and v != key else fallback
-
- def _update_content(self):
- """Widget içeriğini güncelle"""
- title = self.reminder.get("title", "")
- description = self.reminder.get("description", "")
- reminder_time_str = self.reminder.get("reminder_time", "")
- repeat_type = self.reminder.get("repeat_type", "none")
-
- # Başlık
- self.lbl_title.setText(title if title else self._tr("reminder.no_title", "Başlıksız"))
-
- # Açıklama
- if description and description.strip():
- self.lbl_description.setText(description)
- self.lbl_description.show()
- else:
- self.lbl_description.hide()
-
- # Zaman
- try:
- dt = datetime.fromisoformat(reminder_time_str)
- now = datetime.now()
-
- # Geçmiş mi gelecek mi?
- if dt < now:
- time_str = "⏰ " + dt.strftime("%d.%m.%Y %H:%M")
- self.lbl_time.setStyleSheet("color: #f59e0b;") # Turuncu - geçmiş
- else:
- diff = dt - now
- if diff.days > 0:
- time_str = "⏰ " + dt.strftime("%d.%m.%Y %H:%M")
- else:
- hours = diff.seconds // 3600
- minutes = (diff.seconds % 3600) // 60
- if hours > 0:
- time_str = f"⏰ {hours}s {minutes}d sonra"
- else:
- time_str = f"⏰ {minutes}d sonra"
- self.lbl_time.setStyleSheet("") # Normal renk
-
- self.lbl_time.setText(time_str)
- except Exception:
- self.lbl_time.setText("⏰ " + reminder_time_str)
-
- # Tekrar türü
- repeat_labels = {
- "none": "",
- "daily": self._tr("reminder.repeat.daily", "🔄 Günlük"),
- "weekly": self._tr("reminder.repeat.weekly", "🔄 Haftalık"),
- "monthly": self._tr("reminder.repeat.monthly", "🔄 Aylık")
- }
- repeat_text = repeat_labels.get(repeat_type, "")
-
- if repeat_text:
- self.lbl_repeat.setText(repeat_text)
- self.lbl_repeat.show()
- else:
- self.lbl_repeat.hide()
-
- def _update_time_only(self):
- """Sadece zaman label'ını güncelle (timer ile çağrılır)"""
- try:
- reminder_time_str = self.reminder.get("reminder_time", "")
- dt = datetime.fromisoformat(reminder_time_str)
- now = datetime.now()
-
- if dt < now:
- time_str = "⏰ " + dt.strftime("%d.%m.%Y %H:%M")
- self.lbl_time.setStyleSheet("color: #f59e0b;")
- else:
- diff = dt - now
- if diff.days > 0:
- time_str = "⏰ " + dt.strftime("%d.%m.%Y %H:%M")
- else:
- hours = diff.seconds // 3600
- minutes = (diff.seconds % 3600) // 60
- if hours > 0:
- time_str = f"⏰ {hours}s {minutes}d sonra"
- else:
- time_str = f"⏰ {minutes}d sonra"
- self.lbl_time.setStyleSheet("")
-
- self.lbl_time.setText(time_str)
- except Exception:
- pass
-
- def _on_switch_toggled(self, state: bool):
- """Switch değiştiğinde"""
- self.on_toggle_requested.emit(self.reminder_id, state)
-
- def set_active(self, is_active: bool):
- """Dışarıdan aktif durumu güncelle"""
- old = self.switch.blockSignals(True)
- try:
- self.switch.setChecked(is_active)
- self.reminder["is_active"] = 1 if is_active else 0
- finally:
+from __future__ import annotations
+from datetime import datetime
+from PySide6.QtCore import Qt, Signal, QSize, QTimer
+from PySide6.QtGui import QFont
+from PySide6.QtWidgets import (
+ QWidget, QVBoxLayout, QHBoxLayout, QLabel,
+ QPushButton, QFrame, QSizePolicy
+)
+from ..ui.widgets.toggle_switch import ToggleSwitch
+from ..utils import resource_path, svg_icon
+from ..i18n import i18n
+
+
+class ReminderWidget(QWidget):
+ """Hatırlatma kartı widget'ı - Modern uzunlamasına tasarım"""
+
+ on_edit_requested = Signal(int)
+ on_delete_requested = Signal(int)
+ on_toggle_requested = Signal(int, bool)
+
+ def __init__(self, reminder: dict, parent=None):
+ super().__init__(parent)
+ self.reminder = reminder
+ self.reminder_id = int(reminder.get("id", -1) if isinstance(reminder, dict) else reminder["id"])
+
+ self.setObjectName("ReminderCard")
+ self.setAttribute(Qt.WA_StyledBackground, True)
+ # Uzunlamasına kart - tam genişlik, kompakt yükseklik
+ self.setMinimumHeight(68)
+ self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+
+ # Ana layout - Horizontal (sol taraf bilgi, sağ taraf butonlar)
+ main_layout = QHBoxLayout(self)
+ main_layout.setContentsMargins(16, 12, 16, 12)
+ main_layout.setSpacing(16)
+
+ # SOL TARAF: Bilgiler
+ left_layout = QVBoxLayout()
+ left_layout.setSpacing(6)
+
+ # Başlık ve zaman aynı satırda
+ title_time_layout = QHBoxLayout()
+ title_time_layout.setSpacing(6)
+
+ self.lbl_title = QLabel()
+ self.lbl_title.setObjectName("ReminderTitle")
+ self.lbl_title.setWordWrap(False)
+ title_font = QFont()
+ title_font.setPointSize(12)
+ title_font.setBold(True)
+ self.lbl_title.setFont(title_font)
+
+ self.lbl_time = QLabel()
+ self.lbl_time.setObjectName("ReminderTime")
+ time_font = QFont()
+ time_font.setPointSize(9)
+ self.lbl_time.setFont(time_font)
+ self.lbl_time.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
+
+ title_time_layout.addWidget(self.lbl_title)
+ title_time_layout.addWidget(self.lbl_time)
+ title_time_layout.addStretch()
+
+ left_layout.addLayout(title_time_layout)
+
+ # Açıklama (varsa)
+ self.lbl_description = QLabel()
+ self.lbl_description.setObjectName("ReminderDescription")
+ self.lbl_description.setWordWrap(True)
+ self.lbl_description.setContentsMargins(0, 2, 0, 0)
+ self.lbl_description.setMaximumHeight(60)
+ desc_font = QFont()
+ desc_font.setPointSize(9)
+ self.lbl_description.setFont(desc_font)
+ left_layout.addWidget(self.lbl_description)
+
+ # Tekrar bilgisi
+ self.lbl_repeat = QLabel()
+ self.lbl_repeat.setObjectName("ReminderRepeat")
+ repeat_font = QFont()
+ repeat_font.setPointSize(9)
+ self.lbl_repeat.setFont(repeat_font)
+ self.lbl_repeat.setContentsMargins(0, 2, 0, 0)
+ left_layout.addWidget(self.lbl_repeat)
+
+ left_layout.addStretch()
+ main_layout.addLayout(left_layout, 1) # Sol taraf esnek
+
+ # SAĞ TARAF: Kontroller
+ right_layout = QHBoxLayout()
+ right_layout.setSpacing(8)
+ right_layout.setAlignment(Qt.AlignVCenter)
+ right_layout.setContentsMargins(0, 0, 0, 0)
+
+ # Düzenle butonu
+ self.btn_edit = QPushButton()
+ self.btn_edit.setObjectName("ReminderEditBtn")
+ self.btn_edit.setFixedSize(32, 32)
+ self.btn_edit.setToolTip(self._tr("reminder.edit", "Düzenle"))
+ self.btn_edit.clicked.connect(lambda: self.on_edit_requested.emit(self.reminder_id))
+ try:
+ icon_path = resource_path("assets/icons/edit_reminder.svg")
+ if icon_path.exists():
+ self.btn_edit.setIcon(svg_icon(icon_path))
+ self.btn_edit.setIconSize(QSize(18, 18))
+ else:
+ self.btn_edit.setText("✏️")
+ edit_font = QFont()
+ edit_font.setPointSize(13)
+ self.btn_edit.setFont(edit_font)
+ except Exception:
+ self.btn_edit.setText("✏️")
+ edit_font = QFont()
+ edit_font.setPointSize(13)
+ self.btn_edit.setFont(edit_font)
+
+ # Sil butonu
+ self.btn_delete = QPushButton()
+ self.btn_delete.setObjectName("ReminderDeleteBtn")
+ self.btn_delete.setFixedSize(32, 32)
+ self.btn_delete.setToolTip(self._tr("reminder.delete", "Sil"))
+ self.btn_delete.clicked.connect(lambda: self.on_delete_requested.emit(self.reminder_id))
+ try:
+ icon_path = resource_path("assets/icons/delete_reminder.svg")
+ if icon_path.exists():
+ self.btn_delete.setIcon(svg_icon(icon_path))
+ self.btn_delete.setIconSize(QSize(18, 18))
+ else:
+ self.btn_delete.setText("🗑️")
+ delete_font = QFont()
+ delete_font.setPointSize(13)
+ self.btn_delete.setFont(delete_font)
+ except Exception:
+ self.btn_delete.setText("🗑️")
+ delete_font = QFont()
+ delete_font.setPointSize(13)
+ self.btn_delete.setFont(delete_font)
+
+ # Switch EN SAĞDA
+ self.switch = ToggleSwitch(checked=bool(reminder.get("is_active", 1)))
+ self.switch.onToggled(self._on_switch_toggled)
+ self.switch.setToolTip(self._tr("reminder.active", "Aktif/Pasif"))
+
+ right_layout.addWidget(self.btn_edit)
+ right_layout.addWidget(self.btn_delete)
+ right_layout.addWidget(self.switch)
+
+ main_layout.addLayout(right_layout)
+
+ # İçeriği güncelle
+ self._update_content()
+
+ # Dil değişimini dinle
+ i18n.languageChanged.connect(self._update_content)
+
+ # Kalan süreyi canlı güncellemek için timer (30 saniyede bir)
+ self._time_update_timer = QTimer(self)
+ self._time_update_timer.timeout.connect(self._update_time_only)
+ self._time_update_timer.start(30000) # 30 saniye
+
+ # Widget'ı görünür yap
+ self.setVisible(True)
+ self.show()
+
+ def _tr(self, key: str, fallback: str) -> str:
+ try:
+ v = i18n.t(key)
+ except Exception:
+ v = ""
+ return v if v and v != key else fallback
+
+ def _update_content(self):
+ """Widget içeriğini güncelle"""
+ title = self.reminder.get("title", "")
+ description = self.reminder.get("description", "")
+ reminder_time_str = self.reminder.get("reminder_time", "")
+ repeat_type = self.reminder.get("repeat_type", "none")
+
+ # Başlık
+ self.lbl_title.setText(title if title else self._tr("reminder.no_title", "Başlıksız"))
+
+ # Açıklama
+ if description and description.strip():
+ self.lbl_description.setText(description)
+ self.lbl_description.show()
+ else:
+ self.lbl_description.hide()
+
+ # Zaman
+ try:
+ dt = datetime.fromisoformat(reminder_time_str)
+ now = datetime.now()
+
+ # Geçmiş mi gelecek mi?
+ if dt < now:
+ time_str = "⏰ " + dt.strftime("%d.%m.%Y %H:%M")
+ self.lbl_time.setStyleSheet("color: #f59e0b;") # Turuncu - geçmiş
+ else:
+ diff = dt - now
+ if diff.days > 0:
+ time_str = "⏰ " + dt.strftime("%d.%m.%Y %H:%M")
+ else:
+ hours = diff.seconds // 3600
+ minutes = (diff.seconds % 3600) // 60
+ if hours > 0:
+ time_str = f"⏰ {hours}s {minutes}d sonra"
+ else:
+ time_str = f"⏰ {minutes}d sonra"
+ self.lbl_time.setStyleSheet("") # Normal renk
+
+ self.lbl_time.setText(time_str)
+ except Exception:
+ self.lbl_time.setText("⏰ " + reminder_time_str)
+
+ # Tekrar türü
+ repeat_labels = {
+ "none": "",
+ "daily": self._tr("reminder.repeat.daily", "🔄 Günlük"),
+ "weekly": self._tr("reminder.repeat.weekly", "🔄 Haftalık"),
+ "monthly": self._tr("reminder.repeat.monthly", "🔄 Aylık")
+ }
+ repeat_text = repeat_labels.get(repeat_type, "")
+
+ if repeat_text:
+ self.lbl_repeat.setText(repeat_text)
+ self.lbl_repeat.show()
+ else:
+ self.lbl_repeat.hide()
+
+ def _update_time_only(self):
+ """Sadece zaman label'ını güncelle (timer ile çağrılır)"""
+ try:
+ reminder_time_str = self.reminder.get("reminder_time", "")
+ dt = datetime.fromisoformat(reminder_time_str)
+ now = datetime.now()
+
+ if dt < now:
+ time_str = "⏰ " + dt.strftime("%d.%m.%Y %H:%M")
+ self.lbl_time.setStyleSheet("color: #f59e0b;")
+ else:
+ diff = dt - now
+ if diff.days > 0:
+ time_str = "⏰ " + dt.strftime("%d.%m.%Y %H:%M")
+ else:
+ hours = diff.seconds // 3600
+ minutes = (diff.seconds % 3600) // 60
+ if hours > 0:
+ time_str = f"⏰ {hours}s {minutes}d sonra"
+ else:
+ time_str = f"⏰ {minutes}d sonra"
+ self.lbl_time.setStyleSheet("")
+
+ self.lbl_time.setText(time_str)
+ except Exception:
+ pass
+
+ def _on_switch_toggled(self, state: bool):
+ """Switch değiştiğinde"""
+ self.on_toggle_requested.emit(self.reminder_id, state)
+
+ def set_active(self, is_active: bool):
+ """Dışarıdan aktif durumu güncelle"""
+ old = self.switch.blockSignals(True)
+ try:
+ self.switch.setChecked(is_active)
+ self.reminder["is_active"] = 1 if is_active else 0
+ finally:
self.switch.blockSignals(old)
\ No newline at end of file
diff --git a/clipstack/ui/search_box.py b/clipstack/ui/search_box.py
new file mode 100644
index 0000000..bdef4a5
--- /dev/null
+++ b/clipstack/ui/search_box.py
@@ -0,0 +1,74 @@
+"""Üst arama kutusu — solda ikon, sağda Ctrl+K rozeti."""
+from __future__ import annotations
+
+from PySide6.QtCore import Qt, Signal, QSize
+from PySide6.QtWidgets import (
+ QWidget, QHBoxLayout, QLineEdit, QLabel, QFrame,
+)
+from ..utils import svg_icon
+
+
+class SearchBox(QFrame):
+ textChanged = Signal(str)
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setObjectName("SearchBox")
+ self.setAttribute(Qt.WA_StyledBackground, True)
+ self.setMinimumHeight(36)
+
+ lay = QHBoxLayout(self)
+ lay.setContentsMargins(10, 4, 8, 4)
+ lay.setSpacing(8)
+
+ self.icon = QLabel()
+ self.icon.setFixedSize(18, 18)
+ try:
+ self.icon.setPixmap(svg_icon("assets/icons/search.svg").pixmap(QSize(16, 16)))
+ except Exception:
+ self.icon.setText("🔍")
+ self.icon.setStyleSheet("background: transparent; border: none;")
+ lay.addWidget(self.icon)
+
+ self.edit = QLineEdit()
+ self.edit.setObjectName("SearchBoxEdit")
+ self.edit.setPlaceholderText("Ara... (Fuzzy search destekli)")
+ self.edit.setFrame(False)
+ self.edit.setStyleSheet(
+ "QLineEdit#SearchBoxEdit {"
+ " background: transparent; border: none; padding: 2px 0;"
+ " color: #e6edf3;"
+ "}"
+ )
+ self.edit.textChanged.connect(self.textChanged.emit)
+ lay.addWidget(self.edit, 1)
+
+ self.shortcut = QLabel("Ctrl K")
+ self.shortcut.setObjectName("SearchShortcutBadge")
+ self.shortcut.setAlignment(Qt.AlignCenter)
+ self.shortcut.setFixedHeight(22)
+ lay.addWidget(self.shortcut)
+
+ def text(self) -> str:
+ return self.edit.text()
+
+ def setText(self, text: str):
+ self.edit.setText(text)
+
+ def clear(self):
+ self.edit.clear()
+
+ def setFocus(self, reason=Qt.OtherFocusReason):
+ self.edit.setFocus(reason)
+
+ def selectAll(self):
+ self.edit.selectAll()
+
+ def setPlaceholderText(self, text: str):
+ self.edit.setPlaceholderText(text)
+
+ def hasFocus(self) -> bool:
+ return self.edit.hasFocus()
+
+ def blockSignals(self, block: bool) -> bool:
+ return self.edit.blockSignals(block)
diff --git a/clipstack/ui/settings_chrome.py b/clipstack/ui/settings_chrome.py
new file mode 100644
index 0000000..d2a3ebc
--- /dev/null
+++ b/clipstack/ui/settings_chrome.py
@@ -0,0 +1,1014 @@
+"""Haulbase-style chrome widgets and stylesheet for SettingsDialog."""
+
+from __future__ import annotations
+
+import re
+
+from PySide6.QtCore import Qt, Signal, QSize, QRectF, QPointF
+from PySide6.QtGui import QColor, QIcon, QPainter, QPainterPath, QPen, QPixmap
+from PySide6.QtWidgets import (
+ QAbstractButton,
+ QComboBox,
+ QFormLayout,
+ QPushButton,
+ QLabel,
+ QLineEdit,
+ QSpinBox,
+ QWidget,
+ QVBoxLayout,
+ QHBoxLayout,
+ QFrame,
+ QSizePolicy,
+)
+
+
+SETTINGS_STYLE = """
+QDialog#SettingsDialog {
+ background: transparent;
+ color: #F5F5F5;
+}
+
+QFrame#SettingsSurface {
+ background: #111115;
+ border: 1px solid #2B2D36;
+ border-radius: 14px;
+}
+
+QWidget#SettingsHeader,
+QWidget#SettingsFooter,
+QWidget#SettingsContent {
+ background: transparent;
+}
+
+QLabel#SettingsTitle {
+ color: #FFFFFF;
+ font-size: 18px;
+ font-weight: 700;
+ background: transparent;
+}
+
+QLabel#SettingsSubtitle {
+ color: #8A8A8A;
+ font-size: 12px;
+ background: transparent;
+}
+
+QLabel#PageTitle {
+ color: #FFFFFF;
+ font-size: 16px;
+ font-weight: 700;
+ background: transparent;
+}
+
+QLabel#PageSubtitle {
+ color: #8A8A8A;
+ font-size: 12px;
+ background: transparent;
+ padding-bottom: 8px;
+}
+
+QLabel#SidebarFooterHint {
+ color: #777984;
+ font-size: 11px;
+ background: transparent;
+}
+
+QLabel#SettingRowTitle {
+ color: #FFFFFF;
+ font-size: 13px;
+ font-weight: 600;
+ background: transparent;
+}
+
+QLabel#SettingRowDesc {
+ color: #8A8A8A;
+ font-size: 12px;
+ background: transparent;
+}
+
+QLabel#SettingsSectionTitle {
+ color: #FFFFFF;
+ font-size: 14px;
+ font-weight: 700;
+ background: transparent;
+ padding-top: 16px;
+ padding-bottom: 5px;
+}
+
+QLabel#SettingsInfoText {
+ color: #8A8A8A;
+ font-size: 11px;
+ background: transparent;
+ padding-top: 5px;
+ padding-bottom: 8px;
+}
+
+QFrame#AboutHeroCard {
+ background: #1E1E1E;
+ border: 1px solid #333333;
+ border-radius: 12px;
+}
+
+QLabel#AboutAppIcon {
+ background: #2A2A2A;
+ border: 1px solid #4A4A4A;
+ border-radius: 12px;
+}
+
+QLabel#AboutKicker {
+ color: #F2A65A;
+ font-size: 10px;
+ font-weight: 700;
+ background: transparent;
+}
+
+QLabel#AboutProductName {
+ color: #FFFFFF;
+ font-size: 22px;
+ font-weight: 750;
+ background: transparent;
+}
+
+QLabel#AboutProductDesc {
+ color: #8A8A8A;
+ font-size: 12px;
+ background: transparent;
+}
+
+QLabel#AboutVersionBadge {
+ color: #F0F0F0;
+ background: #2A2A2A;
+ border: 1px solid #4A4A4A;
+ border-radius: 10px;
+ padding: 5px 10px;
+ font-size: 11px;
+ font-weight: 700;
+}
+
+QFrame#AboutFeatureCard {
+ background: #181818;
+ border: 1px solid #333333;
+ border-radius: 10px;
+ min-height: 112px;
+}
+
+QLabel#AboutFeatureIcon {
+ background: #2A2A2A;
+ border: none;
+ border-radius: 7px;
+}
+
+QLabel#AboutFeatureTitle,
+QLabel#AboutSectionTitle {
+ color: #FFFFFF;
+ font-size: 12px;
+ font-weight: 650;
+ background: transparent;
+}
+
+QLabel#AboutFeatureDesc,
+QLabel#AboutSectionDesc {
+ color: #8A8A8A;
+ font-size: 11px;
+ background: transparent;
+}
+
+QFrame#AboutUpdateCard,
+QFrame#AboutLinksCard {
+ background: #181818;
+ border: 1px solid #333333;
+ border-radius: 10px;
+}
+
+QPushButton#AboutPrimaryButton {
+ background: #F2A65A;
+ color: #1A1208;
+ border: none;
+ border-radius: 8px;
+ padding: 8px 14px;
+ font-weight: 700;
+}
+
+QPushButton#AboutPrimaryButton:hover {
+ background: #F5B56F;
+}
+
+QPushButton#AboutLinkButton {
+ background: transparent;
+ color: #C7C8CF;
+ border: 1px solid #333333;
+ border-radius: 7px;
+ padding: 6px 11px;
+ min-height: 24px;
+}
+
+QPushButton#AboutLinkButton:hover {
+ color: #FFFFFF;
+ background: #2A2A2A;
+ border-color: #4A4A4A;
+}
+
+QFrame#SidebarDivider {
+ background: #292B33;
+ max-width: 1px;
+ min-width: 1px;
+ border: none;
+}
+
+QFrame#FooterDivider {
+ background: #292B33;
+ max-height: 1px;
+ min-height: 1px;
+ border: none;
+}
+
+QFrame#SettingRowDivider {
+ background: #292B33;
+ max-height: 1px;
+ min-height: 1px;
+ border: none;
+}
+
+QWidget#SettingsSidebar {
+ background: transparent;
+ border: none;
+}
+
+QScrollArea {
+ background: transparent;
+ border: none;
+}
+
+QWidget#SettingsPage {
+ background: #111115;
+}
+
+QComboBox {
+ background: #1E1E1E;
+ color: #F0F0F0;
+ border: 1px solid #333333;
+ border-radius: 8px;
+ padding: 8px 12px;
+ min-height: 20px;
+ min-width: 140px;
+}
+
+QComboBox:hover {
+ border-color: #4A4A4A;
+}
+
+QComboBox::drop-down {
+ border: none;
+ width: 28px;
+}
+
+QComboBox QAbstractItemView {
+ background: #1E1E1E;
+ color: #F0F0F0;
+ border: 1px solid #333333;
+ selection-background-color: #2A2A2A;
+ selection-color: #FFFFFF;
+ outline: none;
+}
+
+QLineEdit, QSpinBox {
+ background: #1E1E1E;
+ color: #F0F0F0;
+ border: 1px solid #333333;
+ border-radius: 8px;
+ padding: 8px 10px;
+ min-height: 20px;
+}
+
+QLineEdit:focus, QSpinBox:focus, QComboBox:focus {
+ border-color: #F2A65A;
+}
+
+QPushButton {
+ background: #1E1E1E;
+ color: #E8E8E8;
+ border: 1px solid #333333;
+ border-radius: 8px;
+ padding: 8px 14px;
+ min-height: 28px;
+}
+
+QPushButton:hover {
+ background: #2A2A2A;
+ border-color: #4A4A4A;
+}
+
+QPushButton:disabled {
+ color: #666666;
+ background: #181818;
+}
+
+QPushButton#SettingsNavBtn {
+ background: transparent;
+ border: none;
+ border-radius: 6px;
+ color: #A5A6AF;
+ text-align: left;
+ padding: 7px 5px;
+ font-size: 13px;
+ min-height: 21px;
+ max-height: 21px;
+}
+
+QPushButton#SettingsNavBtn:hover {
+ background: #1C1C1C;
+ color: #E0E0E0;
+ border: none;
+}
+
+QPushButton#SettingsNavBtn[active="true"] {
+ background: transparent;
+ color: #F3F3F5;
+ font-weight: 600;
+ border: none;
+}
+
+QPushButton#SettingsCloseBtn {
+ background: transparent;
+ border: none;
+ color: #B0B0B0;
+ font-size: 20px;
+ border-radius: 6px;
+ padding: 4px 10px;
+ min-height: 20px;
+}
+
+QPushButton#SettingsCloseBtn:hover {
+ background: #2A2A2A;
+ color: #FFFFFF;
+ border: none;
+}
+
+QPushButton#SettingsResetBtn {
+ background: transparent;
+ border: none;
+ color: #C7C8CF;
+ font-size: 13px;
+ padding: 8px 12px;
+ min-height: 20px;
+}
+
+QPushButton#SettingsResetBtn:hover {
+ color: #FFFFFF;
+ background: transparent;
+ border: none;
+}
+
+QPushButton#SettingsDoneBtn {
+ background: #F2A65A;
+ color: #1A1208;
+ border: none;
+ border-radius: 8px;
+ padding: 8px 0;
+ font-size: 13px;
+ font-weight: 700;
+ min-height: 20px;
+ max-height: 20px;
+}
+
+QPushButton#SettingsDoneBtn:hover {
+ background: #F5B56F;
+ border: none;
+}
+
+QPushButton#SettingsDoneBtn:pressed {
+ background: #E09545;
+ border: none;
+}
+
+QGroupBox {
+ color: #F0F0F0;
+ border: 1px solid #2A2A2A;
+ border-radius: 8px;
+ margin-top: 12px;
+ padding-top: 12px;
+}
+
+QGroupBox::title {
+ subcontrol-origin: margin;
+ left: 10px;
+ padding: 0 4px;
+ color: #C8C8C8;
+}
+
+QLabel {
+ color: #E8E8E8;
+ background: transparent;
+}
+
+QScrollBar:vertical {
+ background: transparent;
+ width: 7px;
+ margin: 0;
+}
+
+QScrollBar::handle:vertical {
+ background: #3A3A3A;
+ border-radius: 4px;
+ min-height: 30px;
+}
+
+QScrollBar::handle:vertical:hover {
+ background: #505050;
+}
+
+QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical,
+QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
+ height: 0;
+ background: transparent;
+}
+"""
+
+
+_BASE_PALETTE = {
+ "bg": "#111115",
+ "border": "#2B2D36",
+ "text": "#E8E8E8",
+ "strong": "#FFFFFF",
+ "muted": "#8A8A8A",
+ "subtle": "#777984",
+ "divider": "#292B33",
+ "control_bg": "#1E1E1E",
+ "control_text": "#F0F0F0",
+ "control_border": "#333333",
+ "focus": "#F2A65A",
+ "hover_bg": "#2A2A2A",
+ "hover_border": "#4A4A4A",
+ "disabled_text": "#666666",
+ "disabled_bg": "#181818",
+ "nav": "#A5A6AF",
+ "nav_hover": "#E0E0E0",
+ "nav_active": "#F3F3F5",
+ "close": "#B0B0B0",
+ "secondary": "#C7C8CF",
+ "accent": "#F2A65A",
+ "accent_text": "#1A1208",
+ "accent_hover": "#F5B56F",
+ "accent_pressed": "#E09545",
+ "scrollbar": "#3A3A3A",
+ "scrollbar_hover": "#505050",
+ "toggle_off": "#2B2C34",
+ "knob": "#FFFFFF",
+}
+
+
+_THEME_PALETTES = {
+ "default": {
+ "bg": "#0B1220", "border": "#1F2937", "text": "#E6EDF3",
+ "strong": "#F8FAFC", "muted": "#93A8C1", "subtle": "#64748B",
+ "divider": "#1F2937", "control_bg": "#0F172A",
+ "control_text": "#E6EDF3", "control_border": "#1F2937",
+ "focus": "#7AA2FF", "hover_bg": "#15233B",
+ "hover_border": "#35507A", "disabled_text": "#64748B",
+ "disabled_bg": "#0B1626", "nav": "#94A3B8",
+ "nav_hover": "#CBD5E1", "nav_active": "#F8FAFC",
+ "close": "#94A3B8", "secondary": "#CBD5E1",
+ "accent": "#1D4ED8", "accent_text": "#F8FAFC",
+ "accent_hover": "#2563EB", "accent_pressed": "#1E3A8A",
+ "scrollbar": "#35507A", "scrollbar_hover": "#40649B",
+ "toggle_off": "#334155", "knob": "#FFFFFF",
+ },
+ "dark": {
+ "bg": "#0A0A0A", "border": "#2A2A2A", "text": "#E8E8E8",
+ "strong": "#F5F5F5", "muted": "#999999", "subtle": "#777777",
+ "divider": "#2A2A2A", "control_bg": "#1A1A1A",
+ "control_text": "#E8E8E8", "control_border": "#2A2A2A",
+ "focus": "#3B82F6", "hover_bg": "#252525",
+ "hover_border": "#404040", "disabled_text": "#666666",
+ "disabled_bg": "#151515", "nav": "#888888",
+ "nav_hover": "#CCCCCC", "nav_active": "#F5F5F5",
+ "close": "#AAAAAA", "secondary": "#C8C8C8",
+ "accent": "#2563EB", "accent_text": "#F9FAFB",
+ "accent_hover": "#1D4ED8", "accent_pressed": "#1E3A8A",
+ "scrollbar": "#2A2A2A", "scrollbar_hover": "#353535",
+ "toggle_off": "#303030", "knob": "#FFFFFF",
+ },
+ "light": {
+ "bg": "#F8FAFC", "border": "#CBD5E1", "text": "#0F172A",
+ "strong": "#0F172A", "muted": "#64748B", "subtle": "#64748B",
+ "divider": "#E2E8F0", "control_bg": "#FFFFFF",
+ "control_text": "#0F172A", "control_border": "#E2E8F0",
+ "focus": "#2563EB", "hover_bg": "#EAF1FF",
+ "hover_border": "#CBD5E1", "disabled_text": "#94A3B8",
+ "disabled_bg": "#F1F5F9", "nav": "#475569",
+ "nav_hover": "#1E293B", "nav_active": "#0F172A",
+ "close": "#475569", "secondary": "#334155",
+ "accent": "#2563EB", "accent_text": "#FFFFFF",
+ "accent_hover": "#1D4ED8", "accent_pressed": "#1E3A8A",
+ "scrollbar": "#CBD5E1", "scrollbar_hover": "#94A3B8",
+ "toggle_off": "#CBD5E1", "knob": "#FFFFFF",
+ },
+ "purple": {
+ "bg": "#0E0C16", "border": "#241E40", "text": "#ECE7FF",
+ "strong": "#F5F3FF", "muted": "#A99DCB", "subtle": "#786F98",
+ "divider": "#241E40", "control_bg": "#141225",
+ "control_text": "#ECE7FF", "control_border": "#241E40",
+ "focus": "#A78BFA", "hover_bg": "#1B1731",
+ "hover_border": "#3B315F", "disabled_text": "#786F98",
+ "disabled_bg": "#100E1C", "nav": "#A99DCB",
+ "nav_hover": "#D8D0F3", "nav_active": "#F5F3FF",
+ "close": "#C8BDF2", "secondary": "#C8BDF2",
+ "accent": "#8B5CF6", "accent_text": "#F5F3FF",
+ "accent_hover": "#7C3AED", "accent_pressed": "#5B21B6",
+ "scrollbar": "#3B315F", "scrollbar_hover": "#524187",
+ "toggle_off": "#3B315F", "knob": "#FFFFFF",
+ },
+ "cyberpunk": {
+ "bg": "#0D0221", "border": "#4A176A", "text": "#F0E7FF",
+ "strong": "#FFFFFF", "muted": "#A88FD9", "subtle": "#77649D",
+ "divider": "#35104F", "control_bg": "#1A0933",
+ "control_text": "#F0E7FF", "control_border": "#FF006E",
+ "focus": "#FF006E", "hover_bg": "#240A44",
+ "hover_border": "#8338EC", "disabled_text": "#77649D",
+ "disabled_bg": "#12031F", "nav": "#A88FD9",
+ "nav_hover": "#FF4DA0", "nav_active": "#FF006E",
+ "close": "#D6C6EE", "secondary": "#D6C6EE",
+ "accent": "#FF006E", "accent_text": "#FFFFFF",
+ "accent_hover": "#FF1A7F", "accent_pressed": "#CC005B",
+ "scrollbar": "#8338EC", "scrollbar_hover": "#9D4EFF",
+ "toggle_off": "#3B1A58", "knob": "#FFFFFF",
+ },
+ "sunset": {
+ "bg": "#2D1B3D", "border": "#6B3B5D", "text": "#FFFFFF",
+ "strong": "#FFFFFF", "muted": "#E0B8C8", "subtle": "#B88D9E",
+ "divider": "#6B3B5D", "control_bg": "#4A2C4F",
+ "control_text": "#FFFFFF", "control_border": "#B96567",
+ "focus": "#FF8C64", "hover_bg": "#5E3A52",
+ "hover_border": "#D0736F", "disabled_text": "#B88D9E",
+ "disabled_bg": "#38223F", "nav": "#E0B8C8",
+ "nav_hover": "#FFD19A", "nav_active": "#FFBE76",
+ "close": "#F0CBD7", "secondary": "#F0CBD7",
+ "accent": "#F77F51", "accent_text": "#FFFFFF",
+ "accent_hover": "#FF9566", "accent_pressed": "#D66843",
+ "scrollbar": "#8C526A", "scrollbar_hover": "#AD667C",
+ "toggle_off": "#6B455F", "knob": "#FFFFFF",
+ },
+ "matrix": {
+ "bg": "#0D0D0D", "border": "#00661A", "text": "#00FF41",
+ "strong": "#3AFF6B", "muted": "#00B82F", "subtle": "#008A23",
+ "divider": "#004D14", "control_bg": "#001A00",
+ "control_text": "#00FF41", "control_border": "#00B82F",
+ "focus": "#00FF41", "hover_bg": "#003300",
+ "hover_border": "#00FF41", "disabled_text": "#007A1F",
+ "disabled_bg": "#001400", "nav": "#00B82F",
+ "nav_hover": "#3AFF6B", "nav_active": "#00FF41",
+ "close": "#00CC33", "secondary": "#00CC33",
+ "accent": "#00FF41", "accent_text": "#001A00",
+ "accent_hover": "#3AFF6B", "accent_pressed": "#00CC33",
+ "scrollbar": "#00661A", "scrollbar_hover": "#009926",
+ "toggle_off": "#004D14", "knob": "#FFFFFF",
+ },
+ "ocean": {
+ "bg": "#0A1929", "border": "#1E5A78", "text": "#FFFFFF",
+ "strong": "#FFFFFF", "muted": "#B3D9F2", "subtle": "#79A9C5",
+ "divider": "#1E4B66", "control_bg": "#16364D",
+ "control_text": "#FFFFFF", "control_border": "#297EAA",
+ "focus": "#29B6F6", "hover_bg": "#1A4A68",
+ "hover_border": "#29B6F6", "disabled_text": "#79A9C5",
+ "disabled_bg": "#10283A", "nav": "#B3D9F2",
+ "nav_hover": "#80DEEA", "nav_active": "#4DD0E1",
+ "close": "#C6E6F5", "secondary": "#C6E6F5",
+ "accent": "#0288D1", "accent_text": "#FFFFFF",
+ "accent_hover": "#039BE5", "accent_pressed": "#0277BD",
+ "scrollbar": "#236184", "scrollbar_hover": "#2C7CA4",
+ "toggle_off": "#23516A", "knob": "#FFFFFF",
+ },
+ "retro": {
+ "bg": "#2673BF", "border": "#7591B8", "text": "#FFFFFF",
+ "strong": "#FFFFFF", "muted": "#D8E8FA", "subtle": "#B8D0EC",
+ "divider": "#7591B8", "control_bg": "#FFFFFF",
+ "control_text": "#000000", "control_border": "#7591B8",
+ "focus": "#3689D9", "hover_bg": "#E8F2FF",
+ "hover_border": "#A0A0A0", "disabled_text": "#777777",
+ "disabled_bg": "#D8D8D8", "nav": "#D8E8FA",
+ "nav_hover": "#FFFFFF", "nav_active": "#FFFFFF",
+ "close": "#FFFFFF", "secondary": "#FFFFFF",
+ "accent": "#2E68B0", "accent_text": "#FFFFFF",
+ "accent_hover": "#3B7EC9", "accent_pressed": "#1F4F8B",
+ "scrollbar": "#D8D8D8", "scrollbar_hover": "#B8B8B8",
+ "toggle_off": "#7591B8", "knob": "#FFFFFF",
+ },
+}
+
+
+def settings_palette(theme_key: str) -> dict[str, str]:
+ palette = dict(_BASE_PALETTE)
+ palette.update(_THEME_PALETTES.get(theme_key, _THEME_PALETTES["default"]))
+ return palette
+
+
+def build_settings_style(theme_key: str) -> str:
+ """Recolor the reference layout using the application's selected theme."""
+ palette = settings_palette(theme_key)
+ replacements = {
+ "#111115": palette["bg"],
+ "#2B2D36": palette["border"],
+ "#F5F5F5": palette["text"],
+ "#FFFFFF": palette["strong"],
+ "#8A8A8A": palette["muted"],
+ "#777984": palette["subtle"],
+ "#292B33": palette["divider"],
+ "#1E1E1E": palette["control_bg"],
+ "#F0F0F0": palette["control_text"],
+ "#333333": palette["control_border"],
+ "#F2A65A": palette["focus"],
+ "#2A2A2A": palette["hover_bg"],
+ "#4A4A4A": palette["hover_border"],
+ "#666666": palette["disabled_text"],
+ "#181818": palette["disabled_bg"],
+ "#A5A6AF": palette["nav"],
+ "#E0E0E0": palette["nav_hover"],
+ "#F3F3F5": palette["nav_active"],
+ "#B0B0B0": palette["close"],
+ "#C7C8CF": palette["secondary"],
+ "#1A1208": palette["accent_text"],
+ "#F5B56F": palette["accent_hover"],
+ "#E09545": palette["accent_pressed"],
+ "#E8E8E8": palette["text"],
+ "#C8C8C8": palette["secondary"],
+ "#3A3A3A": palette["scrollbar"],
+ "#505050": palette["scrollbar_hover"],
+ }
+ # Accent and focus share the same base token in the original style.
+ # Replace the Done button rules after the one-pass palette substitution.
+ pattern = re.compile("|".join(map(re.escape, replacements)))
+ style = pattern.sub(lambda match: replacements[match.group(0)], SETTINGS_STYLE)
+ style += (
+ "\nQPushButton#SettingsDoneBtn {"
+ f" background: {palette['accent']};"
+ f" color: {palette['accent_text']};"
+ "}\n"
+ "QPushButton#SettingsDoneBtn:hover {"
+ f" background: {palette['accent_hover']};"
+ "}\n"
+ "QPushButton#SettingsDoneBtn:pressed {"
+ f" background: {palette['accent_pressed']};"
+ "}\n"
+ )
+ return style
+
+
+class SettingsCloseButton(QPushButton):
+ """Thin, font-independent close icon used by the frameless modal."""
+
+ def __init__(self, color="#B7B8C0", parent=None):
+ super().__init__(parent)
+ self._icon_color = QColor(color)
+ self.setObjectName("SettingsCloseBtn")
+ self.setCursor(Qt.CursorShape.PointingHandCursor)
+
+ def paintEvent(self, event):
+ super().paintEvent(event)
+ painter = QPainter(self)
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
+ painter.setPen(QPen(
+ self._icon_color, 1.5,
+ Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap,
+ ))
+ cx = self.width() / 2
+ cy = self.height() / 2
+ painter.drawLine(QPointF(cx - 4, cy - 4), QPointF(cx + 4, cy + 4))
+ painter.drawLine(QPointF(cx + 4, cy - 4), QPointF(cx - 4, cy + 4))
+
+ def set_icon_color(self, color):
+ self._icon_color = QColor(color)
+ self.update()
+
+
+class SettingsNavButton(QPushButton):
+ """Reference-style sidebar button backed by painted line icons."""
+
+ activated = Signal(int)
+
+ def __init__(
+ self,
+ index: int,
+ icon_name: str,
+ label: str,
+ parent=None,
+ inactive_color="#777984",
+ active_color="#F1F1F4",
+ ):
+ super().__init__(parent)
+ self.index = index
+ self._icon_name = icon_name
+ self._label = label
+ self._inactive_color = inactive_color
+ self._active_color = active_color
+ self.setObjectName("SettingsNavBtn")
+ self.setCursor(Qt.PointingHandCursor)
+ self.setCheckable(False)
+ self.setFocusPolicy(Qt.NoFocus)
+ self._refresh_text()
+ self.clicked.connect(lambda: self.activated.emit(self.index))
+
+ def _refresh_text(self):
+ self.setText(self._label)
+
+ def set_label(self, label: str):
+ self._label = label
+ self._refresh_text()
+
+ def set_active(self, active: bool):
+ value = "true" if active else "false"
+ changed = self.property("active") != value
+ self.setProperty("active", value)
+ self.setIcon(_settings_nav_icon(
+ self._icon_name,
+ self._active_color if active else self._inactive_color,
+ ))
+ self.setIconSize(QSize(17, 17))
+ # Re-polishing hidden child widgets while a frameless top-level window
+ # is being built can create visible native-window flashes on Windows.
+ if changed and self.isVisible():
+ self.style().unpolish(self)
+ self.style().polish(self)
+ self.update()
+
+ def set_theme_colors(self, inactive_color: str, active_color: str):
+ self._inactive_color = inactive_color
+ self._active_color = active_color
+ active = self.property("active") == "true"
+ self.setIcon(_settings_nav_icon(
+ self._icon_name,
+ self._active_color if active else self._inactive_color,
+ ))
+ self.setIconSize(QSize(17, 17))
+ self.update()
+
+
+class SettingRow(QWidget):
+ """Title + description on the left, control on the right (Haulbase row)."""
+
+ def __init__(self, title: str, description: str = "", control: QWidget | None = None, parent=None):
+ super().__init__(parent)
+ self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
+ outer = QVBoxLayout(self)
+ outer.setContentsMargins(0, 0, 0, 0)
+ outer.setSpacing(0)
+
+ row = QHBoxLayout()
+ row.setContentsMargins(0, 10, 0, 10)
+ row.setSpacing(16)
+
+ text_col = QVBoxLayout()
+ text_col.setSpacing(3)
+ text_col.setContentsMargins(0, 0, 0, 0)
+ self.title_label = QLabel(title)
+ self.title_label.setObjectName("SettingRowTitle")
+ self.title_label.setWordWrap(True)
+ self.title_label.setVisible(bool(title))
+ self.desc_label = QLabel(description)
+ self.desc_label.setObjectName("SettingRowDesc")
+ self.desc_label.setWordWrap(True)
+ self.desc_label.setVisible(bool(description))
+ text_col.addWidget(self.title_label)
+ text_col.addWidget(self.desc_label)
+ row.addLayout(text_col, 1)
+
+ if control is not None:
+ control.setParent(self)
+ row.addWidget(control, 0, Qt.AlignVCenter | Qt.AlignRight)
+
+ outer.addLayout(row)
+ div = QFrame()
+ div.setObjectName("SettingRowDivider")
+ div.setFrameShape(QFrame.Shape.NoFrame)
+ outer.addWidget(div)
+ self.setMinimumHeight(64)
+
+ def set_texts(self, title: str, description: str = ""):
+ self.title_label.setText(title)
+ self.title_label.setVisible(bool(title))
+ self.desc_label.setText(description)
+ self.desc_label.setVisible(bool(description))
+
+
+def modernize_form_layout(form: QFormLayout):
+ """Convert legacy QFormLayout rows to the shared settings-row design."""
+ rows = []
+ for row_index in range(form.rowCount()):
+ label_item = form.itemAt(row_index, QFormLayout.ItemRole.LabelRole)
+ field_item = form.itemAt(row_index, QFormLayout.ItemRole.FieldRole)
+ span_item = form.itemAt(row_index, QFormLayout.ItemRole.SpanningRole)
+ rows.append((
+ _layout_item_payload(label_item),
+ _layout_item_payload(field_item),
+ _layout_item_payload(span_item),
+ ))
+
+ while form.count():
+ form.takeAt(0)
+ while form.rowCount():
+ form.removeRow(0)
+
+ form.setContentsMargins(0, 0, 31, 8)
+ form.setHorizontalSpacing(0)
+ form.setVerticalSpacing(0)
+
+ for label_payload, field_payload, span_payload in rows:
+ if span_payload is not None:
+ widget = _payload_widget(span_payload)
+ if widget is None:
+ continue
+ if isinstance(widget, QLabel):
+ text = widget.text().strip()
+ if not text:
+ widget.hide()
+ continue
+ if "" in text.lower():
+ widget.setObjectName("SettingsSectionTitle")
+ else:
+ widget.setObjectName("SettingsInfoText")
+ if "#666" in widget.styleSheet() or "#888" in widget.styleSheet():
+ widget.setStyleSheet("")
+ widget.setWordWrap(True)
+ form.addRow(widget)
+ continue
+
+ label_widget = _payload_widget(label_payload)
+ field_widget = _payload_widget(field_payload)
+ if field_widget is None:
+ continue
+
+ title = ""
+ if isinstance(label_widget, QLabel):
+ title = label_widget.text().strip().rstrip(":")
+ label_widget.hide()
+
+ if not title and isinstance(field_widget, QLabel):
+ text = field_widget.text().strip()
+ if text:
+ field_widget.setObjectName("SettingsInfoText")
+ if "#666" in field_widget.styleSheet() or "#888" in field_widget.styleSheet():
+ field_widget.setStyleSheet("")
+ field_widget.setWordWrap(True)
+ form.addRow(field_widget)
+ continue
+
+ _normalize_control_widths(field_widget)
+ form.addRow(SettingRow(title, "", field_widget))
+
+
+def _layout_item_payload(item):
+ if item is None:
+ return None
+ widget = item.widget()
+ if widget is not None:
+ return ("widget", widget)
+ layout = item.layout()
+ if layout is not None:
+ return ("layout", layout)
+ return None
+
+
+def _payload_widget(payload) -> QWidget | None:
+ if payload is None:
+ return None
+ kind, value = payload
+ if kind == "widget":
+ return value
+
+ source_layout = value
+ wrapper = QWidget()
+ target = QHBoxLayout(wrapper)
+ target.setContentsMargins(0, 0, 0, 0)
+ target.setSpacing(max(6, source_layout.spacing()))
+ while source_layout.count():
+ item = source_layout.takeAt(0)
+ widget = item.widget()
+ child_layout = item.layout()
+ spacer = item.spacerItem()
+ if widget is not None:
+ _normalize_control_widths(widget)
+ target.addWidget(widget, 0, item.alignment())
+ elif child_layout is not None:
+ target.addLayout(child_layout)
+ elif spacer is not None:
+ target.addItem(spacer)
+ return wrapper
+
+
+def _normalize_control_widths(widget: QWidget):
+ if isinstance(widget, QComboBox):
+ widget.setFixedWidth(190)
+ elif isinstance(widget, QSpinBox):
+ widget.setFixedWidth(190)
+ elif isinstance(widget, QLineEdit):
+ widget.setFixedWidth(260)
+ elif isinstance(widget, QAbstractButton):
+ if widget.maximumWidth() > 190:
+ widget.setMaximumWidth(190)
+
+ for child in widget.findChildren(QWidget):
+ if child is widget:
+ continue
+ if isinstance(child, QComboBox):
+ child.setFixedWidth(190)
+ elif isinstance(child, QSpinBox):
+ child.setFixedWidth(190)
+ elif isinstance(child, QLineEdit):
+ child.setFixedWidth(260)
+ elif isinstance(child, QAbstractButton) and child.maximumWidth() > 190:
+ child.setMaximumWidth(190)
+
+
+def _settings_nav_icon(name: str, color: str) -> QIcon:
+ """Create crisp monochrome line icons matching the supplied reference."""
+ pixmap = QPixmap(18, 18)
+ pixmap.fill(Qt.GlobalColor.transparent)
+ painter = QPainter(pixmap)
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
+ painter.setPen(QPen(
+ QColor(color), 1.45, Qt.PenStyle.SolidLine,
+ Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin,
+ ))
+ painter.setBrush(Qt.BrushStyle.NoBrush)
+
+ if name == "general":
+ for y, knob_x in ((4.0, 6.0), (9.0, 12.0), (14.0, 7.5)):
+ painter.drawLine(QPointF(2.5, y), QPointF(15.5, y))
+ painter.drawEllipse(QPointF(knob_x, y), 1.7, 1.7)
+ elif name == "appearance":
+ painter.drawEllipse(QRectF(2.5, 2.5, 13, 13))
+ painter.drawEllipse(QPointF(6.0, 6.4), 0.7, 0.7)
+ painter.drawEllipse(QPointF(10.0, 5.4), 0.7, 0.7)
+ painter.drawEllipse(QPointF(13.0, 8.0), 0.7, 0.7)
+ painter.drawPath(_path("M 8.8 15.5 C 8.0 13.2 10.4 11.8 12.1 12.8"))
+ elif name == "behavior":
+ painter.drawLine(QPointF(9, 2.5), QPointF(9, 12.5))
+ painter.drawLine(QPointF(5.8, 9.5), QPointF(9, 12.7))
+ painter.drawLine(QPointF(12.2, 9.5), QPointF(9, 12.7))
+ painter.drawLine(QPointF(3, 15.0), QPointF(15, 15.0))
+ elif name == "security":
+ painter.drawPath(_path(
+ "M 9 2.2 L 14.3 4.4 L 13.6 11.5 "
+ "C 12.8 14 10.8 15.2 9 16 "
+ "C 7.2 15.2 5.2 14 4.4 11.5 L 3.7 4.4 Z"
+ ))
+ elif name == "video":
+ painter.drawRoundedRect(QRectF(2.3, 4.0, 10.0, 10.0), 2, 2)
+ painter.drawPath(_path("M 12.4 7 L 15.8 5.5 L 15.8 12.5 L 12.4 11"))
+ elif name == "reminders":
+ painter.drawPath(_path(
+ "M 4 12.8 L 5.2 11.2 L 5.2 7.4 "
+ "C 5.2 4.9 6.8 3.3 9 3.3 "
+ "C 11.2 3.3 12.8 4.9 12.8 7.4 "
+ "L 12.8 11.2 L 14 12.8 Z"
+ ))
+ painter.drawArc(QRectF(7.2, 13.0, 3.6, 2.5), 180 * 16, 180 * 16)
+ elif name == "sync":
+ painter.drawArc(QRectF(3.0, 3.0, 12.0, 12.0), 35 * 16, 135 * 16)
+ painter.drawLine(QPointF(3.0, 5.5), QPointF(3.3, 2.4))
+ painter.drawLine(QPointF(3.0, 5.5), QPointF(6.0, 5.0))
+ painter.drawArc(QRectF(3.0, 3.0, 12.0, 12.0), 215 * 16, 135 * 16)
+ painter.drawLine(QPointF(15.0, 12.5), QPointF(14.7, 15.6))
+ painter.drawLine(QPointF(15.0, 12.5), QPointF(12.0, 13.0))
+ elif name == "tray":
+ painter.drawRoundedRect(QRectF(3.0, 2.5, 12.0, 13.0), 2, 2)
+ painter.drawLine(QPointF(3.0, 6.2), QPointF(15.0, 6.2))
+ painter.drawEllipse(QPointF(9.0, 10.6), 1.2, 1.2)
+ else:
+ painter.drawEllipse(QRectF(2.7, 2.7, 12.6, 12.6))
+ painter.drawPoint(QPointF(9, 6))
+ painter.drawLine(QPointF(9, 8.5), QPointF(9, 12.5))
+
+ painter.end()
+ return QIcon(pixmap)
+
+
+def _path(commands: str) -> QPainterPath:
+ """Parse the tiny M/L/C/Z path subset used by the icons above."""
+ tokens = commands.replace(",", " ").split()
+ path = QPainterPath()
+ index = 0
+ command = ""
+ while index < len(tokens):
+ token = tokens[index]
+ if token in {"M", "L", "C", "Z"}:
+ command = token
+ index += 1
+ if command == "Z":
+ path.closeSubpath()
+ continue
+ if command == "M":
+ path.moveTo(float(tokens[index]), float(tokens[index + 1]))
+ index += 2
+ elif command == "L":
+ path.lineTo(float(tokens[index]), float(tokens[index + 1]))
+ index += 2
+ elif command == "C":
+ path.cubicTo(
+ float(tokens[index]), float(tokens[index + 1]),
+ float(tokens[index + 2]), float(tokens[index + 3]),
+ float(tokens[index + 4]), float(tokens[index + 5]),
+ )
+ index += 6
+ else:
+ index += 1
+ return path
diff --git a/clipstack/ui/settings_dialog.py b/clipstack/ui/settings_dialog.py
index 3e33bb1..d2a8284 100644
--- a/clipstack/ui/settings_dialog.py
+++ b/clipstack/ui/settings_dialog.py
@@ -1,2190 +1,2793 @@
-from __future__ import annotations
-
-from pathlib import Path
-from typing import Dict
-
-from PySide6.QtCore import Qt, Signal, QUrl, QSize, QTimer, QThread
-from PySide6.QtGui import QIcon, QDesktopServices, QKeySequence
-from PySide6.QtWidgets import (
- QDialog,
- QVBoxLayout,
- QTabWidget,
- QWidget,
- QFormLayout,
- QComboBox,
- QHBoxLayout,
- QPushButton,
- QFileDialog,
- QLabel,
- QSpinBox,
- QGroupBox,
- QLineEdit,
- QMessageBox,
- QStyle,
- QFrame,
- QScrollArea,
-)
-
-from ..settings import Settings
-from ..utils import resource_path, svg_icon
-from ..i18n import i18n
-from ..theme_manager import theme_manager
-from .widgets.toggle_switch import ToggleSwitch
-from ..sound_player import SoundPlayer, is_sound_backend_available, get_sound_backend_error
-
-
-LANG_MAP: Dict[str, str] = {
- "tr": "Türkçe",
- "en": "English",
- "de": "Deutsch",
- "fr": "Français",
- "es": "Español",
- "it": "Italiano",
- "az": "Azərbaycanca",
- "ru": "Русский",
- "ja": "日本語",
- "zh": "中文",
-}
-
-THEMES = [
- ("default", "Default (Blue)"),
- ("dark", "Dark (Black)"),
- ("light", "Light (White)"),
- ("purple", "Purple"),
- ("cyberpunk", "🌆 Cyberpunk"),
- ("sunset", "🌅 Sunset"),
- ("matrix", "💚 Matrix"),
- ("ocean", "🌊 Ocean"),
- ("retro", "🎮 Retro (XP)"),
-]
-
-
-class GoogleDriveStatusThread(QThread):
- status_ready = Signal(bool)
- status_failed = Signal(str)
-
- def run(self):
- try:
- from clipstack.gdrive_sync import GoogleDriveSync
-
- sync = GoogleDriveSync()
- self.status_ready.emit(sync.is_connected())
- except Exception as exc:
- self.status_failed.emit(str(exc))
-
-
-class VideoProbeThread(QThread):
- probe_ready = Signal(bool, str, list)
- probe_failed = Signal(str)
-
- def __init__(self, settings: Settings, parent=None):
- super().__init__(parent)
- self._settings = settings
-
- def run(self):
- try:
- from clipstack.video_recorder import AdvancedVideoRecorder
-
- recorder = AdvancedVideoRecorder(self._settings)
- available = recorder.is_available()
- encoder_info = recorder.get_encoder_info()
- devices = recorder.list_audio_devices()
- self.probe_ready.emit(available, encoder_info, devices)
- except Exception as exc:
- self.probe_failed.emit(str(exc))
-
-
-class KeyCaptureLineEdit(QLineEdit):
- def __init__(self, parent=None):
- super().__init__(parent)
- self.setClearButtonEnabled(True)
- self.refresh_placeholder()
- i18n.languageChanged.connect(self.refresh_placeholder)
-
- def refresh_placeholder(self):
- self.setPlaceholderText(self._tr("settings.general.hotkey.placeholder", "e.g., ctrl+shift+v"))
-
- def _tr(self, key: str, fallback: str) -> str:
- try:
- v = i18n.t(key)
- except Exception:
- v = ""
- return v if v and v != key else fallback
-
- @staticmethod
- def normalize_combo(combo: str) -> str:
- t = (combo or "").strip().lower()
- if not t:
- return ""
- parts = [p.strip() for p in t.split("+") if p.strip()]
- mods = set()
- key = ""
- alias = {
- "control": "ctrl",
- "cmd": "windows",
- "command": "windows",
- "super": "windows",
- "win": "windows",
- "option": "alt",
- "menu": "alt",
- }
- specials = {
- "space": "space", "tab": "tab", "insert": "insert", "delete": "delete",
- "home": "home", "end": "end", "page up": "pgup", "page down": "pgdn",
- "pgup": "pgup", "pgdn": "pgdn",
- "up": "up", "down": "down", "left": "left", "right": "right",
- "escape": "esc", "esc": "esc",
- "return": "enter", "enter": "enter", "backspace": "backspace",
- }
- for p in parts:
- p2 = alias.get(p, p)
- if p2 in ("ctrl", "shift", "alt", "windows"):
- mods.add(p2)
- else:
- if p2.startswith("f") and p2[1:].isdigit():
- key = p2
- elif p2 in specials:
- key = specials[p2]
- else:
- key = p2
- ordered_mods = [m for m in ("ctrl", "shift", "alt", "windows") if m in mods]
- return "+".join(ordered_mods + ([key] if key else []))
-
- def keyPressEvent(self, e):
- if e.key() in (Qt.Key_Control, Qt.Key_Shift, Qt.Key_Alt, Qt.Key_Meta):
- return
-
- mods = []
- if e.modifiers() & Qt.ControlModifier:
- mods.append("ctrl")
- if e.modifiers() & Qt.ShiftModifier:
- mods.append("shift")
- if e.modifiers() & Qt.AltModifier:
- mods.append("alt")
- if e.modifiers() & Qt.MetaModifier:
- mods.append("windows")
-
- key_text = QKeySequence(e.key()).toString().strip()
- special_map = {
- "Space": "space", "Tab": "tab", "Insert": "insert", "Delete": "delete",
- "Home": "home", "End": "end", "Page Up": "pgup", "Page Down": "pgdn",
- "Up": "up", "Down": "down", "Left": "left", "Right": "right",
- "Escape": "esc", "Return": "enter", "Enter": "enter", "Backspace": "backspace",
- }
- if key_text in special_map:
- key_part = special_map[key_text]
- elif key_text.upper().startswith("F") and key_text[1:].isdigit():
- key_part = key_text.lower()
- else:
- key_part = key_text.lower() if len(key_text) else ""
-
- combo = "+".join(mods + ([key_part] if key_part else [])) if mods or key_part else ""
- norm = self.normalize_combo(combo)
- if norm:
- self.setText(norm)
-
-
-class SettingsDialog(QDialog):
- applied = Signal()
-
- def __init__(self, settings: Settings, parent=None):
- super().__init__(parent)
- self.settings = settings
-
- self._sound_tester: SoundPlayer | None = None
- if is_sound_backend_available():
- try:
- self._sound_tester = SoundPlayer(self)
- try:
- self._sound_tester.playbackFailed.connect(self._on_sound_test_failed)
- except Exception:
- pass
- except Exception as exc:
- print(f"[SETTINGS SOUND] Tester init failed: {exc}")
- else:
- print(f"[SETTINGS SOUND] QtMultimedia backend unavailable: {get_sound_backend_error()}; WAV-only fallback will be used for testing.")
-
- self.setWindowTitle("Settings")
- try:
- self.setWindowIcon(svg_icon("assets/icons/gear.svg"))
- except Exception:
- pass
- self.resize(760, 640)
-
- self.tabs = QTabWidget(self)
- self.tab_general = QWidget()
- self.tab_appearance = QWidget()
- self.tab_behavior = QWidget()
- self.tab_security = QWidget()
- self.tab_video = QWidget()
- self.tab_reminders = QWidget()
- self.tab_sync = QWidget() # Senkronizasyon ve Paylaşım
- self.tab_tray = QWidget()
- self.tab_about = QWidget()
-
- self.tabs.addTab(self.tab_general, "")
- self.tabs.addTab(self.tab_appearance, "")
- self.tabs.addTab(self.tab_behavior, "")
- self.tabs.addTab(self.tab_security, "")
- self.tabs.addTab(self.tab_video, "")
- self.tabs.addTab(self.tab_reminders, "")
- self.tabs.addTab(self.tab_sync, "")
- self.tabs.addTab(self.tab_tray, "")
- self.tabs.addTab(self.tab_about, "")
-
- root = QVBoxLayout(self)
- root.addWidget(self.tabs)
-
- form_g = QFormLayout(self.tab_general)
- form_g.setContentsMargins(12, 12, 12, 12)
- form_g.setSpacing(10)
-
- self.cmb_lang = QComboBox()
- self.cmb_lang.setMinimumHeight(36)
- for code, label in LANG_MAP.items():
- self.cmb_lang.addItem(label, code)
- self.cmb_lang.setCurrentIndex(max(0, self.cmb_lang.findData(settings.get("language", "tr"))))
-
- self.tgl_startup = ToggleSwitch(checked=bool(settings.get("launch_at_startup", True)))
-
- self.txt_hotkey = KeyCaptureLineEdit()
- self.txt_hotkey.setText(KeyCaptureLineEdit.normalize_combo(str(settings.get("hotkey", "ctrl+shift+v"))))
- self.btn_clear_hk = QPushButton()
-
- hk_row = QHBoxLayout()
- hk_row.addWidget(self.txt_hotkey, 1)
- hk_row.addWidget(self.btn_clear_hk)
-
- self.txt_hotkey_paste = KeyCaptureLineEdit()
- self.txt_hotkey_paste.setText(KeyCaptureLineEdit.normalize_combo(str(settings.get("hotkey_paste_last", ""))))
- self.btn_clear_hk_paste = QPushButton()
-
- hk_paste_row = QHBoxLayout()
- hk_paste_row.addWidget(self.txt_hotkey_paste, 1)
- hk_paste_row.addWidget(self.btn_clear_hk_paste)
-
- self.txt_hotkey_quick_note = KeyCaptureLineEdit()
- self.txt_hotkey_quick_note.setText(KeyCaptureLineEdit.normalize_combo(str(settings.get("hotkey_quick_note", ""))))
- self.btn_clear_hk_quick_note = QPushButton()
-
- self.txt_hotkey_screenshot = KeyCaptureLineEdit()
- self.txt_hotkey_screenshot.setText(KeyCaptureLineEdit.normalize_combo(str(settings.get("hotkey_screenshot", ""))))
- self.btn_clear_hk_screenshot = QPushButton()
-
- self.txt_hotkey_ocr = KeyCaptureLineEdit()
- self.txt_hotkey_ocr.setText(KeyCaptureLineEdit.normalize_combo(str(settings.get("hotkey_ocr", ""))))
- self.btn_clear_hk_ocr = QPushButton()
-
- self.txt_hotkey_snip = KeyCaptureLineEdit()
- self.txt_hotkey_snip.setText(KeyCaptureLineEdit.normalize_combo(str(settings.get("hotkey_snip", ""))))
- self.btn_clear_hk_snip = QPushButton()
-
- try:
- reset_icon = self.style().standardIcon(QStyle.SP_LineEditClearButton)
- for btn in (self.btn_clear_hk, self.btn_clear_hk_paste, self.btn_clear_hk_quick_note, self.btn_clear_hk_screenshot, self.btn_clear_hk_ocr, self.btn_clear_hk_snip):
- btn.setIcon(reset_icon)
- btn.setIconSize(QSize(18, 18))
- btn.setCursor(Qt.PointingHandCursor)
- except Exception:
- pass
-
- hk_quick_note_row = QHBoxLayout()
- hk_quick_note_row.addWidget(self.txt_hotkey_quick_note, 1)
- hk_quick_note_row.addWidget(self.btn_clear_hk_quick_note)
-
- hk_screenshot_row = QHBoxLayout()
- hk_screenshot_row.addWidget(self.txt_hotkey_screenshot, 1)
- hk_screenshot_row.addWidget(self.btn_clear_hk_screenshot)
-
- hk_ocr_row = QHBoxLayout()
- hk_ocr_row.addWidget(self.txt_hotkey_ocr, 1)
- hk_ocr_row.addWidget(self.btn_clear_hk_ocr)
-
- hk_snip_row = QHBoxLayout()
- hk_snip_row.addWidget(self.txt_hotkey_snip, 1)
- hk_snip_row.addWidget(self.btn_clear_hk_snip)
-
- self.lbl_hotkey_help = QLabel()
- form_g.addRow(self.lbl_hotkey_help)
- form_g.addRow(self._tr("settings.general.hotkey.label", "Ana kısayol tuşu"), hk_row)
- form_g.addRow(self._tr("settings.general.hotkey_paste.label", "Son içeriği yapıştır"), hk_paste_row)
- form_g.addRow(self._tr("settings.general.hotkey_quick_note.label", "Hızlı not al"), hk_quick_note_row)
- form_g.addRow(self._tr("settings.general.hotkey_screenshot.label", "Tam ekran görüntüsü al"), hk_screenshot_row)
- form_g.addRow(self._tr("settings.general.hotkey_ocr.label", "Ekran OCR (Yazı tanı)"), hk_ocr_row)
- form_g.addRow(self._tr("settings.general.hotkey_snip.label", "Ekran Alıntısı (Lightshot)"), hk_snip_row)
- form_g.addRow(self._tr("settings.general.language", "Dil"), self.cmb_lang)
- form_g.addRow(self._tr("settings.general.launch_at_startup", "Windows ile başlat"), self.tgl_startup)
-
- form_a = QFormLayout(self.tab_appearance)
- form_a.setContentsMargins(12, 12, 12, 12)
- form_a.setSpacing(10)
- self.cmb_theme = QComboBox()
- for key, label in THEMES:
- self.cmb_theme.addItem(label, key)
- self.cmb_theme.setCurrentIndex(max(0, self.cmb_theme.findData(settings.get("theme", "default"))))
- self.tgl_animations = ToggleSwitch(checked=bool(settings.get("animations", True)))
- form_a.addRow(self._tr("settings.appearance.theme", "Tema"), self.cmb_theme)
- form_a.addRow(self._tr("settings.appearance.animations", "Animasyonları etkinleştir"), self.tgl_animations)
-
- form_b = QFormLayout(self.tab_behavior)
- form_b.setContentsMargins(12, 12, 12, 12)
- form_b.setSpacing(10)
- self.tgl_hide_after_copy = ToggleSwitch(checked=bool(settings.get("hide_after_copy", False)))
- self.tgl_stay_on_top = ToggleSwitch(checked=bool(settings.get("stay_on_top", False)))
- self.spn_max_items = QSpinBox()
- self.spn_max_items.setRange(100, 5000)
- self.spn_max_items.setValue(int(settings.get("max_items", 1000)))
- self.spn_dedupe_ms = QSpinBox()
- self.spn_dedupe_ms.setRange(0, 10000)
- self.spn_dedupe_ms.setValue(int(settings.get("dedupe_window_ms", 1200)))
- self.tgl_confirm_delete = ToggleSwitch(checked=bool(settings.get("confirm_delete", True)))
- self.tgl_toast = ToggleSwitch(checked=bool(settings.get("show_toast", True)))
-
- form_b.addRow(self._tr("settings.behavior.hide_after_copy", "Kopyalama sonrası gizle"), self.tgl_hide_after_copy)
- form_b.addRow(self._tr("settings.behavior.stay_on_top", "Pencereyi üstte tut"), self.tgl_stay_on_top)
- form_b.addRow(self._tr("settings.behavior.max_items", "Maksimum öğe sayısı"), self.spn_max_items)
- form_b.addRow(self._tr("settings.behavior.dedupe_ms", "Tekrar engelleme süresi (ms)"), self.spn_dedupe_ms)
- form_b.addRow(self._tr("settings.behavior.confirm_delete", "Silmeden önce onayla"), self.tgl_confirm_delete)
- form_b.addRow(self._tr("settings.behavior.show_toast", "Uygulama içi bildirimleri göster"), self.tgl_toast)
-
- # Güvenlik tab'ı için scroll area
- security_scroll = QScrollArea(self.tab_security)
- security_scroll.setWidgetResizable(True)
- security_scroll.setFrameShape(QFrame.Shape.NoFrame)
- security_content = QWidget()
- form_s = QFormLayout(security_content)
- form_s.setContentsMargins(12, 12, 12, 12)
- form_s.setSpacing(10)
- security_scroll.setWidget(security_content)
-
- security_layout = QVBoxLayout(self.tab_security)
- security_layout.setContentsMargins(0, 0, 0, 0)
- security_layout.addWidget(security_scroll)
-
- self.tgl_encrypt = ToggleSwitch(checked=bool(settings.get("encrypt_data", False)))
- form_s.addRow("Panoyu ve notları şifrele (AES-256)", self.tgl_encrypt)
-
- # Google Authenticator (TOTP)
- form_s.addRow(QLabel("")) # Boşluk
- totp_header = QLabel("📱 Google Authenticator (TOTP)")
- form_s.addRow(totp_header)
-
- # TOTP durum etiketi
- self.lbl_totp_status = QLabel("⏳ Kontrol ediliyor...")
- form_s.addRow("Durum:", self.lbl_totp_status)
-
- # TOTP kurulum/kaldır butonu
- totp_btn_layout = QHBoxLayout()
- self.btn_totp_setup = QPushButton("🔐 2FA Kur")
- self.btn_totp_setup.clicked.connect(self._setup_totp)
- totp_btn_layout.addWidget(self.btn_totp_setup)
-
- self.btn_totp_remove = QPushButton("🗑️ 2FA Kaldır")
- self.btn_totp_remove.clicked.connect(self._remove_totp)
- self.btn_totp_remove.hide()
- totp_btn_layout.addWidget(self.btn_totp_remove)
-
- self.btn_totp_test = QPushButton("🧪 Test Et")
- self.btn_totp_test.clicked.connect(self._test_totp)
- self.btn_totp_test.hide()
- totp_btn_layout.addWidget(self.btn_totp_test)
-
- totp_btn_layout.addStretch()
- form_s.addRow("", totp_btn_layout)
-
- # TOTP seçenekleri
- self.tgl_totp_on_startup = ToggleSwitch(checked=bool(settings.get("totp_on_startup", False)))
- self.tgl_totp_on_startup.setEnabled(False)
- form_s.addRow("Uygulama açılışında kod iste", self.tgl_totp_on_startup)
-
- self.tgl_totp_hourly_lock = ToggleSwitch(checked=bool(settings.get("totp_hourly_lock", False)))
- self.tgl_totp_hourly_lock.setEnabled(False)
- form_s.addRow("Her saat başı kod iste", self.tgl_totp_hourly_lock)
-
- self.tgl_totp_for_sensitive = ToggleSwitch(checked=bool(settings.get("totp_for_sensitive", True)))
- self.tgl_totp_for_sensitive.setEnabled(False)
- form_s.addRow("Hassas verileri göstermek için kod iste", self.tgl_totp_for_sensitive)
-
- self.lbl_totp_info = QLabel("ℹ️ Google Authenticator uygulamasıyla ek güvenlik katmanı ekleyin.")
- self.lbl_totp_info.setWordWrap(True)
- self.lbl_totp_info.setStyleSheet("color: #666; font-size: 10px;")
- form_s.addRow("", self.lbl_totp_info)
-
- # TOTP durumunu kontrol et (defer)
- # self._check_totp_status() will be called in showEvent
-
- # Hassas Veri Koruması
- form_s.addRow(QLabel("")) # Boşluk
- sensitive_header = QLabel("🛡️ Hassas Veri Koruması")
- form_s.addRow(sensitive_header)
-
- self.tgl_sensitive_detection = ToggleSwitch(checked=bool(settings.get("sensitive_data_detection", True)))
- form_s.addRow("Hassas veri algılamayı aktifleştir", self.tgl_sensitive_detection)
-
- self.tgl_mask_credit_cards = ToggleSwitch(checked=bool(settings.get("mask_credit_cards", True)))
- form_s.addRow("Kredi kartlarını maskele", self.tgl_mask_credit_cards)
-
- self.tgl_mask_passwords = ToggleSwitch(checked=bool(settings.get("mask_passwords", True)))
- form_s.addRow("Şifreleri maskele", self.tgl_mask_passwords)
-
- self.tgl_mask_api_keys = ToggleSwitch(checked=bool(settings.get("mask_api_keys", True)))
- form_s.addRow("API anahtarlarını maskele", self.tgl_mask_api_keys)
-
- self.tgl_mask_emails = ToggleSwitch(checked=bool(settings.get("mask_emails", False)))
- form_s.addRow("E-posta adreslerini maskele", self.tgl_mask_emails)
-
- self.tgl_mask_phones = ToggleSwitch(checked=bool(settings.get("mask_phones", False)))
- form_s.addRow("Telefon numaralarını maskele", self.tgl_mask_phones)
-
- self.tgl_mask_tc_ids = ToggleSwitch(checked=bool(settings.get("mask_tc_ids", True)))
- form_s.addRow("TC kimlik no maskele", self.tgl_mask_tc_ids)
-
- self.tgl_mask_ibans = ToggleSwitch(checked=bool(settings.get("mask_ibans", True)))
- form_s.addRow("IBAN numaralarını maskele", self.tgl_mask_ibans)
-
- self.tgl_block_sensitive = ToggleSwitch(checked=bool(settings.get("block_sensitive_data", False)))
- form_s.addRow("Hassas veriyi hiç kaydetme", self.tgl_block_sensitive)
-
- self.cmb_auto_clear = QComboBox()
- self.cmb_auto_clear.addItem("Kapalı", 0)
- self.cmb_auto_clear.addItem("15 saniye", 15)
- self.cmb_auto_clear.addItem("30 saniye", 30)
- self.cmb_auto_clear.addItem("60 saniye", 60)
- cur_clear = int(settings.get("auto_clear_clipboard_seconds", 30) or 0)
- idx_clear = self.cmb_auto_clear.findData(cur_clear)
- self.cmb_auto_clear.setCurrentIndex(idx_clear if idx_clear >= 0 else 2)
- form_s.addRow("Hassas veriyi panodan temizle", self.cmb_auto_clear)
-
- self.tgl_exclude_apps = ToggleSwitch(checked=bool(settings.get("exclude_apps_enabled", True)))
- form_s.addRow("Hariç tutulan uygulamaları atla", self.tgl_exclude_apps)
- self.txt_excluded_apps = QLineEdit(str(settings.get("excluded_apps", "")))
- self.txt_excluded_apps.setPlaceholderText("keepass.exe,1password.exe,...")
- form_s.addRow("Hariç tutulan .exe listesi", self.txt_excluded_apps)
-
- self.tgl_windows_hello = ToggleSwitch(checked=bool(settings.get("windows_hello_enabled", False)))
- form_s.addRow("Windows Hello / biyometrik kilit", self.tgl_windows_hello)
- self.tgl_bio_startup = ToggleSwitch(checked=bool(settings.get("biometric_lock_on_startup", False)))
- form_s.addRow("Başlangıçta kilitle", self.tgl_bio_startup)
- self.spn_bio_timeout = QSpinBox()
- self.spn_bio_timeout.setRange(0, 240)
- self.spn_bio_timeout.setSuffix(" dk (0=kapalı)")
- self.spn_bio_timeout.setValue(int(settings.get("biometric_lock_timeout", 15) or 0))
- form_s.addRow("Hareketsizlikte kilitle", self.spn_bio_timeout)
-
- self.lbl_sensitive_info = QLabel("ℹ️ Hassas veriler otomatik maskelenir veya tamamen engellenir.")
- self.lbl_sensitive_info.setWordWrap(True)
- self.lbl_sensitive_info.setStyleSheet("color: #666; font-size: 10px;")
- form_s.addRow("", self.lbl_sensitive_info)
-
- self.tgl_auto_delete = ToggleSwitch(checked=bool(settings.get("auto_delete_enabled", False)))
- self.cmb_auto_delete = QComboBox()
- for d in [7, 10, 14, 30, 60, 90, 120, 180, 365]:
- self.cmb_auto_delete.addItem(f"{d} gün", d)
- self.cmb_auto_delete.setCurrentIndex([7, 10, 14, 30, 60, 90, 120, 180, 365].index(settings.get("auto_delete_days", 7)))
- self.cmb_auto_delete.setEnabled(self.tgl_auto_delete.isChecked())
- form_s.addRow("Otomatik silme", self.tgl_auto_delete)
- form_s.addRow("Silme süresi", self.cmb_auto_delete)
-
- self.tgl_keep_fav = ToggleSwitch(checked=bool(settings.get("auto_delete_keep_fav", True)))
- form_s.addRow("Favoriler silinmesin", self.tgl_keep_fav)
-
- # Resimleri harici klasöre kaydetme
- self.tgl_save_images_externally = ToggleSwitch(checked=bool(settings.get("save_images_externally", False)))
- form_s.addRow("Resimleri ayrı klasöre kaydet", self.tgl_save_images_externally)
-
- img_path_layout = QHBoxLayout()
- self.txt_external_images_path = QLineEdit()
- self.txt_external_images_path.setText(settings.get("external_images_path", ""))
- self.txt_external_images_path.setEnabled(self.tgl_save_images_externally.isChecked())
- self.btn_browse_images_path = QPushButton("...")
- self.btn_browse_images_path.setMaximumWidth(40)
- self.btn_browse_images_path.setEnabled(self.tgl_save_images_externally.isChecked())
- self.btn_browse_images_path.clicked.connect(self._browse_images_folder)
- img_path_layout.addWidget(self.txt_external_images_path, 1)
- img_path_layout.addWidget(self.btn_browse_images_path)
- form_s.addRow("Resim klasörü", img_path_layout)
-
- self.lbl_images_warning = QLabel("⚠️ Bu ayar açıksa resimler şifrelenmeyecek ve klasöre doğrudan kaydedilecektir.")
- self.lbl_images_warning.setWordWrap(True)
- self.lbl_images_warning.setStyleSheet("color: orange; font-size: 10px;")
- self.lbl_images_warning.setVisible(self.tgl_save_images_externally.isChecked())
- form_s.addRow("", self.lbl_images_warning)
-
- # OCR (Optik Karakter Tanıma) Ayarları
- form_s.addRow(QLabel("")) # Boşluk
- ocr_header = QLabel("🔍 OCR (Optik Karakter Tanıma)")
- form_s.addRow(ocr_header)
-
- self.tgl_ocr = ToggleSwitch(checked=bool(settings.get("ocr_enabled", False)))
- form_s.addRow("Resimlerdeki yazıları tanı", self.tgl_ocr)
-
- self.cmb_ocr_language = QComboBox()
- self.cmb_ocr_language.addItem("Türkçe + İngilizce", "tur+eng")
- self.cmb_ocr_language.addItem("Sadece Türkçe", "tur")
- self.cmb_ocr_language.addItem("Sadece İngilizce", "eng")
- self.cmb_ocr_language.addItem("Tüm Diller", "")
- ocr_lang = settings.get("ocr_language", "tur+eng")
- idx = self.cmb_ocr_language.findData(ocr_lang)
- if idx >= 0:
- self.cmb_ocr_language.setCurrentIndex(idx)
- self.cmb_ocr_language.setEnabled(self.tgl_ocr.isChecked())
- form_s.addRow("OCR Dili", self.cmb_ocr_language)
-
- ocr_path_layout = QHBoxLayout()
- self.txt_tesseract_path = QLineEdit()
- self.txt_tesseract_path.setText(settings.get("tesseract_path", ""))
- self.txt_tesseract_path.setPlaceholderText("Boş bırakılırsa otomatik bulunur")
- self.txt_tesseract_path.setEnabled(self.tgl_ocr.isChecked())
- self.btn_browse_tesseract = QPushButton("...")
- self.btn_browse_tesseract.setMaximumWidth(40)
- self.btn_browse_tesseract.setEnabled(self.tgl_ocr.isChecked())
- self.btn_browse_tesseract.clicked.connect(self._browse_tesseract_path)
- ocr_path_layout.addWidget(self.txt_tesseract_path, 1)
- ocr_path_layout.addWidget(self.btn_browse_tesseract)
- form_s.addRow("Tesseract Yolu", ocr_path_layout)
-
- self.lbl_ocr_info = QLabel("ℹ️ Tesseract OCR kurulu değilse buradan indirebilirsiniz.")
- self.lbl_ocr_info.setWordWrap(True)
- self.lbl_ocr_info.setOpenExternalLinks(True)
- self.lbl_ocr_info.setStyleSheet("color: #666; font-size: 10px;")
- self.lbl_ocr_info.setVisible(self.tgl_ocr.isChecked())
- form_s.addRow("", self.lbl_ocr_info)
-
- def _on_auto_delete_toggle(val):
- self.cmb_auto_delete.setEnabled(val)
- self.tgl_auto_delete.onToggled(_on_auto_delete_toggle)
-
- def _on_ocr_toggle(val):
- self.cmb_ocr_language.setEnabled(val)
- self.txt_tesseract_path.setEnabled(val)
- self.btn_browse_tesseract.setEnabled(val)
- self.lbl_ocr_info.setVisible(val)
- self.tgl_ocr.onToggled(_on_ocr_toggle)
-
- def _on_save_images_externally_toggle(val):
- self.txt_external_images_path.setEnabled(val)
- self.btn_browse_images_path.setEnabled(val)
- self.lbl_images_warning.setVisible(val)
- self.tgl_save_images_externally.onToggled(_on_save_images_externally_toggle)
-
- # Video Tab with scroll
- scroll_video = QScrollArea()
- scroll_video.setWidgetResizable(True)
- scroll_video.setFrameShape(QFrame.Shape.NoFrame)
-
- video_content = QWidget()
- form_v = QFormLayout(video_content)
- form_v.setContentsMargins(12, 12, 12, 12)
- form_v.setSpacing(10)
-
- # Video Kalitesi
- video_header = QLabel("📹 Video Kayıt Ayarları")
- form_v.addRow(video_header)
-
- # FFmpeg durum bilgisi (lazy - defer to after dialog shows)
- self._ffmpeg_status_label = QLabel("⏳ FFmpeg kontrol ediliyor...")
- self._ffmpeg_status_label.setStyleSheet("color: #888; padding: 6px;")
- form_v.addRow(self._ffmpeg_status_label)
- self._form_v = form_v # keep ref for later
-
- # Kaydetme yolu
- save_path_layout = QHBoxLayout()
- self.txt_video_save_path = QLineEdit()
- default_path = str(Path.home() / "Videos" / "ClipStack")
- self.txt_video_save_path.setText(settings.get("video_save_path", default_path))
- self.txt_video_save_path.setReadOnly(True)
- save_path_layout.addWidget(self.txt_video_save_path)
-
- self.btn_browse_video_path = QPushButton("📂")
- self.btn_browse_video_path.setFixedWidth(40)
- self.btn_browse_video_path.setToolTip("Kaydetme klasörünü seç")
- self.btn_browse_video_path.clicked.connect(self._browse_video_save_path)
- save_path_layout.addWidget(self.btn_browse_video_path)
-
- form_v.addRow("Kaydetme Yolu:", save_path_layout)
-
- self.cmb_video_quality = QComboBox()
- self.cmb_video_quality.addItem("🎬 4K (3840x2160)", "4K")
- self.cmb_video_quality.addItem("📺 1440p (2K)", "1440p")
- self.cmb_video_quality.addItem("✨ 1080p (Full HD)", "1080p")
- self.cmb_video_quality.addItem("📹 720p (HD)", "720p")
- self.cmb_video_quality.addItem("📱 480p (SD)", "480p")
- video_quality = settings.get("video_quality", "1080p")
- idx = self.cmb_video_quality.findData(video_quality)
- if idx >= 0:
- self.cmb_video_quality.setCurrentIndex(idx)
- form_v.addRow("Çözünürlük:", self.cmb_video_quality)
-
- # FPS - FFmpeg GPU ile 60 FPS destekleniyor
- self.cmb_video_fps = QComboBox()
- self.cmb_video_fps.addItem("📹 30 FPS (Standart)", 30)
- self.cmb_video_fps.addItem("🎮 60 FPS (Akıcı)", 60)
- self.cmb_video_fps.addItem("🐌 15 FPS (Düşük)", 15)
- video_fps = settings.get("video_fps", 30)
- idx_fps = self.cmb_video_fps.findData(video_fps)
- if idx_fps >= 0:
- self.cmb_video_fps.setCurrentIndex(idx_fps)
- else:
- self.cmb_video_fps.setCurrentIndex(0) # 30 FPS varsayılan
- form_v.addRow("Kare Hızı (FPS):", self.cmb_video_fps)
-
- # Bitrate - GPU encoder için daha yüksek değerler
- self.spn_video_bitrate = QSpinBox()
- self.spn_video_bitrate.setRange(1000, 50000)
- self.spn_video_bitrate.setValue(int(settings.get("video_bitrate", 8000)))
- self.spn_video_bitrate.setSuffix(" kbps")
- self.spn_video_bitrate.setSingleStep(1000)
- form_v.addRow("Bit Hızı:", self.spn_video_bitrate)
-
- # Mikrofon
- form_v.addRow(QLabel(""))
- mic_header = QLabel("🎤 Mikrofon Ayarları")
- form_v.addRow(mic_header)
-
- self.tgl_record_mic = ToggleSwitch(checked=bool(settings.get("video_record_mic", False)))
- form_v.addRow("Mikrofon kaydet:", self.tgl_record_mic)
-
- self.cmb_microphone = QComboBox()
- self.cmb_microphone.addItem("Varsayılan Mikrofon", "default")
- mic_device = settings.get("video_microphone", "default")
- idx = self.cmb_microphone.findData(mic_device)
- if idx >= 0:
- self.cmb_microphone.setCurrentIndex(idx)
- self.cmb_microphone.setEnabled(self.tgl_record_mic.isChecked())
- form_v.addRow("Mikrofon:", self.cmb_microphone)
-
- # Hotkeys
- form_v.addRow(QLabel(""))
- hotkey_header = QLabel("⌨️ Kısayol Tuşları")
- form_v.addRow(hotkey_header)
-
- self.txt_hotkey_video_record = KeyCaptureLineEdit()
- self.txt_hotkey_video_record.setText(KeyCaptureLineEdit.normalize_combo(str(settings.get("hotkey_video_record", ""))))
- self.btn_clear_hk_video_record = QPushButton()
- try:
- reset_icon = self.style().standardIcon(QStyle.StandardPixmap.SP_LineEditClearButton)
- self.btn_clear_hk_video_record.setIcon(reset_icon)
- self.btn_clear_hk_video_record.setIconSize(QSize(18, 18))
- self.btn_clear_hk_video_record.setCursor(Qt.CursorShape.PointingHandCursor)
- except Exception:
- pass
- hk_video_record_row = QHBoxLayout()
- hk_video_record_row.addWidget(self.txt_hotkey_video_record, 1)
- hk_video_record_row.addWidget(self.btn_clear_hk_video_record)
- form_v.addRow("Kayıt Başlat/Durdur:", hk_video_record_row)
-
- self.txt_hotkey_instant_replay = KeyCaptureLineEdit()
- self.txt_hotkey_instant_replay.setText(KeyCaptureLineEdit.normalize_combo(str(settings.get("hotkey_instant_replay", ""))))
- self.btn_clear_hk_instant_replay = QPushButton()
- try:
- reset_icon2 = self.style().standardIcon(QStyle.StandardPixmap.SP_LineEditClearButton)
- self.btn_clear_hk_instant_replay.setIcon(reset_icon2)
- self.btn_clear_hk_instant_replay.setIconSize(QSize(18, 18))
- self.btn_clear_hk_instant_replay.setCursor(Qt.CursorShape.PointingHandCursor)
- except Exception:
- pass
- hk_instant_replay_row = QHBoxLayout()
- hk_instant_replay_row.addWidget(self.txt_hotkey_instant_replay, 1)
- hk_instant_replay_row.addWidget(self.btn_clear_hk_instant_replay)
- form_v.addRow("Instant Replay Kaydet:", hk_instant_replay_row)
-
- # Instant Replay
- form_v.addRow(QLabel(""))
- instant_header = QLabel("⏪ Instant Replay")
- form_v.addRow(instant_header)
-
- self.spn_replay_buffer = QSpinBox()
- self.spn_replay_buffer.setRange(15, 300)
- self.spn_replay_buffer.setValue(int(settings.get("instant_replay_buffer_seconds", 30)))
- self.spn_replay_buffer.setSuffix(" saniye")
- form_v.addRow("Buffer Süresi:", self.spn_replay_buffer)
-
- self.lbl_replay_info = QLabel("ℹ️ Instant Replay son N saniyeyi sürekli buffer'da tutar. Hotkey ile kaydettiğinizde son buffer kaydedilir.")
- self.lbl_replay_info.setWordWrap(True)
- self.lbl_replay_info.setStyleSheet("color: #666; font-size: 10px;")
- form_v.addRow("", self.lbl_replay_info)
-
- scroll_video.setWidget(video_content)
-
- video_layout = QVBoxLayout(self.tab_video)
- video_layout.setContentsMargins(0, 0, 0, 0)
- video_layout.addWidget(scroll_video)
-
- def _on_record_mic_toggle(val):
- self.cmb_microphone.setEnabled(val)
- self.tgl_record_mic.onToggled(_on_record_mic_toggle)
- self.btn_clear_hk_video_record.clicked.connect(lambda: self.txt_hotkey_video_record.clear())
- self.btn_clear_hk_instant_replay.clicked.connect(lambda: self.txt_hotkey_instant_replay.clear())
-
- lay_t = QVBoxLayout(self.tab_tray)
- lay_t.setContentsMargins(12, 12, 12, 12)
- lay_t.setSpacing(10)
-
- form_t = QFormLayout()
- self.cmb_tray = QComboBox()
- # 10 farklı tasarımlı ikon
- tray_icons = [
- ("📋 Klasik Pano", "assets/icons/tray/tray1.svg"),
- ("📄 Kağıt Yığını", "assets/icons/tray/tray2.svg"),
- ("📝 Kopyalama Oku", "assets/icons/tray/tray3.svg"),
- ("💾 Bellek Çipi", "assets/icons/tray/tray4.svg"),
- ("📁 Klasör", "assets/icons/tray/tray5.svg"),
- ("📌 Raptiyeli Not", "assets/icons/tray/tray6.svg"),
- ("✨ Sihirli Pano", "assets/icons/tray/tray7.svg"),
- ("☁️ Bulut Sync", "assets/icons/tray/tray8.svg"),
- ("🛡️ Güvenli Pano", "assets/icons/tray/tray9.svg"),
- ("⚡ Şimşek Hızı", "assets/icons/tray/tray10.svg"),
- ]
- for label, path in tray_icons:
- self.cmb_tray.addItem(label, path)
-
- # Tema varyantları
- tray_variants = [
- ("🌙 Midnight", "assets/icons/tray/tray_default.svg"),
- ("⚫ Carbon", "assets/icons/tray/tray_dark.svg"),
- ("⚪ Polar", "assets/icons/tray/tray_light.svg"),
- ("💜 Royal", "assets/icons/tray/tray_purple.svg"),
- ("🌆 Neon", "assets/icons/tray/tray_cyberpunk.svg"),
- ("🌅 Sunset", "assets/icons/tray/tray_sunset.svg"),
- ("💚 Matrix", "assets/icons/tray/tray_matrix.svg"),
- ("🌊 Ocean", "assets/icons/tray/tray_ocean.svg"),
- ("🎮 Retro", "assets/icons/tray/tray_retro.svg"),
- ("⬜ Minimal", "assets/icons/tray/tray_minimal.svg"),
- ]
- for label, rel in tray_variants:
- self.cmb_tray.addItem(label, rel)
- self.cmb_tray.addItem(self._tr("settings.tray.icon.custom", "Özel seç…"), "__custom__")
- sel = self.settings.get("tray_icon", "assets/icons/tray/tray1.svg")
- idx = self.cmb_tray.findData(sel)
- if idx < 0:
- idx = 0
- self.cmb_tray.setCurrentIndex(idx)
- form_t.addRow(self._tr("settings.tray.icon", "Tepsi ikonu"), self.cmb_tray)
-
- self.btn_preview = QPushButton()
- form_t.addRow("", self.btn_preview)
-
- self.tgl_tray_notifications = ToggleSwitch(checked=bool(settings.get("tray_notifications", True)))
- form_t.addRow(self._tr("settings.tray.notifications", "Tepsi bildirimlerini göster"), self.tgl_tray_notifications)
- lay_t.addLayout(form_t)
-
- form_r = QFormLayout(self.tab_reminders)
- form_r.setContentsMargins(12, 12, 12, 12)
- form_r.setSpacing(10)
-
- # Bildirim türü
- self.cmb_notification_type = QComboBox()
- self.cmb_notification_type.setMinimumHeight(36)
- self.cmb_notification_type.addItem(self._tr("settings.reminders.notif_system", "Sistem Bildirimi"), "system")
- self.cmb_notification_type.addItem(self._tr("settings.reminders.notif_app", "Uygulama Bildirimi"), "app")
- notif_type = settings.get("reminder_notification_type", "system")
- idx = self.cmb_notification_type.findData(notif_type)
- if idx >= 0:
- self.cmb_notification_type.setCurrentIndex(idx)
- form_r.addRow(self._tr("settings.reminders.notification_type", "Bildirim Türü:"), self.cmb_notification_type)
-
- # Popup göster
- self._last_app_popup_choice = bool(settings.get("reminder_show_popup", True))
- self.tgl_show_popup = ToggleSwitch(checked=self._last_app_popup_choice)
- self.tgl_show_popup.onToggled(self._on_show_popup_toggle)
- form_r.addRow(self._tr("settings.reminders.show_popup", "Popup pencere göster"), self.tgl_show_popup)
-
- # Ses etkin
- self.tgl_sound = ToggleSwitch(checked=bool(settings.get("reminder_sound_enabled", True)))
- form_r.addRow(self._tr("settings.reminders.sound_enabled", "Bildirim sesi çal"), self.tgl_sound)
-
- # Ses dosyası seçimi
- sound_layout = QHBoxLayout()
- sound_layout.setSpacing(8)
- sound_layout.setContentsMargins(0, 0, 0, 0)
- self.cmb_sound = QComboBox()
- self.cmb_sound.setMinimumHeight(36)
- self.cmb_sound.addItem(self._tr("settings.reminders.sound_default", "Varsayılan (Sistem)"), "default")
-
- from ..utils import resource_path
- sounds_dir = resource_path("assets/sounds")
- audio_exts = {".mp3", ".wav", ".ogg", ".m4a"}
- if sounds_dir.exists():
- for sound_path in sorted(sounds_dir.iterdir()):
- if not sound_path.is_file():
- continue
- if sound_path.suffix.lower() not in audio_exts:
- continue
- display_name = self._format_sound_label(sound_path)
- self.cmb_sound.addItem(display_name, str(sound_path))
-
- # Özel ses seçimi
- self.cmb_sound.addItem(self._tr("settings.reminders.sound_custom", "➕ Özel Ses Seç..."), "__custom__")
-
- # Mevcut ayarı yükle
- current_sound = settings.get("reminder_sound_file", "default")
- if current_sound and current_sound != "default":
- # Hazır seslerden biri mi kontrol et
- found = False
- for i in range(self.cmb_sound.count()):
- if self.cmb_sound.itemData(i) == current_sound:
- self.cmb_sound.setCurrentIndex(i)
- found = True
- break
-
- # Hazır ses değilse, özel ses olarak ekle
- if not found and current_sound != "__custom__":
- custom_index = self.cmb_sound.count() - 1 # "Özel Ses Seç..." öncesi
- self.cmb_sound.insertItem(custom_index, f"⭐ Özel: {Path(current_sound).name}", current_sound)
- self.cmb_sound.setCurrentIndex(custom_index)
- else:
- self.cmb_sound.setCurrentIndex(0) # Default
-
- self.btn_test_sound = QPushButton(self._tr("settings.reminders.test_sound", "Test"))
- self.btn_test_sound.setMinimumHeight(36)
- self.btn_test_sound.clicked.connect(self._test_reminder_sound)
- self.btn_test_sound.setCursor(Qt.PointingHandCursor)
- try:
- self.btn_test_sound.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
- self.btn_test_sound.setIconSize(QSize(20, 20))
- except Exception:
- pass
- self.btn_test_sound.setProperty("class", "accent")
-
- sound_layout.addWidget(self.cmb_sound, 1)
- sound_layout.addWidget(self.btn_test_sound)
- form_r.addRow(self._tr("settings.reminders.sound_file", "Ses Dosyası:"), sound_layout)
-
- # Ses dosyası değişimini dinle
- self.cmb_sound.currentIndexChanged.connect(self._on_sound_select)
- self.cmb_notification_type.currentIndexChanged.connect(self._on_notification_type_change)
-
- # Otomatik erteleme
- self.tgl_auto_snooze = ToggleSwitch(checked=bool(settings.get("reminder_auto_snooze", False)))
- form_r.addRow(self._tr("settings.reminders.auto_snooze", "Otomatik erteleme"), self.tgl_auto_snooze)
-
- # Erteleme süresi
- self.spn_snooze_minutes = QSpinBox()
- self.spn_snooze_minutes.setRange(1, 60)
- self.spn_snooze_minutes.setValue(int(settings.get("reminder_snooze_minutes", 5)))
- self.spn_snooze_minutes.setSuffix(" " + self._tr("common.minutes", "dakika"))
- self.spn_snooze_minutes.setEnabled(self.tgl_auto_snooze.isChecked())
- form_r.addRow(self._tr("settings.reminders.snooze_duration", "Erteleme süresi:"), self.spn_snooze_minutes)
-
- def _on_auto_snooze_toggle(val):
- self.spn_snooze_minutes.setEnabled(val)
- self.tgl_auto_snooze.onToggled(_on_auto_snooze_toggle)
-
- # Ses kontrolü
- def _on_sound_toggle(val):
- self.cmb_sound.setEnabled(val)
- self.btn_test_sound.setEnabled(val)
- self.tgl_sound.onToggled(_on_sound_toggle)
-
- # ==================== SENKRONİZASYON SEKMESİ ====================
- sync_scroll = QScrollArea(self.tab_sync)
- sync_scroll.setWidgetResizable(True)
- sync_scroll.setFrameShape(QFrame.Shape.NoFrame)
- sync_content = QWidget()
- form_sync = QFormLayout(sync_content)
- form_sync.setContentsMargins(12, 12, 12, 12)
- form_sync.setSpacing(10)
- sync_scroll.setWidget(sync_content)
-
- sync_layout = QVBoxLayout(self.tab_sync)
- sync_layout.setContentsMargins(0, 0, 0, 0)
- sync_layout.addWidget(sync_scroll)
-
- # Export/Import Başlık
- export_header = QLabel("📦 Veri Aktarımı")
- form_sync.addRow(export_header)
-
- # Export butonları
- export_btns = QHBoxLayout()
- export_btns.setSpacing(8)
-
- self.btn_export_json = QPushButton("📤 JSON Olarak Dışa Aktar")
- self.btn_export_json.setMinimumHeight(36)
- self.btn_export_json.setCursor(Qt.PointingHandCursor)
- self.btn_export_json.clicked.connect(self._export_to_json)
- export_btns.addWidget(self.btn_export_json)
-
- self.btn_import_json = QPushButton("📥 JSON'dan İçe Aktar")
- self.btn_import_json.setMinimumHeight(36)
- self.btn_import_json.setCursor(Qt.PointingHandCursor)
- self.btn_import_json.clicked.connect(self._import_from_json)
- export_btns.addWidget(self.btn_import_json)
-
- form_sync.addRow("", export_btns)
-
- # Export Info
- export_info = QLabel("ℹ️ JSON formatında tüm pano geçmişi, notlar, hatırlatmalar ve snippet'ler dışa aktarılır.")
- export_info.setWordWrap(True)
- export_info.setStyleSheet("color: #666; font-size: 11px;")
- form_sync.addRow("", export_info)
-
- # Ayırıcı
- form_sync.addRow(QLabel(""))
-
- # Cloud Sync Başlık
- cloud_header = QLabel("☁️ Bulut Senkronizasyonu")
- form_sync.addRow(cloud_header)
-
- # Google Drive
- gdrive_layout = QHBoxLayout()
- gdrive_layout.setSpacing(8)
-
- self.btn_gdrive_connect = QPushButton("🔗 Google Drive Bağla")
- self.btn_gdrive_connect.setMinimumHeight(36)
- self.btn_gdrive_connect.setCursor(Qt.PointingHandCursor)
- self.btn_gdrive_connect.clicked.connect(self._connect_google_drive)
- gdrive_layout.addWidget(self.btn_gdrive_connect)
-
- self.lbl_gdrive_status = QLabel("❌ Bağlı değil")
- self.lbl_gdrive_status.setStyleSheet("color: #888;")
- gdrive_layout.addWidget(self.lbl_gdrive_status)
- gdrive_layout.addStretch()
-
- form_sync.addRow("Google Drive:", gdrive_layout)
-
- # Otomatik sync
- self.tgl_auto_sync = ToggleSwitch(checked=bool(settings.get("auto_sync_enabled", False)))
- self.tgl_auto_sync.setEnabled(False) # Bağlı değilken devre dışı
- form_sync.addRow("Otomatik senkronizasyon:", self.tgl_auto_sync)
-
- # Sync sıklığı
- self.cmb_sync_interval = QComboBox()
- self.cmb_sync_interval.setMinimumHeight(36)
- self.cmb_sync_interval.addItem("Her 5 dakikada", 5)
- self.cmb_sync_interval.addItem("Her 15 dakikada", 15)
- self.cmb_sync_interval.addItem("Her 30 dakikada", 30)
- self.cmb_sync_interval.addItem("Her saat", 60)
- self.cmb_sync_interval.setEnabled(False)
- sync_interval = settings.get("sync_interval_minutes", 15)
- idx = self.cmb_sync_interval.findData(sync_interval)
- if idx >= 0:
- self.cmb_sync_interval.setCurrentIndex(idx)
- form_sync.addRow("Senkronizasyon sıklığı:", self.cmb_sync_interval)
-
- # Son sync
- self.lbl_last_sync = QLabel("Son senkronizasyon: Hiç")
- self.lbl_last_sync.setStyleSheet("color: #888; font-size: 11px;")
- form_sync.addRow("", self.lbl_last_sync)
-
- # Manuel sync
- self.btn_sync_now = QPushButton("🔄 Şimdi Senkronize Et")
- self.btn_sync_now.setMinimumHeight(36)
- self.btn_sync_now.setEnabled(False)
- self.btn_sync_now.setCursor(Qt.PointingHandCursor)
- self.btn_sync_now.clicked.connect(self._sync_now)
- form_sync.addRow("", self.btn_sync_now)
-
- # Ayırıcı
- form_sync.addRow(QLabel(""))
-
- # Paylaşım Başlık
- share_header = QLabel("🔗 Paylaşım")
- form_sync.addRow(share_header)
-
- # Paylaşım sunucusu
- self.txt_share_server = QLineEdit()
- self.txt_share_server.setPlaceholderText("https://taxclip.com")
- self.txt_share_server.setText(settings.get("share_server_url", ""))
- self.txt_share_server.setMinimumHeight(36)
- form_sync.addRow("Paylaşım sunucusu:", self.txt_share_server)
-
- share_info = QLabel("ℹ️ Paylaşım özelliği için sunucu URL'si. Boş bırakılırsa paylaşım devre dışı kalır.")
- share_info.setWordWrap(True)
- share_info.setStyleSheet("color: #666; font-size: 11px;")
- form_sync.addRow("", share_info)
-
- # Google Drive durumunu kontrol et (defer)
- # self._check_gdrive_status() will be called in showEvent
-
- lay_ab = QVBoxLayout(self.tab_about)
- lay_ab.setContentsMargins(14, 14, 14, 14)
- lay_ab.setSpacing(8)
-
- self.lbl_title = QLabel()
- self.lbl_desc = QLabel()
- self.lbl_desc.setWordWrap(True)
- self.lbl_ai_badge = QLabel()
- self.lbl_ai_badge.setWordWrap(True)
- lay_ab.addWidget(self.lbl_title)
- lay_ab.addWidget(self.lbl_desc)
- lay_ab.addWidget(self.lbl_ai_badge)
-
- # Versiyon bilgisi ve güncelleme kontrolü
- from ..updater import get_current_version
- version_layout = QHBoxLayout()
- version_layout.setSpacing(10)
-
- self.lbl_version = QLabel(f"📦 Versiyon: v{get_current_version()}")
- self.lbl_version.setStyleSheet("font-size: 11pt; font-weight: bold;")
- version_layout.addWidget(self.lbl_version)
-
- self.btn_check_update = QPushButton("🔄 Güncelleme Kontrol Et")
- self.btn_check_update.setMinimumHeight(32)
- self.btn_check_update.setCursor(Qt.PointingHandCursor)
- self.btn_check_update.clicked.connect(self._check_for_updates)
- version_layout.addWidget(self.btn_check_update)
-
- self.lbl_update_status = QLabel("")
- self.lbl_update_status.setStyleSheet("color: #888;")
- version_layout.addWidget(self.lbl_update_status)
- version_layout.addStretch()
-
- lay_ab.addLayout(version_layout)
-
- links = QHBoxLayout()
- links.setSpacing(8)
- self.btn_site = QPushButton()
- self.btn_site.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://miyotu.com/")))
- self.btn_pat = QPushButton()
- self.btn_pat.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://www.patreon.com/c/Taxperia")))
- self.btn_coffee = QPushButton("☕ Buy Me a Coffee")
- self.btn_coffee.setCursor(Qt.PointingHandCursor)
- self.btn_coffee.setStyleSheet("""
- QPushButton {
- background-color: #FFDD00;
- color: #000000;
- border: none;
- border-radius: 6px;
- padding: 8px 16px;
- font-weight: bold;
- }
- QPushButton:hover {
- background-color: #FFE433;
- }
- """)
- self.btn_coffee.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://www.buymeacoffee.com/taxperia")))
- links.addWidget(self.btn_site)
- links.addWidget(self.btn_pat)
- links.addWidget(self.btn_coffee)
- links.addStretch(1)
- lay_ab.addLayout(links)
-
- self.grp_authors = QGroupBox()
- la = QVBoxLayout(self.grp_authors)
- la.setContentsMargins(5, 20, 5, 10)
- la.setSpacing(6)
- self.dev = QLabel("• Developer: Taxperia")
- self.des = QLabel("• Designer: Miyotu")
- self.dev.setWordWrap(True)
- self.des.setWordWrap(True)
- la.addWidget(self.dev)
- la.addWidget(self.des)
- lay_ab.addWidget(self.grp_authors)
- lay_ab.addStretch(1)
-
- btns = QHBoxLayout()
- btns.addStretch(1)
- self.btn_apply = QPushButton()
- self.btn_cancel = QPushButton()
- self.btn_ok = QPushButton()
- btns.addWidget(self.btn_apply)
- btns.addWidget(self.btn_cancel)
- btns.addWidget(self.btn_ok)
- root.addLayout(btns)
-
- self.btn_apply.clicked.connect(self._apply_and_emit)
- self.btn_ok.clicked.connect(self._apply_and_close)
- self.btn_cancel.clicked.connect(self.reject)
- self.cmb_tray.currentIndexChanged.connect(self._on_tray_select)
- self.btn_clear_hk.clicked.connect(lambda: self.txt_hotkey.setText("ctrl+shift+v"))
- self.btn_clear_hk_paste.clicked.connect(lambda: self.txt_hotkey_paste.setText(""))
- self.btn_clear_hk_quick_note.clicked.connect(lambda: self.txt_hotkey_quick_note.setText(""))
- self.btn_clear_hk_screenshot.clicked.connect(lambda: self.txt_hotkey_screenshot.setText(""))
- self.btn_clear_hk_ocr.clicked.connect(lambda: self.txt_hotkey_ocr.setText(""))
- self.btn_clear_hk_snip.clicked.connect(lambda: self.txt_hotkey_snip.setText(""))
- self.btn_preview.clicked.connect(self._preview_tray_icon)
-
- i18n.languageChanged.connect(self.refresh_texts)
- self._update_show_popup_state()
- self.refresh_texts()
-
- # Defer heavy checks to after dialog is visible
- self._deferred_inited = False
- self._gdrive_status_thread: GoogleDriveStatusThread | None = None
- self._video_probe_thread: VideoProbeThread | None = None
-
- def showEvent(self, event):
- super().showEvent(event)
- if not self._deferred_inited:
- self._deferred_inited = True
- QTimer.singleShot(50, self._run_deferred_init)
-
- def _run_deferred_init(self):
- """Heavy checks deferred to after dialog is visible."""
- self._check_totp_status()
- self._check_gdrive_status_async()
- self._check_ffmpeg_deferred()
-
- def _check_ffmpeg_deferred(self):
- """FFmpeg check deferred to after dialog is shown."""
- if self._video_probe_thread and self._video_probe_thread.isRunning():
- return
-
- self._ffmpeg_status_label.setText("⏳ FFmpeg ve mikrofonlar kontrol ediliyor...")
- self._ffmpeg_status_label.setStyleSheet("color: #888; padding: 6px;")
-
- thread = VideoProbeThread(self.settings, self)
- thread.probe_ready.connect(self._apply_video_probe_result)
- thread.probe_failed.connect(self._apply_video_probe_failure)
- thread.finished.connect(thread.deleteLater)
- self._video_probe_thread = thread
- thread.start()
-
- def _reload_microphone_devices(self, device_names=None):
- """FFmpeg ile görülen mikrofonları ayarlar ekranına doldur."""
- current_value = self.cmb_microphone.currentData() or self.settings.get("video_microphone", "default")
- self.cmb_microphone.clear()
- self.cmb_microphone.addItem("Varsayılan Mikrofon", "default")
-
- seen = set()
- for device_name in device_names or []:
- if not device_name or device_name in seen:
- continue
- seen.add(device_name)
- self.cmb_microphone.addItem(device_name, device_name)
-
- idx = self.cmb_microphone.findData(current_value)
- if idx < 0 and current_value and current_value != "default":
- self.cmb_microphone.addItem(f"Özel cihaz: {current_value}", current_value)
- idx = self.cmb_microphone.findData(current_value)
- self.cmb_microphone.setCurrentIndex(max(idx, 0))
- self.cmb_microphone.setEnabled(self.tgl_record_mic.isChecked())
-
- def _apply_video_probe_result(self, available: bool, encoder_info: str, device_names: list):
- if available:
- self._ffmpeg_status_label.setText(f"✅ FFmpeg: Hazır | Encoder: {encoder_info}")
- self._ffmpeg_status_label.setStyleSheet("color: #27ae60; font-weight: bold; padding: 6px; background: rgba(39,174,96,0.1); border-radius: 4px;")
- else:
- self._ffmpeg_status_label.setText("⚠️ FFmpeg bulunamadı! Video kayıt çalışmayacak.\nİndirin: https://www.gyan.dev/ffmpeg/builds/")
- self._ffmpeg_status_label.setStyleSheet("color: #e74c3c; font-weight: bold; padding: 6px; background: rgba(231,76,60,0.1); border-radius: 4px;")
- self._ffmpeg_status_label.setWordWrap(True)
- self._ffmpeg_status_label.setOpenExternalLinks(True)
-
- self._reload_microphone_devices(device_names)
- self._video_probe_thread = None
-
- def _apply_video_probe_failure(self, error: str):
- print(f"[VIDEO] FFmpeg probe hatası: {error}")
- self._ffmpeg_status_label.setText("⚠️ FFmpeg durumu kontrol edilemedi")
- self._ffmpeg_status_label.setStyleSheet("color: #888; padding: 6px;")
- self._reload_microphone_devices([])
- self._video_probe_thread = None
-
- def _browse_images_folder(self):
- """Resim klasörü seç"""
- folder = QFileDialog.getExistingDirectory(
- self,
- self._tr("settings.security.choose_images_folder", "Resim klasörü seç"),
- self.txt_external_images_path.text() or str(Path.home())
- )
- if folder:
- self.txt_external_images_path.setText(folder)
- """Resim klasörü seç"""
- folder = QFileDialog.getExistingDirectory(
- self,
- self._tr("settings.security.choose_images_folder", "Resim klasörü seç"),
- self.txt_external_images_path.text() or str(Path.home())
- )
- if folder:
- self.txt_external_images_path.setText(folder)
-
- def _browse_tesseract_path(self):
- """Tesseract.exe dosyasını seç"""
- file, _ = QFileDialog.getOpenFileName(
- self,
- "Tesseract.exe dosyasını seç",
- self.txt_tesseract_path.text() or "C:/Program Files/Tesseract-OCR",
- "Executable Files (*.exe)"
- )
- if file:
- self.txt_tesseract_path.setText(file)
-
- def _browse_video_save_path(self):
- """Video kaydetme yolunu seç"""
- current_path = self.txt_video_save_path.text() or str(Path.home() / "Videos" / "ClipStack")
- path = QFileDialog.getExistingDirectory(
- self,
- "Video Kaydetme Klasörünü Seç",
- current_path
- )
- if path:
- self.txt_video_save_path.setText(path)
-
- def _check_totp_status(self):
- """TOTP durumunu kontrol et"""
- from ..totp_manager import TOTPManager
-
- totp = TOTPManager(self.settings)
-
- if not totp.is_available():
- self.lbl_totp_status.setText("❌ pyotp yüklü değil")
- self.lbl_totp_status.setStyleSheet("color: #f44336;")
- self.btn_totp_setup.setEnabled(False)
- self.lbl_totp_info.setText("ℹ️ Kurulum için: pip install pyotp qrcode[pil]")
- return
-
- if totp.is_enabled():
- self.lbl_totp_status.setText("✅ Aktif")
- self.lbl_totp_status.setStyleSheet("color: #4CAF50; font-weight: bold;")
- self.btn_totp_setup.hide()
- self.btn_totp_remove.show()
- self.btn_totp_test.show()
- self.tgl_totp_on_startup.setEnabled(True)
- self.tgl_totp_hourly_lock.setEnabled(True)
- self.tgl_totp_for_sensitive.setEnabled(True)
- else:
- self.lbl_totp_status.setText("❌ Kapalı")
- self.lbl_totp_status.setStyleSheet("color: #888;")
- self.btn_totp_setup.show()
- self.btn_totp_remove.hide()
- self.btn_totp_test.hide()
- self.tgl_totp_on_startup.setEnabled(False)
- self.tgl_totp_hourly_lock.setEnabled(False)
- self.tgl_totp_for_sensitive.setEnabled(False)
-
- def _setup_totp(self):
- """TOTP kurulumu başlat"""
- from .totp_dialog import TOTPSetupDialog
-
- dialog = TOTPSetupDialog(self.settings, self)
- dialog.setup_complete.connect(self._check_totp_status)
- dialog.exec()
-
- def _remove_totp(self):
- """TOTP'yi kaldır"""
- from ..totp_manager import TOTPManager
- from .totp_dialog import TOTPVerifyDialog
-
- # Önce doğrulama iste
- verify_dialog = TOTPVerifyDialog(self.settings, self, "2FA Kaldırma Onayı")
- if verify_dialog.exec() and verify_dialog.is_verified():
- totp = TOTPManager(self.settings)
- if totp.disable():
- QMessageBox.information(
- self,
- "Başarılı",
- "✅ İki faktörlü doğrulama kaldırıldı."
- )
- self._check_totp_status()
- else:
- QMessageBox.warning(
- self,
- "Hata",
- "❌ 2FA kaldırılamadı."
- )
-
- def _test_totp(self):
- """TOTP doğrulamasını test et"""
- from .totp_dialog import TOTPVerifyDialog
-
- dialog = TOTPVerifyDialog(self.settings, self, "2FA Test")
- if dialog.exec() and dialog.is_verified():
- QMessageBox.information(
- self,
- "Başarılı",
- "✅ Doğrulama başarılı!"
- )
-
- def _on_sound_select(self, idx: int):
- """Ses dosyası seçimi değişti"""
- data = self.cmb_sound.currentData()
- print(f"[SETTINGS] Ses seçimi değişti: idx={idx}, data={data}")
-
- if data == "__custom__":
- file, _ = QFileDialog.getOpenFileName(
- self,
- self._tr("settings.reminders.choose_sound", "Ses dosyası seç"),
- "",
- "Audio Files (*.mp3 *.wav *.ogg)"
- )
- if file:
- print(f"[SETTINGS] Özel ses seçildi: {file}")
- # Sonsuz döngüyü önlemek için signal'i geçici olarak kes
- self.cmb_sound.blockSignals(True)
-
- # "__custom__" itemını kaldır ve yeni özel ses olarak ekle
- custom_idx = self.cmb_sound.count() - 1
- self.cmb_sound.removeItem(custom_idx)
-
- # Özel sesi ekle
- self.cmb_sound.addItem(f"⭐ Özel: {Path(file).name}", file)
- # "__custom__" seçeneğini tekrar ekle
- self.cmb_sound.addItem(self._tr("settings.reminders.sound_custom", "➕ Özel Ses Seç..."), "__custom__")
-
- # Yeni eklenen özel sesi seç (sondan 2. item)
- self.cmb_sound.setCurrentIndex(self.cmb_sound.count() - 2)
-
- # Signal'i tekrar aç
- self.cmb_sound.blockSignals(False)
-
- # Ayarlara kaydet VE HEMEN DISKE YAZ
- self.settings.set("reminder_sound_file", file)
- self.settings.save() # ← HEMEN KAYDET!
- print(f"[SETTINGS] Ses kaydedildi ve diske yazıldı: {file}")
- else:
- print("[SETTINGS] Özel ses seçimi iptal edildi, default'a dönüyoruz")
- self.cmb_sound.blockSignals(True)
- self.cmb_sound.setCurrentIndex(0)
- self.cmb_sound.blockSignals(False)
-
- def _on_notification_type_change(self, idx: int):
- self._update_show_popup_state()
-
- def _on_show_popup_toggle(self, state: bool):
- if self.cmb_notification_type.currentData() == "app":
- self._last_app_popup_choice = state
-
- def _update_show_popup_state(self):
- notif_type = self.cmb_notification_type.currentData()
- is_app = notif_type == "app"
- self.tgl_show_popup.setEnabled(is_app)
- if not is_app:
- self.tgl_show_popup.setChecked(False)
- else:
- self.tgl_show_popup.setChecked(self._last_app_popup_choice)
-
- @staticmethod
- def _format_sound_label(sound_path: Path) -> str:
- name = sound_path.stem.replace("_", " ").replace("-", " ").strip()
- if name:
- name = " ".join(part.capitalize() for part in name.split())
- else:
- name = sound_path.name
- return f"🔔 {name}"
-
- def _test_reminder_sound(self):
- """Bildirim sesini test et"""
- try:
- sound_file = self.cmb_sound.currentData()
- current_text = self.cmb_sound.currentText()
- current_index = self.cmb_sound.currentIndex()
-
- print(f"[SETTINGS TEST] Test ediliyor:")
- print(f" - Index: {current_index}")
- print(f" - Text: {current_text}")
- print(f" - Data: {sound_file}")
-
- if sound_file == "default" or not sound_file or sound_file == "":
- # Windows sistem sesi
- import winsound
- winsound.MessageBeep(winsound.MB_ICONASTERISK)
- print("[SETTINGS TEST] Windows default ses çalındı")
- elif sound_file == "__custom__":
- # Özel dosya seçilmemiş
- print("[SETTINGS TEST] UYARI: Özel ses seçilmemiş!")
- QMessageBox.information(
- self,
- "Bilgi",
- "Lütfen önce bir ses dosyası seçin."
- )
- else:
- # Özel ses dosyası
- from pathlib import Path
- sound_path = Path(sound_file)
-
- print(f"[SETTINGS TEST] Ses dosyası kontrol ediliyor:")
- print(f" - Path: {sound_path}")
- print(f" - Absolute: {sound_path.absolute()}")
- print(f" - Exists: {sound_path.exists()}")
-
- if not sound_path.exists():
- print(f"[SETTINGS TEST] HATA: Ses dosyası bulunamadı!")
- QMessageBox.warning(
- self,
- self._tr("error.title", "Hata"),
- self._tr("error.sound_not_found", "Ses dosyası bulunamadı:\n{file}", file=sound_file)
- )
- return
-
- if self._sound_tester is not None:
- try:
- print("[SETTINGS TEST] QtMultimedia ile ses çalınıyor...")
- self._sound_tester.stop()
- self._sound_tester.play(sound_path)
- return
- except Exception as play_error:
- print(f"[SETTINGS TEST] QtMultimedia hata verdi: {play_error}")
- QMessageBox.warning(
- self,
- self._tr("error.sound_play", "Ses çalınamadı"),
- str(play_error),
- )
- return
-
- if sound_path.suffix.lower() != ".wav":
- print("[SETTINGS TEST] WAV olmayan dosya winsound ile çalınamaz.")
- QMessageBox.information(
- self,
- self._tr("error.sound_unsupported", "Desteklenmeyen format"),
- self._tr(
- "error.sound_unsupported.detail",
- "Bu sistemde yalnızca WAV dosyaları test edilebilir. QtMultimedia backend'i kullanılamıyor.",
- ),
- )
- return
-
- import winsound
- print("[SETTINGS TEST] winsound ile WAV çalınıyor...")
- winsound.PlaySound(str(sound_path), winsound.SND_FILENAME)
- print("[SETTINGS TEST] ✓ Özel ses çalındı (winsound)")
- except Exception as e:
- print(f"[SETTINGS TEST] HATA: {e}")
- import traceback
- traceback.print_exc()
- QMessageBox.warning(
- self,
- self._tr("error.title", "Hata"),
- self._tr("error.sound_play", "Ses çalınamadı:\n{error}", error=str(e))
- )
-
- def _on_sound_test_failed(self, message: str) -> None:
- QMessageBox.warning(
- self,
- self._tr("settings.reminders.sound_error_title", "Ses çalınamadı"),
- self._tr(
- "settings.reminders.sound_error_body",
- "Seçilen ses dosyası çalınamadı:\n{error}",
- error=message,
- ),
- )
-
- def _tr(self, key: str, fallback: str) -> str:
- try:
- v = i18n.t(key)
- except Exception:
- v = ""
- return v if v and v != key else fallback
-
- def refresh_texts(self):
- self.setWindowTitle(self._tr("settings.title", "Ayarlar"))
- self.tabs.setTabText(0, self._tr("settings.tab.general", "Genel"))
- self.tabs.setTabText(1, self._tr("settings.tab.appearance", "Görünüm"))
- self.tabs.setTabText(2, self._tr("settings.tab.behavior", "Davranış"))
- self.tabs.setTabText(3, self._tr("settings.tab.security", "Güvenlik"))
- self.tabs.setTabText(4, self._tr("settings.tab.video", "Video"))
- self.tabs.setTabText(5, self._tr("settings.tab.reminders", "Hatırlatmalar"))
- self.tabs.setTabText(6, self._tr("settings.tab.sync", "Senkronizasyon"))
- self.tabs.setTabText(7, self._tr("settings.tab.tray", "Tepsi & Bildirimler"))
- self.tabs.setTabText(8, self._tr("settings.tab.about", "Hakkında"))
-
- self.lbl_hotkey_help.setText(self._tr("settings.general.hotkey.help", "Genel kısayol tuşu (örn: windows+v, ctrl+shift+v, alt+space)"))
- self.btn_clear_hk.setText(self._tr("settings.general.hotkey.reset", "Sıfırla"))
- self.btn_clear_hk_paste.setText(self._tr("settings.general.hotkey.reset", "Sıfırla"))
- self.btn_clear_hk_quick_note.setText(self._tr("settings.general.hotkey.reset", "Sıfırla"))
- self.btn_clear_hk_screenshot.setText(self._tr("settings.general.hotkey.reset", "Sıfırla"))
- self.btn_clear_hk_ocr.setText(self._tr("settings.general.hotkey.reset", "Sıfırla"))
- self.btn_clear_hk_snip.setText(self._tr("settings.general.hotkey.reset", "Sıfırla"))
-
- self.btn_preview.setText(self._tr("settings.tray.preview", "Önizle"))
- self.btn_test_sound.setText(self._tr("settings.reminders.test_sound", "Test"))
- self.btn_test_sound.setToolTip(self._tr("settings.reminders.test_sound_hint", "Seçili hatırlatma sesini çal"))
- self.grp_authors.setTitle(self._tr("about.authors", "Authors"))
- self.lbl_title.setText(self._tr("about.title", "TaxClip – Modern Windows Clipboard Manager"))
- self.lbl_desc.setText(self._tr("about.desc",
- "A powerful clipboard manager for text, images, files, snippets, reminders, lists, and drawings. "
- "Includes OCR, screen tools, sensitive-data protection, encryption, and Windows Hello support."))
- self.lbl_ai_badge.setText(self._tr("about.ai_badge",
- "🤖 This project was developed with assistance from AI (Claude AI)."))
- self.btn_site.setText(self._tr("about.website", "Website"))
- self.btn_pat.setText(self._tr("about.patreon", "Patreon"))
-
- self.btn_apply.setText(self._tr("settings.buttons.apply", "Uygula"))
- self.btn_cancel.setText(self._tr("settings.buttons.cancel", "İptal"))
- self.btn_ok.setText(self._tr("settings.buttons.save", "Kaydet"))
-
- def _on_tray_select(self, idx: int):
- data = self.cmb_tray.currentData()
- if data == "__custom__":
- file, _ = QFileDialog.getOpenFileName(
- self, self._tr("settings.tray.choose_icon", "İkon seç (.ico önerilir)"), "", "Icon (*.ico *.png *.svg)"
- )
- if file:
- self.settings.set("tray_icon", file)
- self.cmb_tray.setItemText(idx, f"{self._tr('settings.tray.custom_prefix', 'Özel')}: {Path(file).name}")
- self.cmb_tray.setCurrentIndex(idx)
- else:
- self.cmb_tray.setCurrentIndex(0)
-
- def _current_tray_icon_path(self) -> str:
- data = self.cmb_tray.currentData()
- if data == "__custom__":
- return str(self.settings.get("tray_icon", "") or "")
- return str(data or self.settings.get("tray_icon", "") or "")
-
- def _preview_tray_icon(self):
- path = self._current_tray_icon_path()
- if not path:
- return
- icon = svg_icon(path, 64)
- pix = icon.pixmap(64, 64)
-
- dlg = QDialog(self)
- dlg.setWindowTitle(self._tr("settings.tray.preview_title", "İkon önizleme"))
- v = QVBoxLayout(dlg)
- lbl = QLabel()
- if not pix.isNull():
- lbl.setPixmap(pix)
- else:
- lbl.setText(self._tr("settings.tray.preview_failed", "İkon yüklenemedi."))
- lbl.setAlignment(Qt.AlignCenter)
- v.addWidget(lbl)
- btn = QPushButton(self._tr("common.close", "Kapat"))
- btn.clicked.connect(dlg.accept)
- v.addWidget(btn, alignment=Qt.AlignCenter)
- dlg.exec()
-
- def _apply_common(self):
- self.settings.set("language", self.cmb_lang.currentData())
- self.settings.set("launch_at_startup", self.tgl_startup.isChecked())
- hk = KeyCaptureLineEdit.normalize_combo(self.txt_hotkey.text() or "ctrl+shift+v")
- self.settings.set("hotkey", hk or "ctrl+shift+v")
-
- hk_paste = KeyCaptureLineEdit.normalize_combo(self.txt_hotkey_paste.text() or "")
- self.settings.set("hotkey_paste_last", hk_paste)
-
- hk_quick = KeyCaptureLineEdit.normalize_combo(self.txt_hotkey_quick_note.text() or "")
- self.settings.set("hotkey_quick_note", hk_quick)
-
- hk_screenshot = KeyCaptureLineEdit.normalize_combo(self.txt_hotkey_screenshot.text() or "")
- self.settings.set("hotkey_screenshot", hk_screenshot)
-
- hk_ocr = KeyCaptureLineEdit.normalize_combo(self.txt_hotkey_ocr.text() or "")
- self.settings.set("hotkey_ocr", hk_ocr)
-
- hk_snip = KeyCaptureLineEdit.normalize_combo(self.txt_hotkey_snip.text() or "")
- self.settings.set("hotkey_snip", hk_snip)
-
- theme_key = self.cmb_theme.currentData()
- self.settings.set("theme", theme_key)
- self.settings.set("animations", self.tgl_animations.isChecked())
-
- self.settings.set("hide_after_copy", self.tgl_hide_after_copy.isChecked())
- self.settings.set("stay_on_top", self.tgl_stay_on_top.isChecked())
- self.settings.set("max_items", self.spn_max_items.value())
- self.settings.set("dedupe_window_ms", self.spn_dedupe_ms.value())
- self.settings.set("confirm_delete", self.tgl_confirm_delete.isChecked())
- self.settings.set("show_toast", self.tgl_toast.isChecked())
-
- self.settings.set("encrypt_data", self.tgl_encrypt.isChecked())
-
- # TOTP ayarları
- self.settings.set("totp_on_startup", self.tgl_totp_on_startup.isChecked())
- self.settings.set("totp_hourly_lock", self.tgl_totp_hourly_lock.isChecked())
- self.settings.set("totp_for_sensitive", self.tgl_totp_for_sensitive.isChecked())
-
- # Hassas veri ayarları
- self.settings.set("sensitive_data_detection", self.tgl_sensitive_detection.isChecked())
- self.settings.set("mask_credit_cards", self.tgl_mask_credit_cards.isChecked())
- self.settings.set("mask_passwords", self.tgl_mask_passwords.isChecked())
- self.settings.set("mask_api_keys", self.tgl_mask_api_keys.isChecked())
- self.settings.set("mask_emails", self.tgl_mask_emails.isChecked())
- self.settings.set("mask_phones", self.tgl_mask_phones.isChecked())
- self.settings.set("mask_tc_ids", self.tgl_mask_tc_ids.isChecked())
- self.settings.set("mask_ibans", self.tgl_mask_ibans.isChecked())
- self.settings.set("block_sensitive_data", self.tgl_block_sensitive.isChecked())
-
- try:
- self.settings.set("auto_clear_clipboard_seconds", int(self.cmb_auto_clear.currentData() or 0))
- self.settings.set("exclude_apps_enabled", self.tgl_exclude_apps.isChecked())
- self.settings.set("excluded_apps", self.txt_excluded_apps.text().strip())
- self.settings.set("windows_hello_enabled", self.tgl_windows_hello.isChecked())
- self.settings.set("biometric_lock_on_startup", self.tgl_bio_startup.isChecked())
- self.settings.set("biometric_lock_timeout", int(self.spn_bio_timeout.value()))
- except Exception:
- pass
-
- self.settings.set("save_images_externally", self.tgl_save_images_externally.isChecked())
- self.settings.set("external_images_path", self.txt_external_images_path.text())
- self.settings.set("auto_delete_enabled", self.tgl_auto_delete.isChecked())
- self.settings.set("auto_delete_days", self.cmb_auto_delete.currentData())
- self.settings.set("auto_delete_keep_fav", self.tgl_keep_fav.isChecked())
-
- # OCR ayarları
- self.settings.set("ocr_enabled", self.tgl_ocr.isChecked())
- self.settings.set("ocr_language", self.cmb_ocr_language.currentData())
- self.settings.set("tesseract_path", self.txt_tesseract_path.text())
-
- # Video ayarları
- self.settings.set("video_save_path", self.txt_video_save_path.text())
- self.settings.set("video_quality", self.cmb_video_quality.currentData())
- self.settings.set("video_fps", self.cmb_video_fps.currentData())
- self.settings.set("video_bitrate", self.spn_video_bitrate.value())
- self.settings.set("video_record_mic", self.tgl_record_mic.isChecked())
- self.settings.set("video_microphone", self.cmb_microphone.currentData())
-
- hk_video_record = KeyCaptureLineEdit.normalize_combo(self.txt_hotkey_video_record.text() or "")
- self.settings.set("hotkey_video_record", hk_video_record)
-
- hk_instant_replay = KeyCaptureLineEdit.normalize_combo(self.txt_hotkey_instant_replay.text() or "")
- self.settings.set("hotkey_instant_replay", hk_instant_replay)
-
- self.settings.set("instant_replay_buffer_seconds", self.spn_replay_buffer.value())
-
- data = self.cmb_tray.currentData()
- if data and data != "__custom__":
- self.settings.set("tray_icon", data)
- self.settings.set("tray_notifications", self.tgl_tray_notifications.isChecked())
-
- # Hatırlatma ayarları
- self.settings.set("reminder_notification_type", self.cmb_notification_type.currentData())
- self.settings.set("reminder_show_popup", self.tgl_show_popup.isChecked())
- self.settings.set("reminder_sound_enabled", self.tgl_sound.isChecked())
-
- sound_data = self.cmb_sound.currentData()
- if sound_data and sound_data != "__custom__":
- self.settings.set("reminder_sound_file", sound_data)
-
- self.settings.set("reminder_auto_snooze", self.tgl_auto_snooze.isChecked())
- self.settings.set("reminder_snooze_minutes", self.spn_snooze_minutes.value())
-
- # Paylaşım sunucusu (whitelist doğrulaması)
- share_url = (self.txt_share_server.text() or "").strip() or "https://taxclip.com"
- try:
- from .item_preview_dialog import validate_share_server_url
- ok_url, share_base = validate_share_server_url(share_url)
- if ok_url:
- self.settings.set("share_server_url", share_base)
- else:
- # Geçersiz URL kaydedilmez; varsayılana düş
- self.settings.set("share_server_url", "https://taxclip.com")
- self.txt_share_server.setText("https://taxclip.com")
- except Exception:
- self.settings.set("share_server_url", "https://taxclip.com")
-
- if hasattr(self, "txt_share_api_key"):
- self.settings.set("share_api_key", self.txt_share_api_key.text().strip())
-
- self.settings.save()
- try:
- theme_manager.apply(theme_key)
- except Exception:
- pass
-
- def _apply_and_emit(self):
- self._apply_common()
- self.applied.emit()
-
- def _apply_and_close(self):
- self._apply_common()
- self.accept()
-
- # ==================== SENKRONİZASYON METODLARI ====================
-
- def _export_to_json(self):
- """Tüm verileri JSON dosyasına aktar"""
- import json
- from datetime import datetime
-
- file_path, _ = QFileDialog.getSaveFileName(
- self,
- self._tr("settings.sync.export_title", "Verileri Dışa Aktar"),
- f"taxclip_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
- "JSON Files (*.json)"
- )
-
- if not file_path:
- return
-
- try:
- # Storage'dan verileri al
- from pathlib import Path
- from clipstack.storage import Storage
- data_dir = Path.home() / "AppData" / "Roaming" / "TaxClip"
- storage = Storage(data_dir / "taxclip.db", self.settings)
-
- export_data = {
- "version": "1.0",
- "exported_at": datetime.now().isoformat(),
- "clips": [],
- "notes": [],
- "reminders": [],
- "snippets": [],
- "todos": [],
- "drawings": [],
- "settings": {}
- }
-
- # Klipleri al
- clips = storage.list_items(limit=10000)
- for clip in clips:
- clip_entry = {
- "id": clip.get("id"),
- "item_type": clip.get("item_type"),
- "text_content": clip.get("text_content"),
- "html_content": clip.get("html_content"),
- "favorite": clip.get("favorite"),
- "created_at": clip.get("created_at"),
- "ocr_text": clip.get("ocr_text")
- }
- # Resimleri base64 olarak ekle
- if clip.get("image_blob"):
- import base64
- clip_entry["image_base64"] = base64.b64encode(clip["image_blob"]).decode("utf-8")
- export_data["clips"].append(clip_entry)
-
- # Notları al
- notes = storage.list_notes(limit=10000)
- for note in notes:
- export_data["notes"].append({
- "id": note.get("id"),
- "title": note.get("title", ""),
- "content": note.get("content"),
- "created_at": note.get("created_at"),
- "updated_at": note.get("updated_at")
- })
-
- # Hatırlatıcıları al
- reminders = storage.list_reminders(limit=10000)
- for rem in reminders:
- export_data["reminders"].append({
- "id": rem.get("id"),
- "title": rem.get("title"),
- "description": rem.get("description"),
- "reminder_time": rem.get("reminder_time"),
- "repeat": rem.get("repeat_type"),
- "completed": rem.get("completed")
- })
-
- # Snippetları al
- snippets = storage.list_snippets(limit=10000)
- for snip in snippets:
- export_data["snippets"].append({
- "id": snip.get("id"),
- "title": snip.get("title"),
- "content": snip.get("code"),
- "language": snip.get("language"),
- "tags": snip.get("tags"),
- "created_at": snip.get("created_at")
- })
-
- # Todoları al
- todos = storage.list_todos()
- for todo in todos:
- export_data["todos"].append({
- "id": todo.get("id"),
- "title": todo.get("content"), # todos tablosunda 'content' kullanılıyor
- "completed": todo.get("completed"),
- "list_id": todo.get("list_id"),
- "created_at": todo.get("created_at")
- })
-
- # Çizimleri al
- drawings = storage.list_drawings(limit=10000)
- for drawing in drawings:
- export_data["drawings"].append({
- "id": drawing.get("id"),
- "title": drawing.get("title"),
- "image_data": drawing.get("image"), # list_drawings 'image' olarak dönüyor
- "created_at": drawing.get("created_at")
- })
-
- # Ayarları al
- all_settings = self.settings._data.copy() if hasattr(self.settings, '_data') else {}
- # Hassas verileri çıkar
- safe_settings = {k: v for k, v in all_settings.items()
- if not any(x in k.lower() for x in ['password', 'secret', 'key', 'token'])}
- export_data["settings"] = safe_settings
-
- # JSON'a yaz
- with open(file_path, 'w', encoding='utf-8') as f:
- json.dump(export_data, f, ensure_ascii=False, indent=2, default=str)
-
- total_items = (len(export_data["clips"]) + len(export_data["notes"]) +
- len(export_data["reminders"]) + len(export_data["snippets"]) +
- len(export_data["todos"]) + len(export_data["drawings"]))
-
- QMessageBox.information(
- self,
- self._tr("settings.sync.export_success", "Dışa Aktarma Başarılı"),
- f"✅ {total_items} öğe başarıyla dışa aktarıldı!\n\n📁 {file_path}"
- )
-
- except Exception as e:
- QMessageBox.critical(
- self,
- self._tr("settings.sync.export_error", "Dışa Aktarma Hatası"),
- f"❌ Dışa aktarma başarısız:\n{str(e)}"
- )
-
- def _import_from_json(self):
- """JSON dosyasından verileri içe aktar"""
- import json
-
- file_path, _ = QFileDialog.getOpenFileName(
- self,
- self._tr("settings.sync.import_title", "Verileri İçe Aktar"),
- "",
- "JSON Files (*.json)"
- )
-
- if not file_path:
- return
-
- # Onay iste
- reply = QMessageBox.question(
- self,
- self._tr("settings.sync.import_confirm_title", "İçe Aktarma Onayı"),
- self._tr("settings.sync.import_confirm_msg",
- "⚠️ Mevcut verileriniz korunacak, yeni veriler eklenecek.\n\nDevam etmek istiyor musunuz?"),
- QMessageBox.Yes | QMessageBox.No,
- QMessageBox.No
- )
-
- if reply != QMessageBox.Yes:
- return
-
- try:
- with open(file_path, 'r', encoding='utf-8') as f:
- import_data = json.load(f)
-
- from pathlib import Path
- from clipstack.storage import Storage
- data_dir = Path.home() / "AppData" / "Roaming" / "TaxClip"
- storage = Storage(data_dir / "taxclip.db", self.settings)
-
- imported_counts = {
- "clips": 0,
- "notes": 0,
- "reminders": 0,
- "snippets": 0,
- "todos": 0,
- "drawings": 0
- }
-
- # Klipleri içe aktar
- from clipstack.storage import ClipItemType
- for clip in import_data.get("clips", []):
- try:
- item_type_val = clip.get("item_type", 1)
- item_type = ClipItemType(item_type_val) if isinstance(item_type_val, int) else ClipItemType.TEXT
- text_content = clip.get("text_content") or clip.get("content", "")
- html_content = clip.get("html_content")
- image_bytes = None
- if clip.get("image_base64"):
- import base64
- image_bytes = base64.b64decode(clip["image_base64"])
- created_at = clip.get("created_at") or datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- storage.add_item(
- item_type=item_type,
- text=text_content,
- image_bytes=image_bytes,
- html=html_content,
- created_at=created_at,
- ocr_text=clip.get("ocr_text")
- )
- imported_counts["clips"] += 1
- except Exception:
- pass
-
- # Notları içe aktar
- for note in import_data.get("notes", []):
- try:
- from datetime import datetime
- storage.add_note(
- content=note.get("content", ""),
- created_at=note.get("created_at") or datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- )
- imported_counts["notes"] += 1
- except:
- pass
-
- # Hatırlatıcıları içe aktar
- for rem in import_data.get("reminders", []):
- try:
- storage.add_reminder(
- title=rem.get("title", "İçe Aktarılan Hatırlatıcı"),
- description=rem.get("description", ""),
- reminder_time=rem.get("reminder_time"),
- repeat_type=rem.get("repeat", "none")
- )
- imported_counts["reminders"] += 1
- except:
- pass
-
- # Snippetları içe aktar
- for snip in import_data.get("snippets", []):
- try:
- storage.add_snippet(
- title=snip.get("title", "İçe Aktarılan Snippet"),
- code=snip.get("content", ""),
- language=snip.get("language", "text"),
- tags=snip.get("tags", "")
- )
- imported_counts["snippets"] += 1
- except:
- pass
-
- # Todoları içe aktar (varsayılan liste id=1)
- for todo in import_data.get("todos", []):
- try:
- storage.add_todo(
- list_id=1, # Varsayılan liste
- content=todo.get("title", "") or todo.get("content", "İçe Aktarılan Görev")
- )
- imported_counts["todos"] += 1
- except:
- pass
-
- # Çizimleri içe aktar
- for drawing in import_data.get("drawings", []):
- try:
- storage.add_drawing(
- image_data=drawing.get("image_data", ""),
- title=drawing.get("title", "İçe Aktarılan Çizim")
- )
- imported_counts["drawings"] += 1
- except:
- pass
-
- total = sum(imported_counts.values())
-
- QMessageBox.information(
- self,
- self._tr("settings.sync.import_success", "İçe Aktarma Başarılı"),
- f"✅ Toplam {total} öğe başarıyla içe aktarıldı!\n\n"
- f"📋 Klipler: {imported_counts['clips']}\n"
- f"📝 Notlar: {imported_counts['notes']}\n"
- f"⏰ Hatırlatıcılar: {imported_counts['reminders']}\n"
- f"💻 Snippetlar: {imported_counts['snippets']}\n"
- f"✅ Görevler: {imported_counts['todos']}\n"
- f"🎨 Çizimler: {imported_counts['drawings']}"
- )
-
- except json.JSONDecodeError:
- QMessageBox.critical(
- self,
- self._tr("settings.sync.import_error", "İçe Aktarma Hatası"),
- "❌ Geçersiz JSON dosyası!"
- )
- except Exception as e:
- QMessageBox.critical(
- self,
- self._tr("settings.sync.import_error", "İçe Aktarma Hatası"),
- f"❌ İçe aktarma başarısız:\n{str(e)}"
- )
-
- def _connect_google_drive(self):
- """Google Drive'a bağlan/bağlantıyı kes"""
- from clipstack.gdrive_sync import GoogleDriveSync
-
- sync = GoogleDriveSync(self.settings)
-
- if sync.is_connected():
- # Bağlantıyı kes
- reply = QMessageBox.question(
- self,
- self._tr("settings.sync.gdrive_disconnect_title", "Bağlantıyı Kes"),
- self._tr("settings.sync.gdrive_disconnect_msg", "Google Drive bağlantısını kesmek istediğinize emin misiniz?"),
- QMessageBox.Yes | QMessageBox.No,
- QMessageBox.No
- )
-
- if reply == QMessageBox.Yes:
- success, msg = sync.disconnect()
- if success:
- QMessageBox.information(self, "Başarılı", "✅ Bağlantı kesildi")
- else:
- QMessageBox.warning(self, "Hata", f"❌ {msg}")
- else:
- # Bağlan
- if not sync.is_available():
- QMessageBox.warning(
- self,
- self._tr("settings.sync.gdrive_not_available", "Eksik Kütüphane"),
- "Google Drive entegrasyonu için aşağıdaki kütüphaneler gerekli:\n\n"
- "pip install google-auth-oauthlib google-api-python-client\n\n"
- "Şimdilik JSON dışa/içe aktarma özelliklerini kullanabilirsiniz."
- )
- return
-
- # OAuth akışını başlat
- success, msg = sync.connect()
-
- if success:
- QMessageBox.information(
- self,
- self._tr("settings.sync.gdrive_connected", "Bağlandı"),
- f"✅ {msg}"
- )
- else:
- QMessageBox.warning(
- self,
- self._tr("settings.sync.gdrive_error", "Bağlantı Hatası"),
- f"❌ {msg}"
- )
-
- self._check_gdrive_status()
-
- def _check_gdrive_status(self):
- """Google Drive bağlantı durumunu kontrol et"""
- from clipstack.gdrive_sync import GoogleDriveSync
-
- sync = GoogleDriveSync(self.settings)
- gdrive_connected = sync.is_connected()
-
- if gdrive_connected:
- self.lbl_gdrive_status.setText("✅ Bağlı")
- self.lbl_gdrive_status.setStyleSheet("color: #4CAF50; font-weight: bold;")
- self.btn_gdrive_connect.setText("🔓 Bağlantıyı Kes")
- self.btn_sync_now.setEnabled(True)
- else:
- self.lbl_gdrive_status.setText("❌ Bağlı Değil")
- self.lbl_gdrive_status.setStyleSheet("color: #f44336; font-weight: bold;")
- self.btn_gdrive_connect.setText("🔗 Google Drive'a Bağlan")
- self.btn_sync_now.setEnabled(False)
-
- def _check_gdrive_status_async(self):
- if self._gdrive_status_thread and self._gdrive_status_thread.isRunning():
- return
-
- self.lbl_gdrive_status.setText("⏳ Durum kontrol ediliyor...")
- self.lbl_gdrive_status.setStyleSheet("color: #888;")
-
- thread = GoogleDriveStatusThread(self)
- thread.status_ready.connect(self._apply_gdrive_status)
- thread.status_failed.connect(self._apply_gdrive_status_failure)
- thread.finished.connect(thread.deleteLater)
- self._gdrive_status_thread = thread
- thread.start()
-
- def _apply_gdrive_status(self, connected: bool):
- if connected:
- self.lbl_gdrive_status.setText("✅ Bağlı")
- self.lbl_gdrive_status.setStyleSheet("color: #4CAF50; font-weight: bold;")
- self.btn_gdrive_connect.setText("🔓 Bağlantıyı Kes")
- self.btn_sync_now.setEnabled(True)
- else:
- self.lbl_gdrive_status.setText("❌ Bağlı Değil")
- self.lbl_gdrive_status.setStyleSheet("color: #f44336; font-weight: bold;")
- self.btn_gdrive_connect.setText("🔗 Google Drive'a Bağlan")
- self.btn_sync_now.setEnabled(False)
- self._gdrive_status_thread = None
-
- def _apply_gdrive_status_failure(self, error: str):
- print(f"[GDRIVE] Durum kontrol hatası: {error}")
- self.lbl_gdrive_status.setText("❌ Bağlı Değil")
- self.lbl_gdrive_status.setStyleSheet("color: #f44336; font-weight: bold;")
- self.btn_gdrive_connect.setText("🔗 Google Drive'a Bağlan")
- self.btn_sync_now.setEnabled(False)
- self._gdrive_status_thread = None
-
- def _sync_now(self):
- """Manuel senkronizasyon başlat - Google Drive'a yedekle"""
- from clipstack.gdrive_sync import GoogleDriveSync
-
- sync = GoogleDriveSync(self.settings)
-
- if not sync.is_connected():
- QMessageBox.warning(
- self,
- self._tr("settings.sync.not_connected", "Bağlı Değil"),
- "Önce Google Drive'a bağlanmalısınız."
- )
- return
-
- try:
- # Export verilerini hazırla
- import json
- from datetime import datetime
- from pathlib import Path
- from clipstack.storage import Storage
-
- data_dir = Path.home() / "AppData" / "Roaming" / "TaxClip"
- storage = Storage(data_dir / "taxclip.db", self.settings)
-
- export_data = {
- "version": "1.0",
- "exported_at": datetime.now().isoformat(),
- "clips": [],
- "notes": [],
- "reminders": [],
- "snippets": [],
- "todos": [],
- "drawings": []
- }
-
- # Verileri topla
- clips = storage.list_items(limit=10000)
- for clip in clips:
- clip_entry = {
- "id": clip.get("id"),
- "item_type": clip.get("item_type"),
- "text_content": clip.get("text_content"),
- "html_content": clip.get("html_content"),
- "favorite": clip.get("favorite"),
- "created_at": clip.get("created_at"),
- "ocr_text": clip.get("ocr_text")
- }
- if clip.get("image_blob"):
- import base64
- clip_entry["image_base64"] = base64.b64encode(clip["image_blob"]).decode("utf-8")
- export_data["clips"].append(clip_entry)
-
- export_data["notes"] = storage.list_notes(limit=10000)
- export_data["reminders"] = storage.list_reminders(limit=10000)
- export_data["snippets"] = storage.list_snippets(limit=10000)
- export_data["todos"] = storage.list_todos()
- export_data["drawings"] = storage.list_drawings(limit=10000)
-
- json_data = json.dumps(export_data, ensure_ascii=False, indent=2, default=str)
-
- # Google Drive'a yükle
- success, result = sync.upload_backup(json_data)
-
- if success:
- QMessageBox.information(
- self,
- self._tr("settings.sync.sync_success", "Senkronizasyon Başarılı"),
- f"✅ Veriler Google Drive'a yedeklendi!\n\nDosya ID: {result}"
- )
- else:
- QMessageBox.warning(
- self,
- self._tr("settings.sync.sync_error", "Senkronizasyon Hatası"),
- f"❌ {result}"
- )
-
- except Exception as e:
- QMessageBox.critical(
- self,
- self._tr("settings.sync.sync_error", "Senkronizasyon Hatası"),
- f"❌ Senkronizasyon başarısız:\n{str(e)}"
- )
-
- def _check_for_updates(self):
- """GitHub'dan güncelleme kontrolü yap"""
- from ..updater import Updater, show_update_dialog
-
- self.btn_check_update.setEnabled(False)
- self.lbl_update_status.setText("⏳ Kontrol ediliyor...")
-
- self._updater = Updater(self.settings, self)
-
- def on_update_available(update_info):
- self.btn_check_update.setEnabled(True)
- self.lbl_update_status.setText(f"✅ v{update_info['version']} mevcut!")
- self.lbl_update_status.setStyleSheet("color: #4CAF50; font-weight: bold;")
- show_update_dialog(self, update_info, self._updater)
-
- def on_no_update():
- self.btn_check_update.setEnabled(True)
- self.lbl_update_status.setText("✓ Güncel")
- self.lbl_update_status.setStyleSheet("color: #4CAF50;")
-
- def on_check_failed(error):
- self.btn_check_update.setEnabled(True)
- self.lbl_update_status.setText("❌ Kontrol başarısız")
- self.lbl_update_status.setStyleSheet("color: #f44336;")
- print(f"[UPDATE] Kontrol hatası: {error}")
-
- # Sinyalleri Updater üzerinden bağla (thread check_for_updates'te oluşturulur)
- self._updater.update_available.connect(on_update_available)
- self._updater.update_failed.connect(on_check_failed)
-
- # Önce check başlat (thread oluşturulur)
- self._updater.check_for_updates(silent=False)
-
- # Şimdi thread oluşturuldu, no_update sinyalini bağla
- if self._updater._checker_thread:
- self._updater._checker_thread.no_update.connect(on_no_update)
- self._updater._checker_thread.check_failed.connect(on_check_failed)
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Dict
+
+from PySide6.QtCore import Qt, Signal, QUrl, QSize, QTimer, QThread
+from PySide6.QtGui import QIcon, QDesktopServices, QKeySequence
+from PySide6.QtWidgets import (
+ QDialog,
+ QVBoxLayout,
+ QStackedWidget,
+ QWidget,
+ QFormLayout,
+ QComboBox,
+ QHBoxLayout,
+ QPushButton,
+ QFileDialog,
+ QLabel,
+ QSpinBox,
+ QLineEdit,
+ QMessageBox,
+ QStyle,
+ QFrame,
+ QScrollArea,
+)
+
+from ..settings import Settings
+from ..utils import resource_path, svg_icon
+from ..i18n import i18n
+from ..theme_manager import theme_manager
+from .widgets.toggle_switch import ToggleSwitch
+from .settings_chrome import (
+ build_settings_style,
+ settings_palette,
+ SettingsCloseButton,
+ SettingsNavButton,
+ SettingRow,
+ modernize_form_layout,
+)
+from ..sound_player import SoundPlayer, is_sound_backend_available
+
+# Sidebar: (icon name, title i18n key, title fallback, subtitle key, subtitle fallback)
+_NAV_META = [
+ ("general", "settings.tab.general", "Genel", "settings.page.general.sub", "Kısayollar, dil ve başlangıç tercihleri"),
+ ("appearance", "settings.tab.appearance", "Görünüm", "settings.page.appearance.sub", "Tema ve görsel tercihler"),
+ ("behavior", "settings.tab.behavior", "Davranış", "settings.page.behavior.sub", "Pano davranışı ve etkileşim"),
+ ("security", "settings.tab.security", "Güvenlik", "settings.page.security.sub", "Şifreleme, gizlilik ve veri koruması"),
+ ("video", "settings.tab.video", "Video", "settings.page.video.sub", "Ekran kayıt tercihleri"),
+ ("reminders", "settings.tab.reminders", "Hatırlatmalar", "settings.page.reminders.sub", "Bildirim ve hatırlatma tercihleri"),
+ ("sync", "settings.tab.sync", "Senkronizasyon", "settings.page.sync.sub", "Yedekleme, bulut ve paylaşım"),
+ ("tray", "settings.tab.tray", "Tepsi & Bildirimler", "settings.page.tray.sub", "Sistem tepsisi ve bildirim ikonları"),
+ ("about", "settings.tab.about", "Hakkında", "settings.page.about.sub", "Uygulama bilgisi ve güncellemeler"),
+]
+
+
+LANG_MAP: Dict[str, str] = {
+ "tr": "Türkçe",
+ "en": "English",
+ "de": "Deutsch",
+ "fr": "Français",
+ "es": "Español",
+ "it": "Italiano",
+ "az": "Azərbaycanca",
+ "ru": "Русский",
+ "ja": "日本語",
+ "zh": "中文",
+}
+
+THEMES = [
+ ("default", "Default (Blue)"),
+ ("dark", "Dark (Black)"),
+ ("light", "Light (White)"),
+ ("purple", "Purple"),
+ ("cyberpunk", "🌆 Cyberpunk"),
+ ("sunset", "🌅 Sunset"),
+ ("matrix", "💚 Matrix"),
+ ("ocean", "🌊 Ocean"),
+ ("retro", "🎮 Retro (XP)"),
+]
+
+
+class GoogleDriveStatusThread(QThread):
+ status_ready = Signal(bool)
+ status_failed = Signal(str)
+
+ def run(self):
+ try:
+ from clipstack.gdrive_sync import GoogleDriveSync
+
+ sync = GoogleDriveSync()
+ self.status_ready.emit(sync.is_connected())
+ except Exception as exc:
+ self.status_failed.emit(str(exc))
+
+
+class VideoProbeThread(QThread):
+ probe_ready = Signal(bool, str, list)
+ probe_failed = Signal(str)
+
+ def __init__(self, settings: Settings, parent=None):
+ super().__init__(parent)
+ self._settings = settings
+
+ def run(self):
+ try:
+ from clipstack.video_recorder import AdvancedVideoRecorder
+
+ recorder = AdvancedVideoRecorder(self._settings)
+ available = recorder.is_available()
+ encoder_info = recorder.get_encoder_info()
+ devices = recorder.list_audio_devices()
+ self.probe_ready.emit(available, encoder_info, devices)
+ except Exception as exc:
+ self.probe_failed.emit(str(exc))
+
+
+class KeyCaptureLineEdit(QLineEdit):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setClearButtonEnabled(True)
+ self.refresh_placeholder()
+ i18n.languageChanged.connect(self.refresh_placeholder)
+
+ def refresh_placeholder(self):
+ self.setPlaceholderText(self._tr("settings.general.hotkey.placeholder", "e.g., ctrl+shift+v"))
+
+ def _tr(self, key: str, fallback: str) -> str:
+ try:
+ v = i18n.t(key)
+ except Exception:
+ v = ""
+ return v if v and v != key else fallback
+
+ @staticmethod
+ def normalize_combo(combo: str) -> str:
+ t = (combo or "").strip().lower()
+ if not t:
+ return ""
+ parts = [p.strip() for p in t.split("+") if p.strip()]
+ mods = set()
+ key = ""
+ alias = {
+ "control": "ctrl",
+ "cmd": "windows",
+ "command": "windows",
+ "super": "windows",
+ "win": "windows",
+ "option": "alt",
+ "menu": "alt",
+ }
+ specials = {
+ "space": "space", "tab": "tab", "insert": "insert", "delete": "delete",
+ "home": "home", "end": "end", "page up": "pgup", "page down": "pgdn",
+ "pgup": "pgup", "pgdn": "pgdn",
+ "up": "up", "down": "down", "left": "left", "right": "right",
+ "escape": "esc", "esc": "esc",
+ "return": "enter", "enter": "enter", "backspace": "backspace",
+ }
+ for p in parts:
+ p2 = alias.get(p, p)
+ if p2 in ("ctrl", "shift", "alt", "windows"):
+ mods.add(p2)
+ else:
+ if p2.startswith("f") and p2[1:].isdigit():
+ key = p2
+ elif p2 in specials:
+ key = specials[p2]
+ else:
+ key = p2
+ ordered_mods = [m for m in ("ctrl", "shift", "alt", "windows") if m in mods]
+ return "+".join(ordered_mods + ([key] if key else []))
+
+ def keyPressEvent(self, e):
+ if e.key() in (Qt.Key_Control, Qt.Key_Shift, Qt.Key_Alt, Qt.Key_Meta):
+ return
+
+ mods = []
+ if e.modifiers() & Qt.ControlModifier:
+ mods.append("ctrl")
+ if e.modifiers() & Qt.ShiftModifier:
+ mods.append("shift")
+ if e.modifiers() & Qt.AltModifier:
+ mods.append("alt")
+ if e.modifiers() & Qt.MetaModifier:
+ mods.append("windows")
+
+ key_text = QKeySequence(e.key()).toString().strip()
+ special_map = {
+ "Space": "space", "Tab": "tab", "Insert": "insert", "Delete": "delete",
+ "Home": "home", "End": "end", "Page Up": "pgup", "Page Down": "pgdn",
+ "Up": "up", "Down": "down", "Left": "left", "Right": "right",
+ "Escape": "esc", "Return": "enter", "Enter": "enter", "Backspace": "backspace",
+ }
+ if key_text in special_map:
+ key_part = special_map[key_text]
+ elif key_text.upper().startswith("F") and key_text[1:].isdigit():
+ key_part = key_text.lower()
+ else:
+ key_part = key_text.lower() if len(key_text) else ""
+
+ combo = "+".join(mods + ([key_part] if key_part else [])) if mods or key_part else ""
+ norm = self.normalize_combo(combo)
+ if norm:
+ self.setText(norm)
+
+
+class SettingsDialog(QDialog):
+ applied = Signal()
+
+ def __init__(self, settings: Settings, parent=None, storage=None):
+ super().__init__(parent)
+ # Configure the native window before doing anything that may pump the
+ # Windows event queue. In particular, probing QtMultimedia here used
+ # to expose this QDialog briefly with its default title bar.
+ self.setObjectName("SettingsDialog")
+ self.setWindowTitle("Settings")
+ self.setWindowFlags(
+ Qt.WindowType.Dialog
+ | Qt.WindowType.FramelessWindowHint
+ )
+ self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
+ self.settings = settings
+ self.storage = storage
+ self._bulk_deleted = False
+ self._dispose_requested = False
+
+ # The multimedia backend is needed only when a custom reminder sound
+ # is tested. Probing it while opening Settings blocks the UI and may
+ # create transient native windows on Windows.
+ self._sound_tester: SoundPlayer | None = None
+ try:
+ self.setWindowIcon(svg_icon("assets/icons/gear.svg"))
+ except Exception:
+ pass
+ self.resize(1020, 660)
+ self.setMinimumSize(900, 600)
+ self._settings_theme_key = str(settings.get("theme", "default") or "default")
+ self._settings_palette = settings_palette(self._settings_theme_key)
+ self._drag_offset = None
+
+ self.tab_general = QWidget()
+ self.tab_appearance = QWidget()
+ self.tab_behavior = QWidget()
+ self.tab_security = QWidget()
+ self.tab_video = QWidget()
+ self.tab_reminders = QWidget()
+ self.tab_sync = QWidget()
+ self.tab_tray = QWidget()
+ self.tab_about = QWidget()
+ for page in (
+ self.tab_general, self.tab_appearance, self.tab_behavior,
+ self.tab_security, self.tab_video, self.tab_reminders,
+ self.tab_sync, self.tab_tray, self.tab_about,
+ ):
+ page.setObjectName("SettingsPage")
+
+ self.stack = QStackedWidget(self)
+ self.stack.addWidget(self.tab_general)
+ self.stack.addWidget(self.tab_appearance)
+ self.stack.addWidget(self.tab_behavior)
+ self.stack.addWidget(self.tab_security)
+ self.stack.addWidget(self.tab_video)
+ self.stack.addWidget(self.tab_reminders)
+ self.stack.addWidget(self.tab_sync)
+ self.stack.addWidget(self.tab_tray)
+ self.stack.addWidget(self.tab_about)
+
+ # ---- Shell: header / sidebar / content / footer ----
+ outer = QVBoxLayout(self)
+ outer.setContentsMargins(0, 0, 0, 0)
+ outer.setSpacing(0)
+
+ self.settings_surface = QFrame()
+ self.settings_surface.setObjectName("SettingsSurface")
+ outer.addWidget(self.settings_surface)
+
+ root = QVBoxLayout(self.settings_surface)
+ root.setContentsMargins(1, 1, 1, 1)
+ root.setSpacing(0)
+
+ header = QWidget()
+ header.setObjectName("SettingsHeader")
+ header.setFixedHeight(77)
+ self._drag_handle = header
+ header_l = QHBoxLayout(header)
+ header_l.setContentsMargins(21, 14, 20, 12)
+ header_l.setSpacing(8)
+ title_col = QVBoxLayout()
+ title_col.setSpacing(1)
+ self.lbl_settings_title = QLabel("Settings")
+ self.lbl_settings_title.setObjectName("SettingsTitle")
+ self.lbl_settings_subtitle = QLabel("Clipboard preferences · applies instantly")
+ self.lbl_settings_subtitle.setObjectName("SettingsSubtitle")
+ title_col.addWidget(self.lbl_settings_title)
+ title_col.addWidget(self.lbl_settings_subtitle)
+ header_l.addLayout(title_col, 1)
+ self.btn_close_x = SettingsCloseButton(self._settings_palette["close"])
+ self.btn_close_x.setFixedSize(32, 32)
+ self.btn_close_x.clicked.connect(self.reject)
+ header_l.addWidget(self.btn_close_x, 0, Qt.AlignTop)
+ root.addWidget(header)
+
+ body = QHBoxLayout()
+ body.setContentsMargins(0, 0, 0, 0)
+ body.setSpacing(0)
+
+ sidebar = QWidget()
+ sidebar.setObjectName("SettingsSidebar")
+ sidebar.setFixedWidth(212)
+ side_l = QVBoxLayout(sidebar)
+ side_l.setContentsMargins(13, 13, 11, 0)
+ side_l.setSpacing(0)
+ self._nav_buttons: list[SettingsNavButton] = []
+ for i, (icon_name, _tk, title_fb, _sk, _sf) in enumerate(_NAV_META):
+ btn = SettingsNavButton(
+ i,
+ icon_name,
+ title_fb,
+ inactive_color=self._settings_palette["nav"],
+ active_color=self._settings_palette["nav_active"],
+ )
+ btn.activated.connect(self._switch_page)
+ side_l.addWidget(btn)
+ self._nav_buttons.append(btn)
+ side_l.addStretch(1)
+ body.addWidget(sidebar)
+
+ divider = QFrame()
+ divider.setObjectName("SidebarDivider")
+ divider.setFrameShape(QFrame.Shape.NoFrame)
+ body.addWidget(divider)
+
+ content = QWidget()
+ content.setObjectName("SettingsContent")
+ content_l = QVBoxLayout(content)
+ content_l.setContentsMargins(28, 24, 4, 0)
+ content_l.setSpacing(3)
+ self.lbl_page_title = QLabel()
+ self.lbl_page_title.setObjectName("PageTitle")
+ self.lbl_page_subtitle = QLabel()
+ self.lbl_page_subtitle.setObjectName("PageSubtitle")
+ self.lbl_page_subtitle.setWordWrap(True)
+ content_l.addWidget(self.lbl_page_title)
+ content_l.addWidget(self.lbl_page_subtitle)
+ content_l.addSpacing(6)
+ content_l.addWidget(self.stack, 1)
+ body.addWidget(content, 1)
+ root.addLayout(body, 1)
+
+ footer_div = QFrame()
+ footer_div.setObjectName("FooterDivider")
+ footer_div.setFrameShape(QFrame.Shape.NoFrame)
+ root.addWidget(footer_div)
+
+ # Placeholder — footer added after all tabs are built
+ self._root_layout = root
+
+ # ---- General page (Haulbase-style rows) ----
+ general_scroll = QScrollArea(self.tab_general)
+ general_scroll.setWidgetResizable(True)
+ general_scroll.setFrameShape(QFrame.Shape.NoFrame)
+ general_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
+ general_inner = QWidget()
+ general_inner.setObjectName("SettingsPage")
+ form_g = QVBoxLayout(general_inner)
+ form_g.setContentsMargins(0, 0, 31, 8)
+ form_g.setSpacing(0)
+ general_scroll.setWidget(general_inner)
+ general_page_l = QVBoxLayout(self.tab_general)
+ general_page_l.setContentsMargins(0, 0, 0, 0)
+ general_page_l.addWidget(general_scroll)
+
+ self.cmb_lang = QComboBox()
+ self.cmb_lang.setMinimumHeight(36)
+ self.cmb_lang.setFixedWidth(190)
+ for code, label in LANG_MAP.items():
+ self.cmb_lang.addItem(label, code)
+ self.cmb_lang.setCurrentIndex(max(0, self.cmb_lang.findData(settings.get("language", "tr"))))
+
+ self.tgl_startup = ToggleSwitch(checked=bool(settings.get("launch_at_startup", True)))
+
+ self.txt_hotkey = KeyCaptureLineEdit()
+ self.txt_hotkey.setText(KeyCaptureLineEdit.normalize_combo(str(settings.get("hotkey", "ctrl+shift+v"))))
+ self.btn_clear_hk = QPushButton()
+
+ def _hk_control(edit: KeyCaptureLineEdit, btn: QPushButton) -> QWidget:
+ w = QWidget()
+ lay = QHBoxLayout(w)
+ lay.setContentsMargins(0, 0, 0, 0)
+ lay.setSpacing(0)
+ edit.setFixedWidth(190)
+ lay.addWidget(edit)
+ btn.hide()
+ return w
+
+ self.txt_hotkey_paste = KeyCaptureLineEdit()
+ self.txt_hotkey_paste.setText(KeyCaptureLineEdit.normalize_combo(str(settings.get("hotkey_paste_last", ""))))
+ self.btn_clear_hk_paste = QPushButton()
+
+ self.txt_hotkey_quick_note = KeyCaptureLineEdit()
+ self.txt_hotkey_quick_note.setText(KeyCaptureLineEdit.normalize_combo(str(settings.get("hotkey_quick_note", ""))))
+ self.btn_clear_hk_quick_note = QPushButton()
+
+ self.txt_hotkey_screenshot = KeyCaptureLineEdit()
+ self.txt_hotkey_screenshot.setText(KeyCaptureLineEdit.normalize_combo(str(settings.get("hotkey_screenshot", ""))))
+ self.btn_clear_hk_screenshot = QPushButton()
+
+ self.txt_hotkey_ocr = KeyCaptureLineEdit()
+ self.txt_hotkey_ocr.setText(KeyCaptureLineEdit.normalize_combo(str(settings.get("hotkey_ocr", ""))))
+ self.btn_clear_hk_ocr = QPushButton()
+
+ self.txt_hotkey_snip = KeyCaptureLineEdit()
+ self.txt_hotkey_snip.setText(KeyCaptureLineEdit.normalize_combo(str(settings.get("hotkey_snip", ""))))
+ self.btn_clear_hk_snip = QPushButton()
+
+ try:
+ reset_icon = self.style().standardIcon(QStyle.SP_LineEditClearButton)
+ for btn in (self.btn_clear_hk, self.btn_clear_hk_paste, self.btn_clear_hk_quick_note, self.btn_clear_hk_screenshot, self.btn_clear_hk_ocr, self.btn_clear_hk_snip):
+ btn.setIcon(reset_icon)
+ btn.setIconSize(QSize(18, 18))
+ btn.setCursor(Qt.PointingHandCursor)
+ except Exception:
+ pass
+
+ self.lbl_hotkey_help = QLabel()
+ self.lbl_hotkey_help.setObjectName("SettingRowDesc")
+ self.lbl_hotkey_help.setWordWrap(True)
+ self.lbl_hotkey_help.hide()
+
+ self._row_startup = SettingRow(
+ self._tr("settings.general.launch_at_startup", "Windows ile başlat"),
+ self._tr("settings.general.launch_at_startup.desc", "Windows oturum açıldığında uygulamayı sessizce başlat."),
+ self.tgl_startup,
+ )
+ self._row_lang = SettingRow(
+ self._tr("settings.general.language", "Dil"),
+ self._tr("settings.general.language.desc", "Arayüz dili. İçerik mümkün olduğunda buna uyar."),
+ self.cmb_lang,
+ )
+ self._row_hotkey = SettingRow(
+ self._tr("settings.general.hotkey.label", "Ana kısayol tuşu"),
+ self._tr("settings.general.hotkey.desc", "Pencereyi göstermek / gizlemek için kısayol."),
+ _hk_control(self.txt_hotkey, self.btn_clear_hk),
+ )
+ self._row_hotkey_paste = SettingRow(
+ self._tr("settings.general.hotkey_paste.label", "Son içeriği yapıştır"),
+ self._tr("settings.general.hotkey_paste.desc", "Son kopyalanan içeriği doğrudan yapıştır."),
+ _hk_control(self.txt_hotkey_paste, self.btn_clear_hk_paste),
+ )
+ self._row_hotkey_quick_note = SettingRow(
+ self._tr("settings.general.hotkey_quick_note.label", "Hızlı not al"),
+ self._tr("settings.general.hotkey_quick_note.desc", "Hızlı not penceresini aç."),
+ _hk_control(self.txt_hotkey_quick_note, self.btn_clear_hk_quick_note),
+ )
+ self._row_hotkey_screenshot = SettingRow(
+ self._tr("settings.general.hotkey_screenshot.label", "Tam ekran görüntüsü al"),
+ self._tr("settings.general.hotkey_screenshot.desc", "Ekranın tamamının ekran görüntüsünü al."),
+ _hk_control(self.txt_hotkey_screenshot, self.btn_clear_hk_screenshot),
+ )
+ self._row_hotkey_ocr = SettingRow(
+ self._tr("settings.general.hotkey_ocr.label", "Ekran OCR (Yazı tanı)"),
+ self._tr("settings.general.hotkey_ocr.desc", "Seçilen bölgedeki yazıyı tanı."),
+ _hk_control(self.txt_hotkey_ocr, self.btn_clear_hk_ocr),
+ )
+ self._row_hotkey_snip = SettingRow(
+ self._tr("settings.general.hotkey_snip.label", "Ekran Alıntısı (Lightshot)"),
+ self._tr("settings.general.hotkey_snip.desc", "Bölgesel ekran alıntısı aracı."),
+ _hk_control(self.txt_hotkey_snip, self.btn_clear_hk_snip),
+ )
+
+ form_g.addWidget(self._row_startup)
+ form_g.addWidget(self._row_lang)
+ form_g.addWidget(self._row_hotkey)
+ form_g.addWidget(self._row_hotkey_paste)
+ form_g.addWidget(self._row_hotkey_quick_note)
+ form_g.addWidget(self._row_hotkey_screenshot)
+ form_g.addWidget(self._row_hotkey_ocr)
+ form_g.addWidget(self._row_hotkey_snip)
+ form_g.addStretch(1)
+
+ # ---- Appearance ----
+ appearance_inner = QVBoxLayout(self.tab_appearance)
+ appearance_inner.setContentsMargins(0, 0, 31, 8)
+ appearance_inner.setSpacing(0)
+ self.cmb_theme = QComboBox()
+ self.cmb_theme.setMinimumWidth(180)
+ for key, label in THEMES:
+ self.cmb_theme.addItem(label, key)
+ self.cmb_theme.setCurrentIndex(max(0, self.cmb_theme.findData(settings.get("theme", "default"))))
+ self.tgl_animations = ToggleSwitch(checked=bool(settings.get("animations", True)))
+ self._row_theme = SettingRow(
+ self._tr("settings.appearance.theme", "Tema"),
+ self._tr("settings.appearance.theme.desc", "Uygulama renk teması."),
+ self.cmb_theme,
+ )
+ self._row_animations = SettingRow(
+ self._tr("settings.appearance.animations", "Animasyonları etkinleştir"),
+ self._tr("settings.appearance.animations.desc", "Geçiş ve mikro animasyonları göster."),
+ self.tgl_animations,
+ )
+ appearance_inner.addWidget(self._row_theme)
+ appearance_inner.addWidget(self._row_animations)
+
+ # Toplu silme
+ self.btn_bulk_delete = QPushButton("Toplu silme…")
+ self.btn_bulk_delete.setMinimumHeight(34)
+ self.btn_bulk_delete.clicked.connect(self._open_bulk_delete)
+ self._row_bulk_delete = SettingRow(
+ "Silme işlemleri",
+ "Pano, not ve diğer verileri toplu olarak temizle.",
+ self.btn_bulk_delete,
+ )
+ appearance_inner.addWidget(self._row_bulk_delete)
+ appearance_inner.addStretch(1)
+
+ # ---- Behavior ----
+ behavior_inner = QVBoxLayout(self.tab_behavior)
+ behavior_inner.setContentsMargins(0, 0, 31, 8)
+ behavior_inner.setSpacing(0)
+ self.tgl_hide_after_copy = ToggleSwitch(checked=bool(settings.get("hide_after_copy", False)))
+ self.tgl_stay_on_top = ToggleSwitch(checked=bool(settings.get("stay_on_top", False)))
+ self.spn_max_items = QSpinBox()
+ self.spn_max_items.setRange(100, 5000)
+ self.spn_max_items.setValue(int(settings.get("max_items", 1000)))
+ self.spn_max_items.setMinimumWidth(120)
+ self.spn_dedupe_ms = QSpinBox()
+ self.spn_dedupe_ms.setRange(0, 10000)
+ self.spn_dedupe_ms.setValue(int(settings.get("dedupe_window_ms", 1200)))
+ self.spn_dedupe_ms.setMinimumWidth(120)
+ self.tgl_confirm_delete = ToggleSwitch(checked=bool(settings.get("confirm_delete", True)))
+ self.tgl_toast = ToggleSwitch(checked=bool(settings.get("show_toast", True)))
+
+ behavior_inner.addWidget(SettingRow(
+ self._tr("settings.behavior.hide_after_copy", "Kopyalama sonrası gizle"),
+ "Öğe panoya kopyalandığında pencereyi gizle.",
+ self.tgl_hide_after_copy,
+ ))
+ behavior_inner.addWidget(SettingRow(
+ self._tr("settings.behavior.stay_on_top", "Pencereyi üstte tut"),
+ "Ana pencereyi diğer pencerelerin üstünde tut.",
+ self.tgl_stay_on_top,
+ ))
+ behavior_inner.addWidget(SettingRow(
+ self._tr("settings.behavior.max_items", "Maksimum öğe sayısı"),
+ "Geçmişte tutulacak maksimum kopya sayısı.",
+ self.spn_max_items,
+ ))
+ behavior_inner.addWidget(SettingRow(
+ self._tr("settings.behavior.dedupe_ms", "Tekrar engelleme süresi (ms)"),
+ "Aynı içeriğin yeniden kaydedilmesini engelleyen süre.",
+ self.spn_dedupe_ms,
+ ))
+ behavior_inner.addWidget(SettingRow(
+ self._tr("settings.behavior.confirm_delete", "Silmeden önce onayla"),
+ "Öğeleri silmeden önce onay iste.",
+ self.tgl_confirm_delete,
+ ))
+ behavior_inner.addWidget(SettingRow(
+ self._tr("settings.behavior.show_toast", "Uygulama içi bildirimleri göster"),
+ "Kısa toast bildirimlerini göster.",
+ self.tgl_toast,
+ ))
+ behavior_inner.addStretch(1)
+
+ # Güvenlik tab'ı için scroll area
+ security_scroll = QScrollArea(self.tab_security)
+ security_scroll.setWidgetResizable(True)
+ security_scroll.setFrameShape(QFrame.Shape.NoFrame)
+ security_content = QWidget()
+ security_content.setObjectName("SettingsPage")
+ form_s = QFormLayout(security_content)
+ form_s.setContentsMargins(0, 8, 31, 8)
+ form_s.setSpacing(14)
+ security_scroll.setWidget(security_content)
+
+ security_layout = QVBoxLayout(self.tab_security)
+ security_layout.setContentsMargins(0, 0, 0, 0)
+ security_layout.addWidget(security_scroll)
+
+ self.tgl_encrypt = ToggleSwitch(checked=bool(settings.get("encrypt_data", False)))
+ form_s.addRow("Panoyu ve notları şifrele (AES-256)", self.tgl_encrypt)
+
+ # Google Authenticator (TOTP)
+ form_s.addRow(QLabel("")) # Boşluk
+ totp_header = QLabel("📱 Google Authenticator (TOTP)")
+ form_s.addRow(totp_header)
+
+ # TOTP durum etiketi
+ self.lbl_totp_status = QLabel("⏳ Kontrol ediliyor...")
+ form_s.addRow("Durum:", self.lbl_totp_status)
+
+ # TOTP kurulum/kaldır butonu
+ totp_btn_layout = QHBoxLayout()
+ self.btn_totp_setup = QPushButton("🔐 2FA Kur")
+ self.btn_totp_setup.clicked.connect(self._setup_totp)
+ totp_btn_layout.addWidget(self.btn_totp_setup)
+
+ self.btn_totp_remove = QPushButton("🗑️ 2FA Kaldır")
+ self.btn_totp_remove.clicked.connect(self._remove_totp)
+ self.btn_totp_remove.hide()
+ totp_btn_layout.addWidget(self.btn_totp_remove)
+
+ self.btn_totp_test = QPushButton("🧪 Test Et")
+ self.btn_totp_test.clicked.connect(self._test_totp)
+ self.btn_totp_test.hide()
+ totp_btn_layout.addWidget(self.btn_totp_test)
+
+ totp_btn_layout.addStretch()
+ form_s.addRow("", totp_btn_layout)
+
+ # TOTP seçenekleri
+ self.tgl_totp_on_startup = ToggleSwitch(checked=bool(settings.get("totp_on_startup", False)))
+ self.tgl_totp_on_startup.setEnabled(False)
+ form_s.addRow("Uygulama açılışında kod iste", self.tgl_totp_on_startup)
+
+ self.tgl_totp_hourly_lock = ToggleSwitch(checked=bool(settings.get("totp_hourly_lock", False)))
+ self.tgl_totp_hourly_lock.setEnabled(False)
+ form_s.addRow("Her saat başı kod iste", self.tgl_totp_hourly_lock)
+
+ self.tgl_totp_for_sensitive = ToggleSwitch(checked=bool(settings.get("totp_for_sensitive", True)))
+ self.tgl_totp_for_sensitive.setEnabled(False)
+ form_s.addRow("Hassas verileri göstermek için kod iste", self.tgl_totp_for_sensitive)
+
+ self.lbl_totp_info = QLabel("ℹ️ Google Authenticator uygulamasıyla ek güvenlik katmanı ekleyin.")
+ self.lbl_totp_info.setWordWrap(True)
+ self.lbl_totp_info.setStyleSheet("color: #666; font-size: 10px;")
+ form_s.addRow("", self.lbl_totp_info)
+
+ # TOTP durumunu kontrol et (defer)
+ # self._check_totp_status() will be called in showEvent
+
+ # Hassas Veri Koruması
+ form_s.addRow(QLabel("")) # Boşluk
+ sensitive_header = QLabel("🛡️ Hassas Veri Koruması")
+ form_s.addRow(sensitive_header)
+
+ self.tgl_sensitive_detection = ToggleSwitch(checked=bool(settings.get("sensitive_data_detection", True)))
+ form_s.addRow("Hassas veri algılamayı aktifleştir", self.tgl_sensitive_detection)
+
+ self.tgl_mask_credit_cards = ToggleSwitch(checked=bool(settings.get("mask_credit_cards", True)))
+ form_s.addRow("Kredi kartlarını maskele", self.tgl_mask_credit_cards)
+
+ self.tgl_mask_passwords = ToggleSwitch(checked=bool(settings.get("mask_passwords", True)))
+ form_s.addRow("Şifreleri maskele", self.tgl_mask_passwords)
+
+ self.tgl_mask_api_keys = ToggleSwitch(checked=bool(settings.get("mask_api_keys", True)))
+ form_s.addRow("API anahtarlarını maskele", self.tgl_mask_api_keys)
+
+ self.tgl_mask_emails = ToggleSwitch(checked=bool(settings.get("mask_emails", False)))
+ form_s.addRow("E-posta adreslerini maskele", self.tgl_mask_emails)
+
+ self.tgl_mask_phones = ToggleSwitch(checked=bool(settings.get("mask_phones", False)))
+ form_s.addRow("Telefon numaralarını maskele", self.tgl_mask_phones)
+
+ self.tgl_mask_tc_ids = ToggleSwitch(checked=bool(settings.get("mask_tc_ids", True)))
+ form_s.addRow("TC kimlik no maskele", self.tgl_mask_tc_ids)
+
+ self.tgl_mask_ibans = ToggleSwitch(checked=bool(settings.get("mask_ibans", True)))
+ form_s.addRow("IBAN numaralarını maskele", self.tgl_mask_ibans)
+
+ self.tgl_block_sensitive = ToggleSwitch(checked=bool(settings.get("block_sensitive_data", False)))
+ form_s.addRow("Hassas veriyi hiç kaydetme", self.tgl_block_sensitive)
+
+ self.cmb_auto_clear = QComboBox()
+ self.cmb_auto_clear.addItem("Kapalı", 0)
+ self.cmb_auto_clear.addItem("15 saniye", 15)
+ self.cmb_auto_clear.addItem("30 saniye", 30)
+ self.cmb_auto_clear.addItem("60 saniye", 60)
+ cur_clear = int(settings.get("auto_clear_clipboard_seconds", 30) or 0)
+ idx_clear = self.cmb_auto_clear.findData(cur_clear)
+ self.cmb_auto_clear.setCurrentIndex(idx_clear if idx_clear >= 0 else 2)
+ form_s.addRow("Hassas veriyi panodan temizle", self.cmb_auto_clear)
+
+ self.tgl_exclude_apps = ToggleSwitch(checked=bool(settings.get("exclude_apps_enabled", True)))
+ form_s.addRow("Hariç tutulan uygulamaları atla", self.tgl_exclude_apps)
+ self.txt_excluded_apps = QLineEdit(str(settings.get("excluded_apps", "")))
+ self.txt_excluded_apps.setPlaceholderText("keepass.exe,1password.exe,...")
+ form_s.addRow("Hariç tutulan .exe listesi", self.txt_excluded_apps)
+
+ self.tgl_windows_hello = ToggleSwitch(checked=bool(settings.get("windows_hello_enabled", False)))
+ form_s.addRow("Windows Hello / biyometrik kilit", self.tgl_windows_hello)
+ self.tgl_bio_startup = ToggleSwitch(checked=bool(settings.get("biometric_lock_on_startup", False)))
+ form_s.addRow("Başlangıçta kilitle", self.tgl_bio_startup)
+ self.spn_bio_timeout = QSpinBox()
+ self.spn_bio_timeout.setRange(0, 240)
+ self.spn_bio_timeout.setSuffix(" dk (0=kapalı)")
+ self.spn_bio_timeout.setValue(int(settings.get("biometric_lock_timeout", 15) or 0))
+ form_s.addRow("Hareketsizlikte kilitle", self.spn_bio_timeout)
+
+ self.lbl_sensitive_info = QLabel("ℹ️ Hassas veriler otomatik maskelenir veya tamamen engellenir.")
+ self.lbl_sensitive_info.setWordWrap(True)
+ self.lbl_sensitive_info.setStyleSheet("color: #666; font-size: 10px;")
+ form_s.addRow("", self.lbl_sensitive_info)
+
+ self.tgl_auto_delete = ToggleSwitch(checked=bool(settings.get("auto_delete_enabled", False)))
+ self.cmb_auto_delete = QComboBox()
+ for d in [7, 10, 14, 30, 60, 90, 120, 180, 365]:
+ self.cmb_auto_delete.addItem(f"{d} gün", d)
+ self.cmb_auto_delete.setCurrentIndex([7, 10, 14, 30, 60, 90, 120, 180, 365].index(settings.get("auto_delete_days", 7)))
+ self.cmb_auto_delete.setEnabled(self.tgl_auto_delete.isChecked())
+ form_s.addRow("Otomatik silme", self.tgl_auto_delete)
+ form_s.addRow("Silme süresi", self.cmb_auto_delete)
+
+ self.tgl_keep_fav = ToggleSwitch(checked=bool(settings.get("auto_delete_keep_fav", True)))
+ form_s.addRow("Favoriler silinmesin", self.tgl_keep_fav)
+
+ # Resimleri harici klasöre kaydetme
+ self.tgl_save_images_externally = ToggleSwitch(checked=bool(settings.get("save_images_externally", False)))
+ form_s.addRow("Resimleri ayrı klasöre kaydet", self.tgl_save_images_externally)
+
+ img_path_layout = QHBoxLayout()
+ self.txt_external_images_path = QLineEdit()
+ self.txt_external_images_path.setText(settings.get("external_images_path", ""))
+ self.txt_external_images_path.setEnabled(self.tgl_save_images_externally.isChecked())
+ self.btn_browse_images_path = QPushButton("...")
+ self.btn_browse_images_path.setMaximumWidth(40)
+ self.btn_browse_images_path.setEnabled(self.tgl_save_images_externally.isChecked())
+ self.btn_browse_images_path.clicked.connect(self._browse_images_folder)
+ img_path_layout.addWidget(self.txt_external_images_path, 1)
+ img_path_layout.addWidget(self.btn_browse_images_path)
+ form_s.addRow("Resim klasörü", img_path_layout)
+
+ self.lbl_images_warning = QLabel("⚠️ Bu ayar açıksa resimler şifrelenmeyecek ve klasöre doğrudan kaydedilecektir.")
+ self.lbl_images_warning.setWordWrap(True)
+ self.lbl_images_warning.setStyleSheet("color: orange; font-size: 10px;")
+ self.lbl_images_warning.setVisible(self.tgl_save_images_externally.isChecked())
+ form_s.addRow("", self.lbl_images_warning)
+
+ # OCR (Optik Karakter Tanıma) Ayarları
+ form_s.addRow(QLabel("")) # Boşluk
+ ocr_header = QLabel("🔍 OCR (Optik Karakter Tanıma)")
+ form_s.addRow(ocr_header)
+
+ self.tgl_ocr = ToggleSwitch(checked=bool(settings.get("ocr_enabled", False)))
+ form_s.addRow("Resimlerdeki yazıları tanı", self.tgl_ocr)
+
+ self.cmb_ocr_language = QComboBox()
+ self.cmb_ocr_language.addItem("Türkçe + İngilizce", "tur+eng")
+ self.cmb_ocr_language.addItem("Sadece Türkçe", "tur")
+ self.cmb_ocr_language.addItem("Sadece İngilizce", "eng")
+ self.cmb_ocr_language.addItem("Tüm Diller", "")
+ ocr_lang = settings.get("ocr_language", "tur+eng")
+ idx = self.cmb_ocr_language.findData(ocr_lang)
+ if idx >= 0:
+ self.cmb_ocr_language.setCurrentIndex(idx)
+ self.cmb_ocr_language.setEnabled(self.tgl_ocr.isChecked())
+ form_s.addRow("OCR Dili", self.cmb_ocr_language)
+
+ ocr_path_layout = QHBoxLayout()
+ self.txt_tesseract_path = QLineEdit()
+ self.txt_tesseract_path.setText(settings.get("tesseract_path", ""))
+ self.txt_tesseract_path.setPlaceholderText("Boş bırakılırsa otomatik bulunur")
+ self.txt_tesseract_path.setEnabled(self.tgl_ocr.isChecked())
+ self.btn_browse_tesseract = QPushButton("...")
+ self.btn_browse_tesseract.setMaximumWidth(40)
+ self.btn_browse_tesseract.setEnabled(self.tgl_ocr.isChecked())
+ self.btn_browse_tesseract.clicked.connect(self._browse_tesseract_path)
+ ocr_path_layout.addWidget(self.txt_tesseract_path, 1)
+ ocr_path_layout.addWidget(self.btn_browse_tesseract)
+ form_s.addRow("Tesseract Yolu", ocr_path_layout)
+
+ self.lbl_ocr_info = QLabel("ℹ️ Tesseract OCR kurulu değilse buradan indirebilirsiniz.")
+ self.lbl_ocr_info.setWordWrap(True)
+ self.lbl_ocr_info.setOpenExternalLinks(True)
+ self.lbl_ocr_info.setStyleSheet("color: #666; font-size: 10px;")
+ self.lbl_ocr_info.setVisible(self.tgl_ocr.isChecked())
+ form_s.addRow("", self.lbl_ocr_info)
+
+ def _on_auto_delete_toggle(val):
+ self.cmb_auto_delete.setEnabled(val)
+ self.tgl_auto_delete.onToggled(_on_auto_delete_toggle)
+
+ def _on_ocr_toggle(val):
+ self.cmb_ocr_language.setEnabled(val)
+ self.txt_tesseract_path.setEnabled(val)
+ self.btn_browse_tesseract.setEnabled(val)
+ self.lbl_ocr_info.setVisible(val)
+ self.tgl_ocr.onToggled(_on_ocr_toggle)
+
+ def _on_save_images_externally_toggle(val):
+ self.txt_external_images_path.setEnabled(val)
+ self.btn_browse_images_path.setEnabled(val)
+ self.lbl_images_warning.setVisible(val)
+ self.tgl_save_images_externally.onToggled(_on_save_images_externally_toggle)
+
+ # Video Tab with scroll
+ scroll_video = QScrollArea()
+ scroll_video.setWidgetResizable(True)
+ scroll_video.setFrameShape(QFrame.Shape.NoFrame)
+
+ video_content = QWidget()
+ video_content.setObjectName("SettingsPage")
+ form_v = QFormLayout(video_content)
+ form_v.setContentsMargins(0, 8, 31, 8)
+ form_v.setSpacing(14)
+
+ # Video Kalitesi
+ video_header = QLabel("📹 Video Kayıt Ayarları")
+ form_v.addRow(video_header)
+
+ # FFmpeg durum bilgisi (lazy - defer to after dialog shows)
+ self._ffmpeg_status_label = QLabel("⏳ FFmpeg kontrol ediliyor...")
+ self._ffmpeg_status_label.setStyleSheet("color: #888; padding: 6px;")
+ form_v.addRow(self._ffmpeg_status_label)
+ self._form_v = form_v # keep ref for later
+
+ # Kaydetme yolu
+ save_path_layout = QHBoxLayout()
+ self.txt_video_save_path = QLineEdit()
+ default_path = str(Path.home() / "Videos" / "ClipStack")
+ self.txt_video_save_path.setText(settings.get("video_save_path", default_path))
+ self.txt_video_save_path.setReadOnly(True)
+ save_path_layout.addWidget(self.txt_video_save_path)
+
+ self.btn_browse_video_path = QPushButton("📂")
+ self.btn_browse_video_path.setFixedWidth(40)
+ self.btn_browse_video_path.setToolTip("Kaydetme klasörünü seç")
+ self.btn_browse_video_path.clicked.connect(self._browse_video_save_path)
+ save_path_layout.addWidget(self.btn_browse_video_path)
+
+ form_v.addRow("Kaydetme Yolu:", save_path_layout)
+
+ self.cmb_video_quality = QComboBox()
+ self.cmb_video_quality.addItem("🎬 4K (3840x2160)", "4K")
+ self.cmb_video_quality.addItem("📺 1440p (2K)", "1440p")
+ self.cmb_video_quality.addItem("✨ 1080p (Full HD)", "1080p")
+ self.cmb_video_quality.addItem("📹 720p (HD)", "720p")
+ self.cmb_video_quality.addItem("📱 480p (SD)", "480p")
+ video_quality = settings.get("video_quality", "1080p")
+ idx = self.cmb_video_quality.findData(video_quality)
+ if idx >= 0:
+ self.cmb_video_quality.setCurrentIndex(idx)
+ form_v.addRow("Çözünürlük:", self.cmb_video_quality)
+
+ # FPS - FFmpeg GPU ile 60 FPS destekleniyor
+ self.cmb_video_fps = QComboBox()
+ self.cmb_video_fps.addItem("📹 30 FPS (Standart)", 30)
+ self.cmb_video_fps.addItem("🎮 60 FPS (Akıcı)", 60)
+ self.cmb_video_fps.addItem("🐌 15 FPS (Düşük)", 15)
+ video_fps = settings.get("video_fps", 30)
+ idx_fps = self.cmb_video_fps.findData(video_fps)
+ if idx_fps >= 0:
+ self.cmb_video_fps.setCurrentIndex(idx_fps)
+ else:
+ self.cmb_video_fps.setCurrentIndex(0) # 30 FPS varsayılan
+ form_v.addRow("Kare Hızı (FPS):", self.cmb_video_fps)
+
+ # Bitrate - GPU encoder için daha yüksek değerler
+ self.spn_video_bitrate = QSpinBox()
+ self.spn_video_bitrate.setRange(1000, 50000)
+ self.spn_video_bitrate.setValue(int(settings.get("video_bitrate", 8000)))
+ self.spn_video_bitrate.setSuffix(" kbps")
+ self.spn_video_bitrate.setSingleStep(1000)
+ form_v.addRow("Bit Hızı:", self.spn_video_bitrate)
+
+ # Mikrofon
+ form_v.addRow(QLabel(""))
+ mic_header = QLabel("🎤 Mikrofon Ayarları")
+ form_v.addRow(mic_header)
+
+ self.tgl_record_mic = ToggleSwitch(checked=bool(settings.get("video_record_mic", False)))
+ form_v.addRow("Mikrofon kaydet:", self.tgl_record_mic)
+
+ self.cmb_microphone = QComboBox()
+ self.cmb_microphone.addItem("Varsayılan Mikrofon", "default")
+ mic_device = settings.get("video_microphone", "default")
+ idx = self.cmb_microphone.findData(mic_device)
+ if idx >= 0:
+ self.cmb_microphone.setCurrentIndex(idx)
+ self.cmb_microphone.setEnabled(self.tgl_record_mic.isChecked())
+ form_v.addRow("Mikrofon:", self.cmb_microphone)
+
+ # Hotkeys
+ form_v.addRow(QLabel(""))
+ hotkey_header = QLabel("⌨️ Kısayol Tuşları")
+ form_v.addRow(hotkey_header)
+
+ self.txt_hotkey_video_record = KeyCaptureLineEdit()
+ self.txt_hotkey_video_record.setText(KeyCaptureLineEdit.normalize_combo(str(settings.get("hotkey_video_record", ""))))
+ self.btn_clear_hk_video_record = QPushButton()
+ try:
+ reset_icon = self.style().standardIcon(QStyle.StandardPixmap.SP_LineEditClearButton)
+ self.btn_clear_hk_video_record.setIcon(reset_icon)
+ self.btn_clear_hk_video_record.setIconSize(QSize(18, 18))
+ self.btn_clear_hk_video_record.setCursor(Qt.CursorShape.PointingHandCursor)
+ except Exception:
+ pass
+ hk_video_record_row = QHBoxLayout()
+ hk_video_record_row.addWidget(self.txt_hotkey_video_record, 1)
+ hk_video_record_row.addWidget(self.btn_clear_hk_video_record)
+ form_v.addRow("Kayıt Başlat/Durdur:", hk_video_record_row)
+
+ self.txt_hotkey_instant_replay = KeyCaptureLineEdit()
+ self.txt_hotkey_instant_replay.setText(KeyCaptureLineEdit.normalize_combo(str(settings.get("hotkey_instant_replay", ""))))
+ self.btn_clear_hk_instant_replay = QPushButton()
+ try:
+ reset_icon2 = self.style().standardIcon(QStyle.StandardPixmap.SP_LineEditClearButton)
+ self.btn_clear_hk_instant_replay.setIcon(reset_icon2)
+ self.btn_clear_hk_instant_replay.setIconSize(QSize(18, 18))
+ self.btn_clear_hk_instant_replay.setCursor(Qt.CursorShape.PointingHandCursor)
+ except Exception:
+ pass
+ hk_instant_replay_row = QHBoxLayout()
+ hk_instant_replay_row.addWidget(self.txt_hotkey_instant_replay, 1)
+ hk_instant_replay_row.addWidget(self.btn_clear_hk_instant_replay)
+ form_v.addRow("Instant Replay Kaydet:", hk_instant_replay_row)
+
+ # Instant Replay
+ form_v.addRow(QLabel(""))
+ instant_header = QLabel("⏪ Instant Replay")
+ form_v.addRow(instant_header)
+
+ self.spn_replay_buffer = QSpinBox()
+ self.spn_replay_buffer.setRange(15, 300)
+ self.spn_replay_buffer.setValue(int(settings.get("instant_replay_buffer_seconds", 30)))
+ self.spn_replay_buffer.setSuffix(" saniye")
+ form_v.addRow("Buffer Süresi:", self.spn_replay_buffer)
+
+ self.lbl_replay_info = QLabel("ℹ️ Instant Replay son N saniyeyi sürekli buffer'da tutar. Hotkey ile kaydettiğinizde son buffer kaydedilir.")
+ self.lbl_replay_info.setWordWrap(True)
+ self.lbl_replay_info.setStyleSheet("color: #666; font-size: 10px;")
+ form_v.addRow("", self.lbl_replay_info)
+
+ scroll_video.setWidget(video_content)
+
+ video_layout = QVBoxLayout(self.tab_video)
+ video_layout.setContentsMargins(0, 0, 0, 0)
+ video_layout.addWidget(scroll_video)
+
+ def _on_record_mic_toggle(val):
+ self.cmb_microphone.setEnabled(val)
+ self.tgl_record_mic.onToggled(_on_record_mic_toggle)
+ self.btn_clear_hk_video_record.clicked.connect(lambda: self.txt_hotkey_video_record.clear())
+ self.btn_clear_hk_instant_replay.clicked.connect(lambda: self.txt_hotkey_instant_replay.clear())
+
+ lay_t = QVBoxLayout(self.tab_tray)
+ lay_t.setContentsMargins(0, 8, 31, 8)
+ lay_t.setSpacing(14)
+
+ form_t = QFormLayout()
+ self.cmb_tray = QComboBox()
+ # 10 farklı tasarımlı ikon
+ tray_icons = [
+ ("📋 Klasik Pano", "assets/icons/tray/tray1.svg"),
+ ("📄 Kağıt Yığını", "assets/icons/tray/tray2.svg"),
+ ("📝 Kopyalama Oku", "assets/icons/tray/tray3.svg"),
+ ("💾 Bellek Çipi", "assets/icons/tray/tray4.svg"),
+ ("📁 Klasör", "assets/icons/tray/tray5.svg"),
+ ("📌 Raptiyeli Not", "assets/icons/tray/tray6.svg"),
+ ("✨ Sihirli Pano", "assets/icons/tray/tray7.svg"),
+ ("☁️ Bulut Sync", "assets/icons/tray/tray8.svg"),
+ ("🛡️ Güvenli Pano", "assets/icons/tray/tray9.svg"),
+ ("⚡ Şimşek Hızı", "assets/icons/tray/tray10.svg"),
+ ]
+ for label, path in tray_icons:
+ self.cmb_tray.addItem(label, path)
+
+ # Tema varyantları
+ tray_variants = [
+ ("🌙 Midnight", "assets/icons/tray/tray_default.svg"),
+ ("⚫ Carbon", "assets/icons/tray/tray_dark.svg"),
+ ("⚪ Polar", "assets/icons/tray/tray_light.svg"),
+ ("💜 Royal", "assets/icons/tray/tray_purple.svg"),
+ ("🌆 Neon", "assets/icons/tray/tray_cyberpunk.svg"),
+ ("🌅 Sunset", "assets/icons/tray/tray_sunset.svg"),
+ ("💚 Matrix", "assets/icons/tray/tray_matrix.svg"),
+ ("🌊 Ocean", "assets/icons/tray/tray_ocean.svg"),
+ ("🎮 Retro", "assets/icons/tray/tray_retro.svg"),
+ ("⬜ Minimal", "assets/icons/tray/tray_minimal.svg"),
+ ]
+ for label, rel in tray_variants:
+ self.cmb_tray.addItem(label, rel)
+ self.cmb_tray.addItem(self._tr("settings.tray.icon.custom", "Özel seç…"), "__custom__")
+ sel = self.settings.get("tray_icon", "assets/icons/tray/tray1.svg")
+ idx = self.cmb_tray.findData(sel)
+ if idx < 0:
+ idx = 0
+ self.cmb_tray.setCurrentIndex(idx)
+ form_t.addRow(self._tr("settings.tray.icon", "Tepsi ikonu"), self.cmb_tray)
+
+ self.btn_preview = QPushButton()
+ form_t.addRow("", self.btn_preview)
+
+ self.tgl_tray_notifications = ToggleSwitch(checked=bool(settings.get("tray_notifications", True)))
+ form_t.addRow(self._tr("settings.tray.notifications", "Tepsi bildirimlerini göster"), self.tgl_tray_notifications)
+ lay_t.addLayout(form_t)
+
+ form_r = QFormLayout(self.tab_reminders)
+ form_r.setContentsMargins(0, 8, 31, 8)
+ form_r.setSpacing(14)
+
+ # Bildirim türü
+ self.cmb_notification_type = QComboBox()
+ self.cmb_notification_type.setMinimumHeight(36)
+ self.cmb_notification_type.addItem(self._tr("settings.reminders.notif_system", "Sistem Bildirimi"), "system")
+ self.cmb_notification_type.addItem(self._tr("settings.reminders.notif_app", "Uygulama Bildirimi"), "app")
+ notif_type = settings.get("reminder_notification_type", "system")
+ idx = self.cmb_notification_type.findData(notif_type)
+ if idx >= 0:
+ self.cmb_notification_type.setCurrentIndex(idx)
+ form_r.addRow(self._tr("settings.reminders.notification_type", "Bildirim Türü:"), self.cmb_notification_type)
+
+ # Popup göster
+ self._last_app_popup_choice = bool(settings.get("reminder_show_popup", True))
+ self.tgl_show_popup = ToggleSwitch(checked=self._last_app_popup_choice)
+ self.tgl_show_popup.onToggled(self._on_show_popup_toggle)
+ form_r.addRow(self._tr("settings.reminders.show_popup", "Popup pencere göster"), self.tgl_show_popup)
+
+ # Ses etkin
+ self.tgl_sound = ToggleSwitch(checked=bool(settings.get("reminder_sound_enabled", True)))
+ form_r.addRow(self._tr("settings.reminders.sound_enabled", "Bildirim sesi çal"), self.tgl_sound)
+
+ # Ses dosyası seçimi
+ sound_layout = QHBoxLayout()
+ sound_layout.setSpacing(8)
+ sound_layout.setContentsMargins(0, 0, 0, 0)
+ self.cmb_sound = QComboBox()
+ self.cmb_sound.setMinimumHeight(36)
+ self.cmb_sound.addItem(self._tr("settings.reminders.sound_default", "Varsayılan (Sistem)"), "default")
+
+ from ..utils import resource_path
+ sounds_dir = resource_path("assets/sounds")
+ audio_exts = {".mp3", ".wav", ".ogg", ".m4a"}
+ if sounds_dir.exists():
+ for sound_path in sorted(sounds_dir.iterdir()):
+ if not sound_path.is_file():
+ continue
+ if sound_path.suffix.lower() not in audio_exts:
+ continue
+ display_name = self._format_sound_label(sound_path)
+ self.cmb_sound.addItem(display_name, str(sound_path))
+
+ # Özel ses seçimi
+ self.cmb_sound.addItem(self._tr("settings.reminders.sound_custom", "➕ Özel Ses Seç..."), "__custom__")
+
+ # Mevcut ayarı yükle
+ current_sound = settings.get("reminder_sound_file", "default")
+ if current_sound and current_sound != "default":
+ # Hazır seslerden biri mi kontrol et
+ found = False
+ for i in range(self.cmb_sound.count()):
+ if self.cmb_sound.itemData(i) == current_sound:
+ self.cmb_sound.setCurrentIndex(i)
+ found = True
+ break
+
+ # Hazır ses değilse, özel ses olarak ekle
+ if not found and current_sound != "__custom__":
+ custom_index = self.cmb_sound.count() - 1 # "Özel Ses Seç..." öncesi
+ self.cmb_sound.insertItem(custom_index, f"⭐ Özel: {Path(current_sound).name}", current_sound)
+ self.cmb_sound.setCurrentIndex(custom_index)
+ else:
+ self.cmb_sound.setCurrentIndex(0) # Default
+
+ self.btn_test_sound = QPushButton(self._tr("settings.reminders.test_sound", "Test"))
+ self.btn_test_sound.setMinimumHeight(36)
+ self.btn_test_sound.clicked.connect(self._test_reminder_sound)
+ self.btn_test_sound.setCursor(Qt.PointingHandCursor)
+ try:
+ self.btn_test_sound.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
+ self.btn_test_sound.setIconSize(QSize(20, 20))
+ except Exception:
+ pass
+ self.btn_test_sound.setProperty("class", "accent")
+
+ sound_layout.addWidget(self.cmb_sound, 1)
+ sound_layout.addWidget(self.btn_test_sound)
+ form_r.addRow(self._tr("settings.reminders.sound_file", "Ses Dosyası:"), sound_layout)
+
+ # Ses dosyası değişimini dinle
+ self.cmb_sound.currentIndexChanged.connect(self._on_sound_select)
+ self.cmb_notification_type.currentIndexChanged.connect(self._on_notification_type_change)
+
+ # Otomatik erteleme
+ self.tgl_auto_snooze = ToggleSwitch(checked=bool(settings.get("reminder_auto_snooze", False)))
+ form_r.addRow(self._tr("settings.reminders.auto_snooze", "Otomatik erteleme"), self.tgl_auto_snooze)
+
+ # Erteleme süresi
+ self.spn_snooze_minutes = QSpinBox()
+ self.spn_snooze_minutes.setRange(1, 60)
+ self.spn_snooze_minutes.setValue(int(settings.get("reminder_snooze_minutes", 5)))
+ self.spn_snooze_minutes.setSuffix(" " + self._tr("common.minutes", "dakika"))
+ self.spn_snooze_minutes.setEnabled(self.tgl_auto_snooze.isChecked())
+ form_r.addRow(self._tr("settings.reminders.snooze_duration", "Erteleme süresi:"), self.spn_snooze_minutes)
+
+ def _on_auto_snooze_toggle(val):
+ self.spn_snooze_minutes.setEnabled(val)
+ self.tgl_auto_snooze.onToggled(_on_auto_snooze_toggle)
+
+ # Ses kontrolü
+ def _on_sound_toggle(val):
+ self.cmb_sound.setEnabled(val)
+ self.btn_test_sound.setEnabled(val)
+ self.tgl_sound.onToggled(_on_sound_toggle)
+
+ # ==================== SENKRONİZASYON SEKMESİ ====================
+ sync_scroll = QScrollArea(self.tab_sync)
+ sync_scroll.setWidgetResizable(True)
+ sync_scroll.setFrameShape(QFrame.Shape.NoFrame)
+ sync_content = QWidget()
+ sync_content.setObjectName("SettingsPage")
+ form_sync = QFormLayout(sync_content)
+ form_sync.setContentsMargins(0, 8, 31, 8)
+ form_sync.setSpacing(14)
+ sync_scroll.setWidget(sync_content)
+
+ sync_layout = QVBoxLayout(self.tab_sync)
+ sync_layout.setContentsMargins(0, 0, 0, 0)
+ sync_layout.addWidget(sync_scroll)
+
+ # Export/Import Başlık
+ export_header = QLabel("📦 Veri Aktarımı")
+ form_sync.addRow(export_header)
+
+ # Export butonları
+ export_btns = QHBoxLayout()
+ export_btns.setSpacing(8)
+
+ self.btn_export_json = QPushButton("📤 JSON Olarak Dışa Aktar")
+ self.btn_export_json.setMinimumHeight(36)
+ self.btn_export_json.setCursor(Qt.PointingHandCursor)
+ self.btn_export_json.clicked.connect(self._export_to_json)
+ export_btns.addWidget(self.btn_export_json)
+
+ self.btn_import_json = QPushButton("📥 JSON'dan İçe Aktar")
+ self.btn_import_json.setMinimumHeight(36)
+ self.btn_import_json.setCursor(Qt.PointingHandCursor)
+ self.btn_import_json.clicked.connect(self._import_from_json)
+ export_btns.addWidget(self.btn_import_json)
+
+ form_sync.addRow("", export_btns)
+
+ # Export Info
+ export_info = QLabel("ℹ️ JSON formatında tüm pano geçmişi, notlar, hatırlatmalar ve snippet'ler dışa aktarılır.")
+ export_info.setWordWrap(True)
+ export_info.setStyleSheet("color: #666; font-size: 11px;")
+ form_sync.addRow("", export_info)
+
+ # Ayırıcı
+ form_sync.addRow(QLabel(""))
+
+ # Cloud Sync Başlık
+ cloud_header = QLabel("☁️ Bulut Senkronizasyonu")
+ form_sync.addRow(cloud_header)
+
+ # Google Drive
+ gdrive_layout = QHBoxLayout()
+ gdrive_layout.setSpacing(8)
+
+ self.btn_gdrive_connect = QPushButton("🔗 Google Drive Bağla")
+ self.btn_gdrive_connect.setMinimumHeight(36)
+ self.btn_gdrive_connect.setCursor(Qt.PointingHandCursor)
+ self.btn_gdrive_connect.clicked.connect(self._connect_google_drive)
+ gdrive_layout.addWidget(self.btn_gdrive_connect)
+
+ self.lbl_gdrive_status = QLabel("❌ Bağlı değil")
+ self.lbl_gdrive_status.setStyleSheet("color: #888;")
+ gdrive_layout.addWidget(self.lbl_gdrive_status)
+ gdrive_layout.addStretch()
+
+ form_sync.addRow("Google Drive:", gdrive_layout)
+
+ # Otomatik sync
+ self.tgl_auto_sync = ToggleSwitch(checked=bool(settings.get("auto_sync_enabled", False)))
+ self.tgl_auto_sync.setEnabled(False) # Bağlı değilken devre dışı
+ form_sync.addRow("Otomatik senkronizasyon:", self.tgl_auto_sync)
+
+ # Sync sıklığı
+ self.cmb_sync_interval = QComboBox()
+ self.cmb_sync_interval.setMinimumHeight(36)
+ self.cmb_sync_interval.addItem("Her 5 dakikada", 5)
+ self.cmb_sync_interval.addItem("Her 15 dakikada", 15)
+ self.cmb_sync_interval.addItem("Her 30 dakikada", 30)
+ self.cmb_sync_interval.addItem("Her saat", 60)
+ self.cmb_sync_interval.setEnabled(False)
+ sync_interval = settings.get("sync_interval_minutes", 15)
+ idx = self.cmb_sync_interval.findData(sync_interval)
+ if idx >= 0:
+ self.cmb_sync_interval.setCurrentIndex(idx)
+ form_sync.addRow("Senkronizasyon sıklığı:", self.cmb_sync_interval)
+
+ # Son sync
+ self.lbl_last_sync = QLabel("Son senkronizasyon: Hiç")
+ self.lbl_last_sync.setStyleSheet("color: #888; font-size: 11px;")
+ form_sync.addRow("", self.lbl_last_sync)
+
+ # Manuel sync
+ self.btn_sync_now = QPushButton("🔄 Şimdi Senkronize Et")
+ self.btn_sync_now.setMinimumHeight(36)
+ self.btn_sync_now.setEnabled(False)
+ self.btn_sync_now.setCursor(Qt.PointingHandCursor)
+ self.btn_sync_now.clicked.connect(self._sync_now)
+ form_sync.addRow("", self.btn_sync_now)
+
+ # Ayırıcı
+ form_sync.addRow(QLabel(""))
+
+ # Paylaşım Başlık
+ share_header = QLabel("🔗 Paylaşım")
+ form_sync.addRow(share_header)
+
+ # Paylaşım sunucusu
+ self.txt_share_server = QLineEdit()
+ self.txt_share_server.setPlaceholderText("https://taxclip.com")
+ self.txt_share_server.setText(settings.get("share_server_url", ""))
+ self.txt_share_server.setMinimumHeight(36)
+ form_sync.addRow("Paylaşım sunucusu:", self.txt_share_server)
+
+ share_info = QLabel("ℹ️ Paylaşım özelliği için sunucu URL'si. Boş bırakılırsa paylaşım devre dışı kalır.")
+ share_info.setWordWrap(True)
+ share_info.setStyleSheet("color: #666; font-size: 11px;")
+ form_sync.addRow("", share_info)
+
+ # Google Drive durumunu kontrol et (defer)
+ # self._check_gdrive_status() will be called in showEvent
+
+ lay_ab = QVBoxLayout(self.tab_about)
+ lay_ab.setContentsMargins(0, 8, 31, 10)
+ lay_ab.setSpacing(14)
+
+ # Product hero
+ about_hero = QFrame(self.tab_about)
+ about_hero.setObjectName("AboutHeroCard")
+ hero_layout = QHBoxLayout(about_hero)
+ hero_layout.setContentsMargins(22, 20, 22, 20)
+ hero_layout.setSpacing(16)
+
+ self.lbl_about_icon = QLabel(about_hero)
+ self.lbl_about_icon.setObjectName("AboutAppIcon")
+ self.lbl_about_icon.setFixedSize(54, 54)
+ self.lbl_about_icon.setAlignment(Qt.AlignCenter)
+ try:
+ self.lbl_about_icon.setPixmap(svg_icon("assets/icons/app.svg").pixmap(QSize(38, 38)))
+ except Exception:
+ self.lbl_about_icon.setText("T")
+ hero_layout.addWidget(self.lbl_about_icon, 0, Qt.AlignTop)
+
+ hero_text = QVBoxLayout()
+ hero_text.setSpacing(4)
+ self.lbl_about_kicker = QLabel(self._tr("about.kicker", "PANO ÇALIŞMA ALANI"))
+ self.lbl_about_kicker.setObjectName("AboutKicker")
+ self.lbl_title = QLabel("TaxClip")
+ self.lbl_title.setObjectName("AboutProductName")
+ self.lbl_desc = QLabel()
+ self.lbl_desc.setObjectName("AboutProductDesc")
+ self.lbl_desc.setWordWrap(True)
+ hero_text.addWidget(self.lbl_about_kicker)
+ hero_text.addWidget(self.lbl_title)
+ hero_text.addWidget(self.lbl_desc)
+ hero_layout.addLayout(hero_text, 1)
+
+ from ..updater import get_current_version
+ self.lbl_version = QLabel(f"v{get_current_version()}")
+ self.lbl_version.setObjectName("AboutVersionBadge")
+ self.lbl_version.setAlignment(Qt.AlignCenter)
+ hero_layout.addWidget(self.lbl_version, 0, Qt.AlignTop)
+ lay_ab.addWidget(about_hero)
+
+ # Feature summary
+ feature_row = QHBoxLayout()
+ feature_row.setSpacing(10)
+
+ def _about_feature(icon_path: str, title: str, description: str):
+ card = QFrame(self.tab_about)
+ card.setObjectName("AboutFeatureCard")
+ card_layout = QVBoxLayout(card)
+ card_layout.setContentsMargins(15, 14, 15, 14)
+ card_layout.setSpacing(7)
+ icon = QLabel(card)
+ icon.setObjectName("AboutFeatureIcon")
+ icon.setFixedSize(28, 28)
+ icon.setAlignment(Qt.AlignCenter)
+ try:
+ icon.setPixmap(svg_icon(icon_path).pixmap(QSize(18, 18)))
+ except Exception:
+ pass
+ title_label = QLabel(title, card)
+ title_label.setObjectName("AboutFeatureTitle")
+ desc_label = QLabel(description, card)
+ desc_label.setObjectName("AboutFeatureDesc")
+ desc_label.setWordWrap(True)
+ card_layout.addWidget(icon)
+ card_layout.addWidget(title_label)
+ card_layout.addWidget(desc_label)
+ card_layout.addStretch(1)
+ feature_row.addWidget(card, 1)
+
+ _about_feature(
+ "assets/icons/clipboard.svg",
+ self._tr("about.feature.organize", "Her şey tek yerde"),
+ self._tr("about.feature.organize.desc", "Metin, görsel, dosya ve not geçmişini düzenli tut."),
+ )
+ _about_feature(
+ "assets/icons/search.svg",
+ self._tr("about.feature.access", "Hızlı erişim"),
+ self._tr("about.feature.access.desc", "Arama, snippet ve listelere tek pencereden ulaş."),
+ )
+ _about_feature(
+ "assets/icons/lock.svg",
+ self._tr("about.feature.privacy", "Gizlilik odaklı"),
+ self._tr("about.feature.privacy.desc", "Hassas veri koruması ve güvenlik seçenekleri."),
+ )
+ lay_ab.addLayout(feature_row)
+
+ # Version and update status
+ update_card = QFrame(self.tab_about)
+ update_card.setObjectName("AboutUpdateCard")
+ update_layout = QHBoxLayout(update_card)
+ update_layout.setContentsMargins(16, 13, 16, 13)
+ update_layout.setSpacing(12)
+ update_text = QVBoxLayout()
+ update_text.setSpacing(2)
+ self.lbl_update_title = QLabel(self._tr("about.updates", "Sürüm ve güncellemeler"))
+ self.lbl_update_title.setObjectName("AboutSectionTitle")
+ self.lbl_update_status = QLabel(self._tr(
+ "about.updates.desc",
+ "TaxClip güncellemelerini buradan kontrol edebilirsin.",
+ ))
+ self.lbl_update_status.setObjectName("AboutSectionDesc")
+ self.lbl_update_status.setWordWrap(True)
+ update_text.addWidget(self.lbl_update_title)
+ update_text.addWidget(self.lbl_update_status)
+ update_layout.addLayout(update_text, 1)
+ self.btn_check_update = QPushButton(
+ self._tr("about.check_updates", "Güncellemeleri kontrol et"),
+ update_card,
+ )
+ self.btn_check_update.setObjectName("AboutPrimaryButton")
+ self.btn_check_update.setCursor(Qt.PointingHandCursor)
+ self.btn_check_update.clicked.connect(self._check_for_updates)
+ update_layout.addWidget(self.btn_check_update)
+ lay_ab.addWidget(update_card)
+
+ # External links
+ links_card = QFrame(self.tab_about)
+ links_card.setObjectName("AboutLinksCard")
+ links_layout = QHBoxLayout(links_card)
+ links_layout.setContentsMargins(16, 12, 16, 12)
+ links_layout.setSpacing(8)
+ links_title = QLabel(self._tr("about.links", "Bağlantılar"), links_card)
+ links_title.setObjectName("AboutSectionTitle")
+ links_layout.addWidget(links_title)
+ links_layout.addStretch(1)
+ self.btn_site = QPushButton(links_card)
+ self.btn_site.setObjectName("AboutLinkButton")
+ self.btn_site.setCursor(Qt.PointingHandCursor)
+ self.btn_site.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://miyotu.com/")))
+ self.btn_pat = QPushButton(links_card)
+ self.btn_pat.setObjectName("AboutLinkButton")
+ self.btn_pat.setCursor(Qt.PointingHandCursor)
+ self.btn_pat.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://www.patreon.com/c/Taxperia")))
+ self.btn_coffee = QPushButton(self._tr("about.support", "Destek ol"), links_card)
+ self.btn_coffee.setObjectName("AboutLinkButton")
+ self.btn_coffee.setCursor(Qt.PointingHandCursor)
+ self.btn_coffee.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://www.buymeacoffee.com/taxperia")))
+ links_layout.addWidget(self.btn_site)
+ links_layout.addWidget(self.btn_pat)
+ links_layout.addWidget(self.btn_coffee)
+ lay_ab.addWidget(links_card)
+ lay_ab.addStretch(1)
+
+ # Bring all legacy pages into the same row/divider/control alignment
+ # used by General, Appearance and Behavior.
+ for legacy_form in (form_s, form_v, form_t, form_r, form_sync):
+ modernize_form_layout(legacy_form)
+
+ # Footer: Reset defaults + Done
+ footer = QWidget()
+ footer.setObjectName("SettingsFooter")
+ footer.setFixedHeight(60)
+ btns = QHBoxLayout(footer)
+ btns.setContentsMargins(21, 11, 23, 11)
+ btns.setSpacing(10)
+ self.lbl_sidebar_hint = QLabel("Settings are saved automatically")
+ self.lbl_sidebar_hint.setObjectName("SidebarFooterHint")
+ btns.addWidget(self.lbl_sidebar_hint)
+ btns.addStretch(1)
+ self.btn_reset = QPushButton()
+ self.btn_reset.setObjectName("SettingsResetBtn")
+ self.btn_reset.setCursor(Qt.PointingHandCursor)
+ self.btn_reset.setFixedSize(104, 36)
+ self.btn_reset.clicked.connect(self._reset_defaults)
+ self.btn_ok = QPushButton()
+ self.btn_ok.setObjectName("SettingsDoneBtn")
+ self.btn_ok.setCursor(Qt.PointingHandCursor)
+ self.btn_ok.setFixedSize(64, 36)
+ # Keep apply/cancel aliases for any external refs; hide legacy apply/cancel
+ self.btn_apply = QPushButton()
+ self.btn_apply.hide()
+ self.btn_cancel = QPushButton()
+ self.btn_cancel.hide()
+ btns.addWidget(self.btn_reset)
+ btns.addWidget(self.btn_ok)
+ self._root_layout.addWidget(footer)
+
+ self.btn_apply.clicked.connect(self._apply_and_emit)
+ self.btn_ok.clicked.connect(self._apply_and_close)
+ self.btn_cancel.clicked.connect(self.reject)
+ self.cmb_theme.currentIndexChanged.connect(self._preview_selected_theme)
+ self.cmb_tray.currentIndexChanged.connect(self._on_tray_select)
+ self.btn_clear_hk.clicked.connect(lambda: self.txt_hotkey.setText("ctrl+shift+v"))
+ self.btn_clear_hk_paste.clicked.connect(lambda: self.txt_hotkey_paste.setText(""))
+ self.btn_clear_hk_quick_note.clicked.connect(lambda: self.txt_hotkey_quick_note.setText(""))
+ self.btn_clear_hk_screenshot.clicked.connect(lambda: self.txt_hotkey_screenshot.setText(""))
+ self.btn_clear_hk_ocr.clicked.connect(lambda: self.txt_hotkey_ocr.setText(""))
+ self.btn_clear_hk_snip.clicked.connect(lambda: self.txt_hotkey_snip.setText(""))
+ self.btn_preview.clicked.connect(self._preview_tray_icon)
+
+ i18n.languageChanged.connect(self.refresh_texts)
+ # Apply the large dialog stylesheet once, after the widget tree is
+ # complete. Applying it before creating every child caused repeated
+ # polish/layout work and made the Settings click appear frozen.
+ self.setStyleSheet(build_settings_style(self._settings_theme_key))
+ self._update_show_popup_state()
+ self.refresh_texts()
+ self._apply_toggle_theme_colors()
+ self._switch_page(0)
+
+ # Defer heavy checks to after dialog is visible
+ self._deferred_inited = False
+ self._gdrive_status_thread: GoogleDriveStatusThread | None = None
+ self._video_probe_thread: VideoProbeThread | None = None
+
+ def _switch_page(self, index: int):
+ if index < 0 or index >= self.stack.count():
+ return
+ self.stack.setCurrentIndex(index)
+ for i, btn in enumerate(self._nav_buttons):
+ btn.set_active(i == index)
+ glyph, title_key, title_fb, sub_key, sub_fb = _NAV_META[index]
+ self.lbl_page_title.setText(self._tr(title_key, title_fb))
+ self.lbl_page_subtitle.setText(self._tr(sub_key, sub_fb))
+ # External checks are started only when their page is actually opened.
+ # Starting both probes for the default General page made dialog startup
+ # heavier and left QThreads running when the user closed it quickly.
+ if getattr(self, "_deferred_inited", False):
+ if index == 4: # Video
+ self._check_ffmpeg_deferred()
+ elif index == 6: # Sync
+ self._check_gdrive_status_async()
+
+ def _preview_selected_theme(self):
+ theme_key = str(self.cmb_theme.currentData() or "default")
+ self._apply_settings_theme(theme_key)
+
+ def _apply_toggle_theme_colors(self):
+ for toggle in self.findChildren(ToggleSwitch):
+ toggle.setColors(
+ self._settings_palette["accent"],
+ self._settings_palette["toggle_off"],
+ self._settings_palette["knob"],
+ )
+
+ def _apply_settings_theme(self, theme_key: str):
+ self._settings_theme_key = theme_key or "default"
+ self._settings_palette = settings_palette(self._settings_theme_key)
+ self.setStyleSheet(build_settings_style(self._settings_theme_key))
+ self.btn_close_x.set_icon_color(self._settings_palette["close"])
+ for button in self._nav_buttons:
+ button.set_theme_colors(
+ self._settings_palette["nav"],
+ self._settings_palette["nav_active"],
+ )
+ self._apply_toggle_theme_colors()
+ current_index = self.stack.currentIndex()
+ if current_index >= 0:
+ self._switch_page(current_index)
+
+ def _reset_defaults(self):
+ reply = QMessageBox.question(
+ self,
+ self._tr("settings.reset.title", "Varsayılanlara sıfırla"),
+ self._tr(
+ "settings.reset.confirm",
+ "Tüm ayarlar varsayılan değerlere döndürülsün mü? Bu işlem geri alınamaz.",
+ ),
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
+ QMessageBox.StandardButton.No,
+ )
+ if reply != QMessageBox.StandardButton.Yes:
+ return
+
+ defaults = Settings(self.settings.path)._data
+
+ def _set_combo(cmb: QComboBox, value):
+ idx = cmb.findData(value)
+ if idx >= 0:
+ cmb.setCurrentIndex(idx)
+
+ _set_combo(self.cmb_lang, defaults.get("language", "tr"))
+ self.tgl_startup.setChecked(bool(defaults.get("launch_at_startup", True)))
+ self.txt_hotkey.setText(KeyCaptureLineEdit.normalize_combo(str(defaults.get("hotkey", "windows+v"))))
+ self.txt_hotkey_paste.setText("")
+ self.txt_hotkey_quick_note.setText("")
+ self.txt_hotkey_screenshot.setText(KeyCaptureLineEdit.normalize_combo(str(defaults.get("hotkey_screenshot", "ctrl+shift+s"))))
+ self.txt_hotkey_ocr.setText(KeyCaptureLineEdit.normalize_combo(str(defaults.get("hotkey_ocr", "ctrl+shift+t"))))
+ self.txt_hotkey_snip.setText("")
+
+ _set_combo(self.cmb_theme, defaults.get("theme", "default"))
+ self.tgl_animations.setChecked(bool(defaults.get("animations", True)))
+
+ self.tgl_hide_after_copy.setChecked(bool(defaults.get("hide_after_copy", False)))
+ self.tgl_stay_on_top.setChecked(bool(defaults.get("stay_on_top", False)))
+ self.spn_max_items.setValue(int(defaults.get("max_items", 1000)))
+ self.spn_dedupe_ms.setValue(int(defaults.get("dedupe_window_ms", 1200)))
+ self.tgl_confirm_delete.setChecked(bool(defaults.get("confirm_delete", True)))
+ self.tgl_toast.setChecked(bool(defaults.get("show_toast", True)))
+
+ self.tgl_encrypt.setChecked(bool(defaults.get("encrypt_data", False)))
+ self.tgl_sensitive_detection.setChecked(bool(defaults.get("sensitive_data_detection", True)))
+ self.tgl_mask_credit_cards.setChecked(bool(defaults.get("mask_credit_cards", True)))
+ self.tgl_mask_passwords.setChecked(bool(defaults.get("mask_passwords", True)))
+ self.tgl_mask_api_keys.setChecked(bool(defaults.get("mask_api_keys", True)))
+ self.tgl_mask_emails.setChecked(bool(defaults.get("mask_emails", False)))
+ self.tgl_mask_phones.setChecked(bool(defaults.get("mask_phones", False)))
+ self.tgl_mask_tc_ids.setChecked(bool(defaults.get("mask_tc_ids", True)))
+ self.tgl_mask_ibans.setChecked(bool(defaults.get("mask_ibans", True)))
+ self.tgl_block_sensitive.setChecked(bool(defaults.get("block_sensitive_data", False)))
+ _set_combo(self.cmb_auto_clear, int(defaults.get("auto_clear_clipboard_seconds", 30) or 0))
+ self.tgl_exclude_apps.setChecked(bool(defaults.get("exclude_apps_enabled", True)))
+ self.txt_excluded_apps.setText(str(defaults.get("excluded_apps", "")))
+ self.tgl_windows_hello.setChecked(bool(defaults.get("windows_hello_enabled", False)))
+ self.tgl_bio_startup.setChecked(bool(defaults.get("biometric_lock_on_startup", False)))
+ self.spn_bio_timeout.setValue(int(defaults.get("biometric_lock_timeout", 15) or 0))
+ self.tgl_auto_delete.setChecked(bool(defaults.get("auto_delete_enabled", False)))
+ _set_combo(self.cmb_auto_delete, defaults.get("auto_delete_days", 7))
+ self.tgl_keep_fav.setChecked(bool(defaults.get("auto_delete_keep_fav", True)))
+ self.tgl_save_images_externally.setChecked(bool(defaults.get("save_images_externally", False)))
+ self.txt_external_images_path.setText(str(defaults.get("external_images_path", "")))
+ self.tgl_ocr.setChecked(bool(defaults.get("ocr_enabled", False)))
+ _set_combo(self.cmb_ocr_language, defaults.get("ocr_language", "tur+eng"))
+ self.txt_tesseract_path.setText(str(defaults.get("tesseract_path", "")))
+
+ _set_combo(self.cmb_video_quality, defaults.get("video_quality", "1080p"))
+ _set_combo(self.cmb_video_fps, defaults.get("video_fps", 30))
+ self.spn_video_bitrate.setValue(int(defaults.get("video_bitrate", 8000)))
+ self.tgl_record_mic.setChecked(bool(defaults.get("video_record_mic", False)))
+ self.spn_replay_buffer.setValue(int(defaults.get("instant_replay_buffer_seconds", 30)))
+
+ _set_combo(self.cmb_tray, defaults.get("tray_icon", "assets/icons/tray/tray1.svg"))
+ self.tgl_tray_notifications.setChecked(bool(defaults.get("tray_notifications", True)))
+
+ _set_combo(self.cmb_notification_type, defaults.get("reminder_notification_type", "system"))
+ self.tgl_show_popup.setChecked(bool(defaults.get("reminder_show_popup", True)))
+ self.tgl_sound.setChecked(bool(defaults.get("reminder_sound_enabled", True)))
+ _set_combo(self.cmb_sound, "default")
+ self.tgl_auto_snooze.setChecked(bool(defaults.get("reminder_auto_snooze", False)))
+ self.spn_snooze_minutes.setValue(int(defaults.get("reminder_snooze_minutes", 5)))
+
+ self.tgl_auto_sync.setChecked(bool(defaults.get("auto_sync_enabled", False)))
+ _set_combo(self.cmb_sync_interval, defaults.get("sync_interval_minutes", 15))
+ self.txt_share_server.setText(str(defaults.get("share_server_url", "https://taxclip.com")))
+
+ self._apply_and_emit()
+ QMessageBox.information(
+ self,
+ self._tr("settings.reset.title", "Varsayılanlara sıfırla"),
+ self._tr("settings.reset.done", "Ayarlar varsayılan değerlere döndürüldü."),
+ )
+
+ def mousePressEvent(self, event):
+ if (
+ event.button() == Qt.MouseButton.LeftButton
+ and event.position().y() <= 78
+ ):
+ self._drag_offset = (
+ event.globalPosition().toPoint()
+ - self.frameGeometry().topLeft()
+ )
+ event.accept()
+ return
+ super().mousePressEvent(event)
+
+ def mouseMoveEvent(self, event):
+ if (
+ self._drag_offset is not None
+ and event.buttons() & Qt.MouseButton.LeftButton
+ ):
+ self.move(event.globalPosition().toPoint() - self._drag_offset)
+ event.accept()
+ return
+ super().mouseMoveEvent(event)
+
+ def mouseReleaseEvent(self, event):
+ self._drag_offset = None
+ super().mouseReleaseEvent(event)
+
+ def showEvent(self, event):
+ super().showEvent(event)
+ if not self._deferred_inited:
+ self._deferred_inited = True
+ QTimer.singleShot(50, self._run_deferred_init)
+
+ def _run_deferred_init(self):
+ """Heavy checks deferred to after dialog is visible."""
+ if not self.isVisible():
+ return
+ self._check_totp_status()
+ self._switch_page(self.stack.currentIndex())
+
+ def request_dispose_when_idle(self):
+ """Delete the dialog only after every child QThread has finished."""
+ self._dispose_requested = True
+ QTimer.singleShot(100, self._dispose_when_idle)
+
+ def _dispose_when_idle(self):
+ if not self._dispose_requested or self.isVisible():
+ return
+ if any(thread.isRunning() for thread in self.findChildren(QThread)):
+ QTimer.singleShot(100, self._dispose_when_idle)
+ return
+ self.deleteLater()
+
+ def _check_ffmpeg_deferred(self):
+ """FFmpeg check deferred to after dialog is shown."""
+ if self._video_probe_thread and self._video_probe_thread.isRunning():
+ return
+
+ self._ffmpeg_status_label.setText("⏳ FFmpeg ve mikrofonlar kontrol ediliyor...")
+ self._ffmpeg_status_label.setStyleSheet("color: #888; padding: 6px;")
+
+ thread = VideoProbeThread(self.settings, self)
+ thread.probe_ready.connect(self._apply_video_probe_result)
+ thread.probe_failed.connect(self._apply_video_probe_failure)
+ thread.finished.connect(thread.deleteLater)
+ self._video_probe_thread = thread
+ thread.start()
+
+ def _reload_microphone_devices(self, device_names=None):
+ """FFmpeg ile görülen mikrofonları ayarlar ekranına doldur."""
+ current_value = self.cmb_microphone.currentData() or self.settings.get("video_microphone", "default")
+ self.cmb_microphone.clear()
+ self.cmb_microphone.addItem("Varsayılan Mikrofon", "default")
+
+ seen = set()
+ for device_name in device_names or []:
+ if not device_name or device_name in seen:
+ continue
+ seen.add(device_name)
+ self.cmb_microphone.addItem(device_name, device_name)
+
+ idx = self.cmb_microphone.findData(current_value)
+ if idx < 0 and current_value and current_value != "default":
+ self.cmb_microphone.addItem(f"Özel cihaz: {current_value}", current_value)
+ idx = self.cmb_microphone.findData(current_value)
+ self.cmb_microphone.setCurrentIndex(max(idx, 0))
+ self.cmb_microphone.setEnabled(self.tgl_record_mic.isChecked())
+
+ def _apply_video_probe_result(self, available: bool, encoder_info: str, device_names: list):
+ if available:
+ self._ffmpeg_status_label.setText(f"✅ FFmpeg: Hazır | Encoder: {encoder_info}")
+ self._ffmpeg_status_label.setStyleSheet("color: #27ae60; font-weight: bold; padding: 6px; background: rgba(39,174,96,0.1); border-radius: 4px;")
+ else:
+ self._ffmpeg_status_label.setText("⚠️ FFmpeg bulunamadı! Video kayıt çalışmayacak.\nİndirin: https://www.gyan.dev/ffmpeg/builds/")
+ self._ffmpeg_status_label.setStyleSheet("color: #e74c3c; font-weight: bold; padding: 6px; background: rgba(231,76,60,0.1); border-radius: 4px;")
+ self._ffmpeg_status_label.setWordWrap(True)
+ self._ffmpeg_status_label.setOpenExternalLinks(True)
+
+ self._reload_microphone_devices(device_names)
+ self._video_probe_thread = None
+
+ def _apply_video_probe_failure(self, error: str):
+ print(f"[VIDEO] FFmpeg probe hatası: {error}")
+ self._ffmpeg_status_label.setText("⚠️ FFmpeg durumu kontrol edilemedi")
+ self._ffmpeg_status_label.setStyleSheet("color: #888; padding: 6px;")
+ self._reload_microphone_devices([])
+ self._video_probe_thread = None
+
+ def _browse_images_folder(self):
+ """Resim klasörü seç"""
+ folder = QFileDialog.getExistingDirectory(
+ self,
+ self._tr("settings.security.choose_images_folder", "Resim klasörü seç"),
+ self.txt_external_images_path.text() or str(Path.home())
+ )
+ if folder:
+ self.txt_external_images_path.setText(folder)
+ """Resim klasörü seç"""
+ folder = QFileDialog.getExistingDirectory(
+ self,
+ self._tr("settings.security.choose_images_folder", "Resim klasörü seç"),
+ self.txt_external_images_path.text() or str(Path.home())
+ )
+ if folder:
+ self.txt_external_images_path.setText(folder)
+
+ def _browse_tesseract_path(self):
+ """Tesseract.exe dosyasını seç"""
+ file, _ = QFileDialog.getOpenFileName(
+ self,
+ "Tesseract.exe dosyasını seç",
+ self.txt_tesseract_path.text() or "C:/Program Files/Tesseract-OCR",
+ "Executable Files (*.exe)"
+ )
+ if file:
+ self.txt_tesseract_path.setText(file)
+
+ def _browse_video_save_path(self):
+ """Video kaydetme yolunu seç"""
+ current_path = self.txt_video_save_path.text() or str(Path.home() / "Videos" / "ClipStack")
+ path = QFileDialog.getExistingDirectory(
+ self,
+ "Video Kaydetme Klasörünü Seç",
+ current_path
+ )
+ if path:
+ self.txt_video_save_path.setText(path)
+
+ def _check_totp_status(self):
+ """TOTP durumunu kontrol et"""
+ from ..totp_manager import TOTPManager
+
+ totp = TOTPManager(self.settings)
+
+ if not totp.is_available():
+ self.lbl_totp_status.setText("❌ pyotp yüklü değil")
+ self.lbl_totp_status.setStyleSheet("color: #f44336;")
+ self.btn_totp_setup.setEnabled(False)
+ self.lbl_totp_info.setText("ℹ️ Kurulum için: pip install pyotp qrcode[pil]")
+ return
+
+ if totp.is_enabled():
+ self.lbl_totp_status.setText("✅ Aktif")
+ self.lbl_totp_status.setStyleSheet("color: #4CAF50; font-weight: bold;")
+ self.btn_totp_setup.hide()
+ self.btn_totp_remove.show()
+ self.btn_totp_test.show()
+ self.tgl_totp_on_startup.setEnabled(True)
+ self.tgl_totp_hourly_lock.setEnabled(True)
+ self.tgl_totp_for_sensitive.setEnabled(True)
+ else:
+ self.lbl_totp_status.setText("❌ Kapalı")
+ self.lbl_totp_status.setStyleSheet("color: #888;")
+ self.btn_totp_setup.show()
+ self.btn_totp_remove.hide()
+ self.btn_totp_test.hide()
+ self.tgl_totp_on_startup.setEnabled(False)
+ self.tgl_totp_hourly_lock.setEnabled(False)
+ self.tgl_totp_for_sensitive.setEnabled(False)
+
+ def _setup_totp(self):
+ """TOTP kurulumu başlat"""
+ from .totp_dialog import TOTPSetupDialog
+
+ dialog = TOTPSetupDialog(self.settings, self)
+ dialog.setup_complete.connect(self._check_totp_status)
+ dialog.exec()
+
+ def _remove_totp(self):
+ """TOTP'yi kaldır"""
+ from ..totp_manager import TOTPManager
+ from .totp_dialog import TOTPVerifyDialog
+
+ # Önce doğrulama iste
+ verify_dialog = TOTPVerifyDialog(self.settings, self, "2FA Kaldırma Onayı")
+ if verify_dialog.exec() and verify_dialog.is_verified():
+ totp = TOTPManager(self.settings)
+ if totp.disable():
+ QMessageBox.information(
+ self,
+ "Başarılı",
+ "✅ İki faktörlü doğrulama kaldırıldı."
+ )
+ self._check_totp_status()
+ else:
+ QMessageBox.warning(
+ self,
+ "Hata",
+ "❌ 2FA kaldırılamadı."
+ )
+
+ def _test_totp(self):
+ """TOTP doğrulamasını test et"""
+ from .totp_dialog import TOTPVerifyDialog
+
+ dialog = TOTPVerifyDialog(self.settings, self, "2FA Test")
+ if dialog.exec() and dialog.is_verified():
+ QMessageBox.information(
+ self,
+ "Başarılı",
+ "✅ Doğrulama başarılı!"
+ )
+
+ def _on_sound_select(self, idx: int):
+ """Ses dosyası seçimi değişti"""
+ data = self.cmb_sound.currentData()
+ print(f"[SETTINGS] Ses seçimi değişti: idx={idx}, data={data}")
+
+ if data == "__custom__":
+ file, _ = QFileDialog.getOpenFileName(
+ self,
+ self._tr("settings.reminders.choose_sound", "Ses dosyası seç"),
+ "",
+ "Audio Files (*.mp3 *.wav *.ogg)"
+ )
+ if file:
+ print(f"[SETTINGS] Özel ses seçildi: {file}")
+ # Sonsuz döngüyü önlemek için signal'i geçici olarak kes
+ self.cmb_sound.blockSignals(True)
+
+ # "__custom__" itemını kaldır ve yeni özel ses olarak ekle
+ custom_idx = self.cmb_sound.count() - 1
+ self.cmb_sound.removeItem(custom_idx)
+
+ # Özel sesi ekle
+ self.cmb_sound.addItem(f"⭐ Özel: {Path(file).name}", file)
+ # "__custom__" seçeneğini tekrar ekle
+ self.cmb_sound.addItem(self._tr("settings.reminders.sound_custom", "➕ Özel Ses Seç..."), "__custom__")
+
+ # Yeni eklenen özel sesi seç (sondan 2. item)
+ self.cmb_sound.setCurrentIndex(self.cmb_sound.count() - 2)
+
+ # Signal'i tekrar aç
+ self.cmb_sound.blockSignals(False)
+
+ # Ayarlara kaydet VE HEMEN DISKE YAZ
+ self.settings.set("reminder_sound_file", file)
+ self.settings.save() # ← HEMEN KAYDET!
+ print(f"[SETTINGS] Ses kaydedildi ve diske yazıldı: {file}")
+ else:
+ print("[SETTINGS] Özel ses seçimi iptal edildi, default'a dönüyoruz")
+ self.cmb_sound.blockSignals(True)
+ self.cmb_sound.setCurrentIndex(0)
+ self.cmb_sound.blockSignals(False)
+
+ def _on_notification_type_change(self, idx: int):
+ self._update_show_popup_state()
+
+ def _on_show_popup_toggle(self, state: bool):
+ if self.cmb_notification_type.currentData() == "app":
+ self._last_app_popup_choice = state
+
+ def _update_show_popup_state(self):
+ notif_type = self.cmb_notification_type.currentData()
+ is_app = notif_type == "app"
+ self.tgl_show_popup.setEnabled(is_app)
+ if not is_app:
+ self.tgl_show_popup.setChecked(False)
+ else:
+ self.tgl_show_popup.setChecked(self._last_app_popup_choice)
+
+ @staticmethod
+ def _format_sound_label(sound_path: Path) -> str:
+ name = sound_path.stem.replace("_", " ").replace("-", " ").strip()
+ if name:
+ name = " ".join(part.capitalize() for part in name.split())
+ else:
+ name = sound_path.name
+ return f"🔔 {name}"
+
+ def _test_reminder_sound(self):
+ """Bildirim sesini test et"""
+ try:
+ sound_file = self.cmb_sound.currentData()
+ current_text = self.cmb_sound.currentText()
+ current_index = self.cmb_sound.currentIndex()
+
+ print(f"[SETTINGS TEST] Test ediliyor:")
+ print(f" - Index: {current_index}")
+ print(f" - Text: {current_text}")
+ print(f" - Data: {sound_file}")
+
+ if sound_file == "default" or not sound_file or sound_file == "":
+ # Windows sistem sesi
+ import winsound
+ winsound.MessageBeep(winsound.MB_ICONASTERISK)
+ print("[SETTINGS TEST] Windows default ses çalındı")
+ elif sound_file == "__custom__":
+ # Özel dosya seçilmemiş
+ print("[SETTINGS TEST] UYARI: Özel ses seçilmemiş!")
+ QMessageBox.information(
+ self,
+ "Bilgi",
+ "Lütfen önce bir ses dosyası seçin."
+ )
+ else:
+ # Özel ses dosyası
+ from pathlib import Path
+ sound_path = Path(sound_file)
+
+ print(f"[SETTINGS TEST] Ses dosyası kontrol ediliyor:")
+ print(f" - Path: {sound_path}")
+ print(f" - Absolute: {sound_path.absolute()}")
+ print(f" - Exists: {sound_path.exists()}")
+
+ if not sound_path.exists():
+ print(f"[SETTINGS TEST] HATA: Ses dosyası bulunamadı!")
+ QMessageBox.warning(
+ self,
+ self._tr("error.title", "Hata"),
+ self._tr("error.sound_not_found", "Ses dosyası bulunamadı:\n{file}", file=sound_file)
+ )
+ return
+
+ if self._sound_tester is None and is_sound_backend_available():
+ try:
+ self._sound_tester = SoundPlayer(self)
+ self._sound_tester.playbackFailed.connect(self._on_sound_test_failed)
+ except Exception as init_error:
+ print(f"[SETTINGS TEST] QtMultimedia başlatılamadı: {init_error}")
+ self._sound_tester = None
+
+ if self._sound_tester is not None:
+ try:
+ print("[SETTINGS TEST] QtMultimedia ile ses çalınıyor...")
+ self._sound_tester.stop()
+ self._sound_tester.play(sound_path)
+ return
+ except Exception as play_error:
+ print(f"[SETTINGS TEST] QtMultimedia hata verdi: {play_error}")
+ QMessageBox.warning(
+ self,
+ self._tr("error.sound_play", "Ses çalınamadı"),
+ str(play_error),
+ )
+ return
+
+ if sound_path.suffix.lower() != ".wav":
+ print("[SETTINGS TEST] WAV olmayan dosya winsound ile çalınamaz.")
+ QMessageBox.information(
+ self,
+ self._tr("error.sound_unsupported", "Desteklenmeyen format"),
+ self._tr(
+ "error.sound_unsupported.detail",
+ "Bu sistemde yalnızca WAV dosyaları test edilebilir. QtMultimedia backend'i kullanılamıyor.",
+ ),
+ )
+ return
+
+ import winsound
+ print("[SETTINGS TEST] winsound ile WAV çalınıyor...")
+ winsound.PlaySound(str(sound_path), winsound.SND_FILENAME)
+ print("[SETTINGS TEST] ✓ Özel ses çalındı (winsound)")
+ except Exception as e:
+ print(f"[SETTINGS TEST] HATA: {e}")
+ import traceback
+ traceback.print_exc()
+ QMessageBox.warning(
+ self,
+ self._tr("error.title", "Hata"),
+ self._tr("error.sound_play", "Ses çalınamadı:\n{error}", error=str(e))
+ )
+
+ def _on_sound_test_failed(self, message: str) -> None:
+ QMessageBox.warning(
+ self,
+ self._tr("settings.reminders.sound_error_title", "Ses çalınamadı"),
+ self._tr(
+ "settings.reminders.sound_error_body",
+ "Seçilen ses dosyası çalınamadı:\n{error}",
+ error=message,
+ ),
+ )
+
+ def _tr(self, key: str, fallback: str) -> str:
+ try:
+ v = i18n.t(key)
+ except Exception:
+ v = ""
+ return v if v and v != key else fallback
+
+ def refresh_texts(self):
+ self.setWindowTitle(self._tr("settings.title", "Ayarlar"))
+ self.lbl_settings_title.setText(self._tr("settings.title", "Ayarlar"))
+ self.lbl_settings_subtitle.setText(
+ self._tr("settings.subtitle", "Pano tercihleri · anında uygulanır")
+ )
+ self.lbl_sidebar_hint.setText(
+ self._tr("settings.autosave_hint", "Ayarlar otomatik kaydedilir")
+ )
+ self.btn_reset.setText(self._tr("settings.buttons.reset", "Reset defaults"))
+ self.btn_ok.setText(self._tr("settings.buttons.done", "Done"))
+ self.btn_apply.setText(self._tr("settings.buttons.apply", "Uygula"))
+ self.btn_cancel.setText(self._tr("settings.buttons.cancel", "İptal"))
+
+ for i, (glyph, title_key, title_fb, _sk, _sf) in enumerate(_NAV_META):
+ if i < len(self._nav_buttons):
+ self._nav_buttons[i].set_label(self._tr(title_key, title_fb))
+
+ # Refresh current page header
+ cur = self.stack.currentIndex()
+ if 0 <= cur < len(_NAV_META):
+ _g, tk, tf, sk, sf = _NAV_META[cur]
+ self.lbl_page_title.setText(self._tr(tk, tf))
+ self.lbl_page_subtitle.setText(self._tr(sk, sf))
+
+ self.lbl_hotkey_help.setText(self._tr("settings.general.hotkey.help", "Genel kısayol tuşu (örn: windows+v, ctrl+shift+v, alt+space)"))
+ self.btn_clear_hk.setText(self._tr("settings.general.hotkey.reset", "Sıfırla"))
+ self.btn_clear_hk_paste.setText(self._tr("settings.general.hotkey.reset", "Sıfırla"))
+ self.btn_clear_hk_quick_note.setText(self._tr("settings.general.hotkey.reset", "Sıfırla"))
+ self.btn_clear_hk_screenshot.setText(self._tr("settings.general.hotkey.reset", "Sıfırla"))
+ self.btn_clear_hk_ocr.setText(self._tr("settings.general.hotkey.reset", "Sıfırla"))
+ self.btn_clear_hk_snip.setText(self._tr("settings.general.hotkey.reset", "Sıfırla"))
+
+ if hasattr(self, "_row_startup"):
+ self._row_startup.set_texts(
+ self._tr("settings.general.launch_at_startup", "Windows ile başlat"),
+ self._tr("settings.general.launch_at_startup.desc", "Windows oturum açıldığında uygulamayı sessizce başlat."),
+ )
+ self._row_lang.set_texts(
+ self._tr("settings.general.language", "Dil"),
+ self._tr("settings.general.language.desc", "Arayüz dili. İçerik mümkün olduğunda buna uyar."),
+ )
+ self._row_hotkey.set_texts(
+ self._tr("settings.general.hotkey.label", "Ana kısayol tuşu"),
+ self._tr("settings.general.hotkey.desc", "Pencereyi göstermek / gizlemek için kısayol."),
+ )
+ self._row_hotkey_paste.set_texts(
+ self._tr("settings.general.hotkey_paste.label", "Son içeriği yapıştır"),
+ self._tr("settings.general.hotkey_paste.desc", "Son kopyalanan içeriği doğrudan yapıştır."),
+ )
+ self._row_hotkey_quick_note.set_texts(
+ self._tr("settings.general.hotkey_quick_note.label", "Hızlı not al"),
+ self._tr("settings.general.hotkey_quick_note.desc", "Hızlı not penceresini aç."),
+ )
+ self._row_hotkey_screenshot.set_texts(
+ self._tr("settings.general.hotkey_screenshot.label", "Tam ekran görüntüsü al"),
+ self._tr("settings.general.hotkey_screenshot.desc", "Ekranın tamamının ekran görüntüsünü al."),
+ )
+ self._row_hotkey_ocr.set_texts(
+ self._tr("settings.general.hotkey_ocr.label", "Ekran OCR (Yazı tanı)"),
+ self._tr("settings.general.hotkey_ocr.desc", "Seçilen bölgedeki yazıyı tanı."),
+ )
+ self._row_hotkey_snip.set_texts(
+ self._tr("settings.general.hotkey_snip.label", "Ekran Alıntısı (Lightshot)"),
+ self._tr("settings.general.hotkey_snip.desc", "Bölgesel ekran alıntısı aracı."),
+ )
+ if hasattr(self, "_row_theme"):
+ self._row_theme.set_texts(
+ self._tr("settings.appearance.theme", "Tema"),
+ self._tr("settings.appearance.theme.desc", "Uygulama renk teması."),
+ )
+ self._row_animations.set_texts(
+ self._tr("settings.appearance.animations", "Animasyonları etkinleştir"),
+ self._tr("settings.appearance.animations.desc", "Geçiş ve mikro animasyonları göster."),
+ )
+
+ self.btn_preview.setText(self._tr("settings.tray.preview", "Önizle"))
+ self.btn_test_sound.setText(self._tr("settings.reminders.test_sound", "Test"))
+ self.btn_test_sound.setToolTip(self._tr("settings.reminders.test_sound_hint", "Seçili hatırlatma sesini çal"))
+ self.lbl_about_kicker.setText(self._tr("about.kicker", "PANO ÇALIŞMA ALANI"))
+ self.lbl_title.setText("TaxClip")
+ self.lbl_desc.setText(self._tr("about.desc",
+ "A powerful clipboard manager for text, images, files, snippets, reminders, lists, and drawings. "
+ "Includes OCR, screen tools, sensitive-data protection, encryption, and Windows Hello support."))
+ self.lbl_update_title.setText(self._tr("about.updates", "Sürüm ve güncellemeler"))
+ self.btn_check_update.setText(self._tr("about.check_updates", "Güncellemeleri kontrol et"))
+ self.btn_site.setText(self._tr("about.website", "Website"))
+ self.btn_pat.setText(self._tr("about.patreon", "Patreon"))
+ self.btn_coffee.setText(self._tr("about.support", "Destek ol"))
+
+ def _on_tray_select(self, idx: int):
+ data = self.cmb_tray.currentData()
+ if data == "__custom__":
+ file, _ = QFileDialog.getOpenFileName(
+ self, self._tr("settings.tray.choose_icon", "İkon seç (.ico önerilir)"), "", "Icon (*.ico *.png *.svg)"
+ )
+ if file:
+ self.settings.set("tray_icon", file)
+ self.cmb_tray.setItemText(idx, f"{self._tr('settings.tray.custom_prefix', 'Özel')}: {Path(file).name}")
+ self.cmb_tray.setCurrentIndex(idx)
+ else:
+ self.cmb_tray.setCurrentIndex(0)
+
+ def _current_tray_icon_path(self) -> str:
+ data = self.cmb_tray.currentData()
+ if data == "__custom__":
+ return str(self.settings.get("tray_icon", "") or "")
+ return str(data or self.settings.get("tray_icon", "") or "")
+
+ def _preview_tray_icon(self):
+ path = self._current_tray_icon_path()
+ if not path:
+ return
+ icon = svg_icon(path, 64)
+ pix = icon.pixmap(64, 64)
+
+ dlg = QDialog(self)
+ dlg.setWindowTitle(self._tr("settings.tray.preview_title", "İkon önizleme"))
+ v = QVBoxLayout(dlg)
+ lbl = QLabel()
+ if not pix.isNull():
+ lbl.setPixmap(pix)
+ else:
+ lbl.setText(self._tr("settings.tray.preview_failed", "İkon yüklenemedi."))
+ lbl.setAlignment(Qt.AlignCenter)
+ v.addWidget(lbl)
+ btn = QPushButton(self._tr("common.close", "Kapat"))
+ btn.clicked.connect(dlg.accept)
+ v.addWidget(btn, alignment=Qt.AlignCenter)
+ dlg.exec()
+
+ def _open_bulk_delete(self):
+ from .bulk_delete_dialog import BulkDeleteDialog
+
+ if self.storage is None:
+ QMessageBox.warning(self, "Toplu silme", "Depolama bağlantısı yok.")
+ return
+
+ def do_delete(targets: list):
+ try:
+ if "clips" in targets:
+ self.storage.clear_all()
+ if "notes" in targets and hasattr(self.storage, "clear_notes"):
+ self.storage.clear_notes()
+ if "reminders" in targets and hasattr(self.storage, "clear_reminders"):
+ self.storage.clear_reminders()
+ if "snippets" in targets:
+ for sn in list(self.storage.list_snippets(limit=10000) or []):
+ try:
+ self.storage.delete_snippet(sn["id"])
+ except Exception:
+ pass
+ if "todos" in targets:
+ for lst in list(self.storage.list_todo_lists(limit=10000) or []):
+ try:
+ self.storage.delete_todo_list(lst["id"])
+ except Exception:
+ pass
+ if "drawings" in targets:
+ if hasattr(self.storage, "clear_all_drawings"):
+ self.storage.clear_all_drawings()
+ else:
+ for d in list(self.storage.list_drawings(limit=10000) or []):
+ try:
+ self.storage.delete_drawing(d["id"])
+ except Exception:
+ pass
+ self._bulk_deleted = True
+ QMessageBox.information(self, "Toplu silme", "Seçilen içerikler silindi.")
+ except Exception as e:
+ QMessageBox.critical(self, "Hata", f"Silme sırasında hata: {e}")
+
+ dlg = BulkDeleteDialog(self, on_delete=do_delete)
+ dlg.exec()
+
+ def _apply_common(self):
+ self.settings.set("language", self.cmb_lang.currentData())
+ self.settings.set("launch_at_startup", self.tgl_startup.isChecked())
+ hk = KeyCaptureLineEdit.normalize_combo(self.txt_hotkey.text() or "ctrl+shift+v")
+ self.settings.set("hotkey", hk or "ctrl+shift+v")
+
+ hk_paste = KeyCaptureLineEdit.normalize_combo(self.txt_hotkey_paste.text() or "")
+ self.settings.set("hotkey_paste_last", hk_paste)
+
+ hk_quick = KeyCaptureLineEdit.normalize_combo(self.txt_hotkey_quick_note.text() or "")
+ self.settings.set("hotkey_quick_note", hk_quick)
+
+ hk_screenshot = KeyCaptureLineEdit.normalize_combo(self.txt_hotkey_screenshot.text() or "")
+ self.settings.set("hotkey_screenshot", hk_screenshot)
+
+ hk_ocr = KeyCaptureLineEdit.normalize_combo(self.txt_hotkey_ocr.text() or "")
+ self.settings.set("hotkey_ocr", hk_ocr)
+
+ hk_snip = KeyCaptureLineEdit.normalize_combo(self.txt_hotkey_snip.text() or "")
+ self.settings.set("hotkey_snip", hk_snip)
+
+ theme_key = self.cmb_theme.currentData()
+ self.settings.set("theme", theme_key)
+ self.settings.set("animations", self.tgl_animations.isChecked())
+
+ self.settings.set("hide_after_copy", self.tgl_hide_after_copy.isChecked())
+ self.settings.set("stay_on_top", self.tgl_stay_on_top.isChecked())
+ self.settings.set("max_items", self.spn_max_items.value())
+ self.settings.set("dedupe_window_ms", self.spn_dedupe_ms.value())
+ self.settings.set("confirm_delete", self.tgl_confirm_delete.isChecked())
+ self.settings.set("show_toast", self.tgl_toast.isChecked())
+
+ self.settings.set("encrypt_data", self.tgl_encrypt.isChecked())
+
+ # TOTP ayarları
+ self.settings.set("totp_on_startup", self.tgl_totp_on_startup.isChecked())
+ self.settings.set("totp_hourly_lock", self.tgl_totp_hourly_lock.isChecked())
+ self.settings.set("totp_for_sensitive", self.tgl_totp_for_sensitive.isChecked())
+
+ # Hassas veri ayarları
+ self.settings.set("sensitive_data_detection", self.tgl_sensitive_detection.isChecked())
+ self.settings.set("mask_credit_cards", self.tgl_mask_credit_cards.isChecked())
+ self.settings.set("mask_passwords", self.tgl_mask_passwords.isChecked())
+ self.settings.set("mask_api_keys", self.tgl_mask_api_keys.isChecked())
+ self.settings.set("mask_emails", self.tgl_mask_emails.isChecked())
+ self.settings.set("mask_phones", self.tgl_mask_phones.isChecked())
+ self.settings.set("mask_tc_ids", self.tgl_mask_tc_ids.isChecked())
+ self.settings.set("mask_ibans", self.tgl_mask_ibans.isChecked())
+ self.settings.set("block_sensitive_data", self.tgl_block_sensitive.isChecked())
+
+ try:
+ self.settings.set("auto_clear_clipboard_seconds", int(self.cmb_auto_clear.currentData() or 0))
+ self.settings.set("exclude_apps_enabled", self.tgl_exclude_apps.isChecked())
+ self.settings.set("excluded_apps", self.txt_excluded_apps.text().strip())
+ self.settings.set("windows_hello_enabled", self.tgl_windows_hello.isChecked())
+ self.settings.set("biometric_lock_on_startup", self.tgl_bio_startup.isChecked())
+ self.settings.set("biometric_lock_timeout", int(self.spn_bio_timeout.value()))
+ except Exception:
+ pass
+
+ self.settings.set("save_images_externally", self.tgl_save_images_externally.isChecked())
+ self.settings.set("external_images_path", self.txt_external_images_path.text())
+ self.settings.set("auto_delete_enabled", self.tgl_auto_delete.isChecked())
+ self.settings.set("auto_delete_days", self.cmb_auto_delete.currentData())
+ self.settings.set("auto_delete_keep_fav", self.tgl_keep_fav.isChecked())
+
+ # OCR ayarları
+ self.settings.set("ocr_enabled", self.tgl_ocr.isChecked())
+ self.settings.set("ocr_language", self.cmb_ocr_language.currentData())
+ self.settings.set("tesseract_path", self.txt_tesseract_path.text())
+
+ # Video ayarları
+ self.settings.set("video_save_path", self.txt_video_save_path.text())
+ self.settings.set("video_quality", self.cmb_video_quality.currentData())
+ self.settings.set("video_fps", self.cmb_video_fps.currentData())
+ self.settings.set("video_bitrate", self.spn_video_bitrate.value())
+ self.settings.set("video_record_mic", self.tgl_record_mic.isChecked())
+ self.settings.set("video_microphone", self.cmb_microphone.currentData())
+
+ hk_video_record = KeyCaptureLineEdit.normalize_combo(self.txt_hotkey_video_record.text() or "")
+ self.settings.set("hotkey_video_record", hk_video_record)
+
+ hk_instant_replay = KeyCaptureLineEdit.normalize_combo(self.txt_hotkey_instant_replay.text() or "")
+ self.settings.set("hotkey_instant_replay", hk_instant_replay)
+
+ self.settings.set("instant_replay_buffer_seconds", self.spn_replay_buffer.value())
+
+ data = self.cmb_tray.currentData()
+ if data and data != "__custom__":
+ self.settings.set("tray_icon", data)
+ self.settings.set("tray_notifications", self.tgl_tray_notifications.isChecked())
+
+ # Hatırlatma ayarları
+ self.settings.set("reminder_notification_type", self.cmb_notification_type.currentData())
+ self.settings.set("reminder_show_popup", self.tgl_show_popup.isChecked())
+ self.settings.set("reminder_sound_enabled", self.tgl_sound.isChecked())
+
+ sound_data = self.cmb_sound.currentData()
+ if sound_data and sound_data != "__custom__":
+ self.settings.set("reminder_sound_file", sound_data)
+
+ self.settings.set("reminder_auto_snooze", self.tgl_auto_snooze.isChecked())
+ self.settings.set("reminder_snooze_minutes", self.spn_snooze_minutes.value())
+
+ # Paylaşım sunucusu (whitelist doğrulaması)
+ share_url = (self.txt_share_server.text() or "").strip() or "https://taxclip.com"
+ try:
+ from .item_preview_dialog import validate_share_server_url
+ ok_url, share_base = validate_share_server_url(share_url)
+ if ok_url:
+ self.settings.set("share_server_url", share_base)
+ else:
+ # Geçersiz URL kaydedilmez; varsayılana düş
+ self.settings.set("share_server_url", "https://taxclip.com")
+ self.txt_share_server.setText("https://taxclip.com")
+ except Exception:
+ self.settings.set("share_server_url", "https://taxclip.com")
+
+ if hasattr(self, "txt_share_api_key"):
+ self.settings.set("share_api_key", self.txt_share_api_key.text().strip())
+
+ self.settings.save()
+ self._apply_settings_theme(str(theme_key or "default"))
+ try:
+ theme_manager.apply(theme_key)
+ except Exception:
+ pass
+
+ def _apply_and_emit(self):
+ self._apply_common()
+ self.applied.emit()
+
+ def _apply_and_close(self):
+ self._apply_common()
+ self.applied.emit()
+ self.accept()
+
+ # ==================== SENKRONİZASYON METODLARI ====================
+
+ def _export_to_json(self):
+ """Tüm verileri JSON dosyasına aktar"""
+ import json
+ from datetime import datetime
+
+ file_path, _ = QFileDialog.getSaveFileName(
+ self,
+ self._tr("settings.sync.export_title", "Verileri Dışa Aktar"),
+ f"taxclip_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
+ "JSON Files (*.json)"
+ )
+
+ if not file_path:
+ return
+
+ try:
+ # Storage'dan verileri al
+ from pathlib import Path
+ from clipstack.storage import Storage
+ data_dir = Path.home() / "AppData" / "Roaming" / "TaxClip"
+ storage = Storage(data_dir / "taxclip.db", self.settings)
+
+ export_data = {
+ "version": "1.0",
+ "exported_at": datetime.now().isoformat(),
+ "clips": [],
+ "notes": [],
+ "reminders": [],
+ "snippets": [],
+ "todos": [],
+ "drawings": [],
+ "settings": {}
+ }
+
+ # Klipleri al
+ clips = storage.list_items(limit=10000)
+ for clip in clips:
+ clip_entry = {
+ "id": clip.get("id"),
+ "item_type": clip.get("item_type"),
+ "text_content": clip.get("text_content"),
+ "html_content": clip.get("html_content"),
+ "favorite": clip.get("favorite"),
+ "created_at": clip.get("created_at"),
+ "ocr_text": clip.get("ocr_text")
+ }
+ # Resimleri base64 olarak ekle
+ if clip.get("image_blob"):
+ import base64
+ clip_entry["image_base64"] = base64.b64encode(clip["image_blob"]).decode("utf-8")
+ export_data["clips"].append(clip_entry)
+
+ # Notları al
+ notes = storage.list_notes(limit=10000)
+ for note in notes:
+ export_data["notes"].append({
+ "id": note.get("id"),
+ "title": note.get("title", ""),
+ "content": note.get("content"),
+ "created_at": note.get("created_at"),
+ "updated_at": note.get("updated_at")
+ })
+
+ # Hatırlatıcıları al
+ reminders = storage.list_reminders(limit=10000)
+ for rem in reminders:
+ export_data["reminders"].append({
+ "id": rem.get("id"),
+ "title": rem.get("title"),
+ "description": rem.get("description"),
+ "reminder_time": rem.get("reminder_time"),
+ "repeat": rem.get("repeat_type"),
+ "completed": rem.get("completed")
+ })
+
+ # Snippetları al
+ snippets = storage.list_snippets(limit=10000)
+ for snip in snippets:
+ export_data["snippets"].append({
+ "id": snip.get("id"),
+ "title": snip.get("title"),
+ "content": snip.get("code"),
+ "language": snip.get("language"),
+ "tags": snip.get("tags"),
+ "created_at": snip.get("created_at")
+ })
+
+ # Todoları al
+ todos = storage.list_todos()
+ for todo in todos:
+ export_data["todos"].append({
+ "id": todo.get("id"),
+ "title": todo.get("content"), # todos tablosunda 'content' kullanılıyor
+ "completed": todo.get("completed"),
+ "list_id": todo.get("list_id"),
+ "created_at": todo.get("created_at")
+ })
+
+ # Çizimleri al
+ drawings = storage.list_drawings(limit=10000)
+ for drawing in drawings:
+ export_data["drawings"].append({
+ "id": drawing.get("id"),
+ "title": drawing.get("title"),
+ "image_data": drawing.get("image"), # list_drawings 'image' olarak dönüyor
+ "created_at": drawing.get("created_at")
+ })
+
+ # Ayarları al
+ all_settings = self.settings._data.copy() if hasattr(self.settings, '_data') else {}
+ # Hassas verileri çıkar
+ safe_settings = {k: v for k, v in all_settings.items()
+ if not any(x in k.lower() for x in ['password', 'secret', 'key', 'token'])}
+ export_data["settings"] = safe_settings
+
+ # JSON'a yaz
+ with open(file_path, 'w', encoding='utf-8') as f:
+ json.dump(export_data, f, ensure_ascii=False, indent=2, default=str)
+
+ total_items = (len(export_data["clips"]) + len(export_data["notes"]) +
+ len(export_data["reminders"]) + len(export_data["snippets"]) +
+ len(export_data["todos"]) + len(export_data["drawings"]))
+
+ QMessageBox.information(
+ self,
+ self._tr("settings.sync.export_success", "Dışa Aktarma Başarılı"),
+ f"✅ {total_items} öğe başarıyla dışa aktarıldı!\n\n📁 {file_path}"
+ )
+
+ except Exception as e:
+ QMessageBox.critical(
+ self,
+ self._tr("settings.sync.export_error", "Dışa Aktarma Hatası"),
+ f"❌ Dışa aktarma başarısız:\n{str(e)}"
+ )
+
+ def _import_from_json(self):
+ """JSON dosyasından verileri içe aktar"""
+ import json
+
+ file_path, _ = QFileDialog.getOpenFileName(
+ self,
+ self._tr("settings.sync.import_title", "Verileri İçe Aktar"),
+ "",
+ "JSON Files (*.json)"
+ )
+
+ if not file_path:
+ return
+
+ # Onay iste
+ reply = QMessageBox.question(
+ self,
+ self._tr("settings.sync.import_confirm_title", "İçe Aktarma Onayı"),
+ self._tr("settings.sync.import_confirm_msg",
+ "⚠️ Mevcut verileriniz korunacak, yeni veriler eklenecek.\n\nDevam etmek istiyor musunuz?"),
+ QMessageBox.Yes | QMessageBox.No,
+ QMessageBox.No
+ )
+
+ if reply != QMessageBox.Yes:
+ return
+
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ import_data = json.load(f)
+
+ from pathlib import Path
+ from clipstack.storage import Storage
+ data_dir = Path.home() / "AppData" / "Roaming" / "TaxClip"
+ storage = Storage(data_dir / "taxclip.db", self.settings)
+
+ imported_counts = {
+ "clips": 0,
+ "notes": 0,
+ "reminders": 0,
+ "snippets": 0,
+ "todos": 0,
+ "drawings": 0
+ }
+
+ # Klipleri içe aktar
+ from clipstack.storage import ClipItemType
+ for clip in import_data.get("clips", []):
+ try:
+ item_type_val = clip.get("item_type", 1)
+ item_type = ClipItemType(item_type_val) if isinstance(item_type_val, int) else ClipItemType.TEXT
+ text_content = clip.get("text_content") or clip.get("content", "")
+ html_content = clip.get("html_content")
+ image_bytes = None
+ if clip.get("image_base64"):
+ import base64
+ image_bytes = base64.b64decode(clip["image_base64"])
+ created_at = clip.get("created_at") or datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ storage.add_item(
+ item_type=item_type,
+ text=text_content,
+ image_bytes=image_bytes,
+ html=html_content,
+ created_at=created_at,
+ ocr_text=clip.get("ocr_text")
+ )
+ imported_counts["clips"] += 1
+ except Exception:
+ pass
+
+ # Notları içe aktar
+ for note in import_data.get("notes", []):
+ try:
+ from datetime import datetime
+ storage.add_note(
+ content=note.get("content", ""),
+ created_at=note.get("created_at") or datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ )
+ imported_counts["notes"] += 1
+ except:
+ pass
+
+ # Hatırlatıcıları içe aktar
+ for rem in import_data.get("reminders", []):
+ try:
+ storage.add_reminder(
+ title=rem.get("title", "İçe Aktarılan Hatırlatıcı"),
+ description=rem.get("description", ""),
+ reminder_time=rem.get("reminder_time"),
+ repeat_type=rem.get("repeat", "none")
+ )
+ imported_counts["reminders"] += 1
+ except:
+ pass
+
+ # Snippetları içe aktar
+ for snip in import_data.get("snippets", []):
+ try:
+ storage.add_snippet(
+ title=snip.get("title", "İçe Aktarılan Snippet"),
+ code=snip.get("content", ""),
+ language=snip.get("language", "text"),
+ tags=snip.get("tags", "")
+ )
+ imported_counts["snippets"] += 1
+ except:
+ pass
+
+ # Todoları içe aktar (varsayılan liste id=1)
+ for todo in import_data.get("todos", []):
+ try:
+ storage.add_todo(
+ list_id=1, # Varsayılan liste
+ content=todo.get("title", "") or todo.get("content", "İçe Aktarılan Görev")
+ )
+ imported_counts["todos"] += 1
+ except:
+ pass
+
+ # Çizimleri içe aktar
+ for drawing in import_data.get("drawings", []):
+ try:
+ storage.add_drawing(
+ image_data=drawing.get("image_data", ""),
+ title=drawing.get("title", "İçe Aktarılan Çizim")
+ )
+ imported_counts["drawings"] += 1
+ except:
+ pass
+
+ total = sum(imported_counts.values())
+
+ QMessageBox.information(
+ self,
+ self._tr("settings.sync.import_success", "İçe Aktarma Başarılı"),
+ f"✅ Toplam {total} öğe başarıyla içe aktarıldı!\n\n"
+ f"📋 Klipler: {imported_counts['clips']}\n"
+ f"📝 Notlar: {imported_counts['notes']}\n"
+ f"⏰ Hatırlatıcılar: {imported_counts['reminders']}\n"
+ f"💻 Snippetlar: {imported_counts['snippets']}\n"
+ f"✅ Görevler: {imported_counts['todos']}\n"
+ f"🎨 Çizimler: {imported_counts['drawings']}"
+ )
+
+ except json.JSONDecodeError:
+ QMessageBox.critical(
+ self,
+ self._tr("settings.sync.import_error", "İçe Aktarma Hatası"),
+ "❌ Geçersiz JSON dosyası!"
+ )
+ except Exception as e:
+ QMessageBox.critical(
+ self,
+ self._tr("settings.sync.import_error", "İçe Aktarma Hatası"),
+ f"❌ İçe aktarma başarısız:\n{str(e)}"
+ )
+
+ def _connect_google_drive(self):
+ """Google Drive'a bağlan/bağlantıyı kes"""
+ from clipstack.gdrive_sync import GoogleDriveSync
+
+ sync = GoogleDriveSync(self.settings)
+
+ if sync.is_connected():
+ # Bağlantıyı kes
+ reply = QMessageBox.question(
+ self,
+ self._tr("settings.sync.gdrive_disconnect_title", "Bağlantıyı Kes"),
+ self._tr("settings.sync.gdrive_disconnect_msg", "Google Drive bağlantısını kesmek istediğinize emin misiniz?"),
+ QMessageBox.Yes | QMessageBox.No,
+ QMessageBox.No
+ )
+
+ if reply == QMessageBox.Yes:
+ success, msg = sync.disconnect()
+ if success:
+ QMessageBox.information(self, "Başarılı", "✅ Bağlantı kesildi")
+ else:
+ QMessageBox.warning(self, "Hata", f"❌ {msg}")
+ else:
+ # Bağlan
+ if not sync.is_available():
+ QMessageBox.warning(
+ self,
+ self._tr("settings.sync.gdrive_not_available", "Eksik Kütüphane"),
+ "Google Drive entegrasyonu için aşağıdaki kütüphaneler gerekli:\n\n"
+ "pip install google-auth-oauthlib google-api-python-client\n\n"
+ "Şimdilik JSON dışa/içe aktarma özelliklerini kullanabilirsiniz."
+ )
+ return
+
+ # OAuth akışını başlat
+ success, msg = sync.connect()
+
+ if success:
+ QMessageBox.information(
+ self,
+ self._tr("settings.sync.gdrive_connected", "Bağlandı"),
+ f"✅ {msg}"
+ )
+ else:
+ QMessageBox.warning(
+ self,
+ self._tr("settings.sync.gdrive_error", "Bağlantı Hatası"),
+ f"❌ {msg}"
+ )
+
+ self._check_gdrive_status()
+
+ def _check_gdrive_status(self):
+ """Google Drive bağlantı durumunu kontrol et"""
+ from clipstack.gdrive_sync import GoogleDriveSync
+
+ sync = GoogleDriveSync(self.settings)
+ gdrive_connected = sync.is_connected()
+
+ if gdrive_connected:
+ self.lbl_gdrive_status.setText("✅ Bağlı")
+ self.lbl_gdrive_status.setStyleSheet("color: #4CAF50; font-weight: bold;")
+ self.btn_gdrive_connect.setText("🔓 Bağlantıyı Kes")
+ self.btn_sync_now.setEnabled(True)
+ else:
+ self.lbl_gdrive_status.setText("❌ Bağlı Değil")
+ self.lbl_gdrive_status.setStyleSheet("color: #f44336; font-weight: bold;")
+ self.btn_gdrive_connect.setText("🔗 Google Drive'a Bağlan")
+ self.btn_sync_now.setEnabled(False)
+
+ def _check_gdrive_status_async(self):
+ if self._gdrive_status_thread and self._gdrive_status_thread.isRunning():
+ return
+
+ self.lbl_gdrive_status.setText("⏳ Durum kontrol ediliyor...")
+ self.lbl_gdrive_status.setStyleSheet("color: #888;")
+
+ thread = GoogleDriveStatusThread(self)
+ thread.status_ready.connect(self._apply_gdrive_status)
+ thread.status_failed.connect(self._apply_gdrive_status_failure)
+ thread.finished.connect(thread.deleteLater)
+ self._gdrive_status_thread = thread
+ thread.start()
+
+ def _apply_gdrive_status(self, connected: bool):
+ if connected:
+ self.lbl_gdrive_status.setText("✅ Bağlı")
+ self.lbl_gdrive_status.setStyleSheet("color: #4CAF50; font-weight: bold;")
+ self.btn_gdrive_connect.setText("🔓 Bağlantıyı Kes")
+ self.btn_sync_now.setEnabled(True)
+ else:
+ self.lbl_gdrive_status.setText("❌ Bağlı Değil")
+ self.lbl_gdrive_status.setStyleSheet("color: #f44336; font-weight: bold;")
+ self.btn_gdrive_connect.setText("🔗 Google Drive'a Bağlan")
+ self.btn_sync_now.setEnabled(False)
+ self._gdrive_status_thread = None
+
+ def _apply_gdrive_status_failure(self, error: str):
+ print(f"[GDRIVE] Durum kontrol hatası: {error}")
+ self.lbl_gdrive_status.setText("❌ Bağlı Değil")
+ self.lbl_gdrive_status.setStyleSheet("color: #f44336; font-weight: bold;")
+ self.btn_gdrive_connect.setText("🔗 Google Drive'a Bağlan")
+ self.btn_sync_now.setEnabled(False)
+ self._gdrive_status_thread = None
+
+ def _sync_now(self):
+ """Manuel senkronizasyon başlat - Google Drive'a yedekle"""
+ from clipstack.gdrive_sync import GoogleDriveSync
+
+ sync = GoogleDriveSync(self.settings)
+
+ if not sync.is_connected():
+ QMessageBox.warning(
+ self,
+ self._tr("settings.sync.not_connected", "Bağlı Değil"),
+ "Önce Google Drive'a bağlanmalısınız."
+ )
+ return
+
+ try:
+ # Export verilerini hazırla
+ import json
+ from datetime import datetime
+ from pathlib import Path
+ from clipstack.storage import Storage
+
+ data_dir = Path.home() / "AppData" / "Roaming" / "TaxClip"
+ storage = Storage(data_dir / "taxclip.db", self.settings)
+
+ export_data = {
+ "version": "1.0",
+ "exported_at": datetime.now().isoformat(),
+ "clips": [],
+ "notes": [],
+ "reminders": [],
+ "snippets": [],
+ "todos": [],
+ "drawings": []
+ }
+
+ # Verileri topla
+ clips = storage.list_items(limit=10000)
+ for clip in clips:
+ clip_entry = {
+ "id": clip.get("id"),
+ "item_type": clip.get("item_type"),
+ "text_content": clip.get("text_content"),
+ "html_content": clip.get("html_content"),
+ "favorite": clip.get("favorite"),
+ "created_at": clip.get("created_at"),
+ "ocr_text": clip.get("ocr_text")
+ }
+ if clip.get("image_blob"):
+ import base64
+ clip_entry["image_base64"] = base64.b64encode(clip["image_blob"]).decode("utf-8")
+ export_data["clips"].append(clip_entry)
+
+ export_data["notes"] = storage.list_notes(limit=10000)
+ export_data["reminders"] = storage.list_reminders(limit=10000)
+ export_data["snippets"] = storage.list_snippets(limit=10000)
+ export_data["todos"] = storage.list_todos()
+ export_data["drawings"] = storage.list_drawings(limit=10000)
+
+ json_data = json.dumps(export_data, ensure_ascii=False, indent=2, default=str)
+
+ # Google Drive'a yükle
+ success, result = sync.upload_backup(json_data)
+
+ if success:
+ QMessageBox.information(
+ self,
+ self._tr("settings.sync.sync_success", "Senkronizasyon Başarılı"),
+ f"✅ Veriler Google Drive'a yedeklendi!\n\nDosya ID: {result}"
+ )
+ else:
+ QMessageBox.warning(
+ self,
+ self._tr("settings.sync.sync_error", "Senkronizasyon Hatası"),
+ f"❌ {result}"
+ )
+
+ except Exception as e:
+ QMessageBox.critical(
+ self,
+ self._tr("settings.sync.sync_error", "Senkronizasyon Hatası"),
+ f"❌ Senkronizasyon başarısız:\n{str(e)}"
+ )
+
+ def _check_for_updates(self):
+ """GitHub'dan güncelleme kontrolü yap"""
+ from ..updater import Updater, show_update_dialog
+
+ self.btn_check_update.setEnabled(False)
+ self.lbl_update_status.setText("⏳ Kontrol ediliyor...")
+
+ self._updater = Updater(self.settings, self)
+
+ def on_update_available(update_info):
+ self.btn_check_update.setEnabled(True)
+ self.lbl_update_status.setText(f"✅ v{update_info['version']} mevcut!")
+ self.lbl_update_status.setStyleSheet("color: #4CAF50; font-weight: bold;")
+ show_update_dialog(self, update_info, self._updater)
+
+ def on_no_update():
+ self.btn_check_update.setEnabled(True)
+ self.lbl_update_status.setText("✓ Güncel")
+ self.lbl_update_status.setStyleSheet("color: #4CAF50;")
+
+ def on_check_failed(error):
+ self.btn_check_update.setEnabled(True)
+ self.lbl_update_status.setText("❌ Kontrol başarısız")
+ self.lbl_update_status.setStyleSheet("color: #f44336;")
+ print(f"[UPDATE] Kontrol hatası: {error}")
+
+ # Sinyalleri Updater üzerinden bağla (thread check_for_updates'te oluşturulur)
+ self._updater.update_available.connect(on_update_available)
+ self._updater.update_failed.connect(on_check_failed)
+
+ # Önce check başlat (thread oluşturulur)
+ self._updater.check_for_updates(silent=False)
+
+ # Şimdi thread oluşturuldu, no_update sinyalini bağla
+ if self._updater._checker_thread:
+ self._updater._checker_thread.no_update.connect(on_no_update)
+ self._updater._checker_thread.check_failed.connect(on_check_failed)
diff --git a/clipstack/ui/sidebar_widget.py b/clipstack/ui/sidebar_widget.py
new file mode 100644
index 0000000..a8648b2
--- /dev/null
+++ b/clipstack/ui/sidebar_widget.py
@@ -0,0 +1,235 @@
+from __future__ import annotations
+
+from typing import Dict, Optional
+
+from PySide6.QtCore import Qt, Signal, QSize
+from PySide6.QtGui import QFont
+from PySide6.QtWidgets import (
+ QWidget, QVBoxLayout, QHBoxLayout, QLabel, QToolButton,
+ QPushButton, QFrame, QScrollArea, QSizePolicy,
+)
+
+from .. import __app_name__, __version__
+from ..utils import svg_icon, resource_path
+
+
+NAV_ITEMS = [
+ ("all", "Tümü", "assets/icons/nav_all.svg"),
+ ("text", "Metin", "assets/icons/nav_text.svg"),
+ ("image", "Resim", "assets/icons/nav_image.svg"),
+ ("files", "Dosyalar", "assets/icons/nav_file.svg"),
+ ("fav", "Favoriler", "assets/icons/star.svg"),
+ ("notes", "Notlar", "assets/icons/nav_note.svg"),
+ ("reminders", "Hatırlatmalar", "assets/icons/nav_reminder.svg"),
+ ("snippets", "Snippet", "assets/icons/nav_snippet.svg"),
+ ("todos", "Listeler", "assets/icons/nav_todo.svg"),
+ ("drawings", "Çizimler", "assets/icons/nav_drawing.svg"),
+ ("video", "Video", "assets/icons/video_camera.svg"),
+]
+
+class SidebarNavButton(QPushButton):
+ def __init__(self, key: str, label: str, icon_path: str, parent=None):
+ super().__init__(parent)
+ self.key = key
+ self._label = label
+ self.setObjectName("SidebarNavButton")
+ self.setCheckable(True)
+ self.setCursor(Qt.PointingHandCursor)
+ self.setMinimumHeight(36)
+
+ lay = QHBoxLayout(self)
+ lay.setContentsMargins(10, 4, 8, 4)
+ lay.setSpacing(10)
+
+ self.icon_lbl = QLabel()
+ self.icon_lbl.setFixedSize(20, 20)
+ self.icon_lbl.setAttribute(Qt.WA_TransparentForMouseEvents)
+ try:
+ ic = svg_icon(icon_path)
+ self.icon_lbl.setPixmap(ic.pixmap(QSize(18, 18)))
+ except Exception:
+ self.icon_lbl.setText("•")
+ lay.addWidget(self.icon_lbl)
+
+ self.text_lbl = QLabel(label)
+ self.text_lbl.setObjectName("SidebarNavText")
+ self.text_lbl.setAttribute(Qt.WA_TransparentForMouseEvents)
+ lay.addWidget(self.text_lbl, 1)
+
+ self.count_lbl = QLabel("0")
+ self.count_lbl.setObjectName("SidebarCountBadge")
+ self.count_lbl.setAlignment(Qt.AlignCenter)
+ self.count_lbl.setMinimumWidth(28)
+ self.count_lbl.setAttribute(Qt.WA_TransparentForMouseEvents)
+ lay.addWidget(self.count_lbl)
+
+ # QPushButton kendi metnini kullanmasın
+ self.setText("")
+ self.setFlat(True)
+
+ def set_count(self, n: int):
+ self.count_lbl.setText(str(n) if n < 1000 else "999+")
+
+ def set_collapsed(self, collapsed: bool):
+ self.text_lbl.setVisible(not collapsed)
+ self.count_lbl.setVisible(not collapsed)
+ self.setToolTip(self._label if collapsed else "")
+
+
+class AppSidebar(QWidget):
+ """Açılır/kapanır sol menü."""
+
+ nav_changed = Signal(str)
+ settings_clicked = Signal()
+ collapse_toggled = Signal(bool)
+
+ EXPANDED_W = 228
+ COLLAPSED_W = 64
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setObjectName("AppSidebar")
+ self._collapsed = False
+ self._buttons: Dict[str, SidebarNavButton] = {}
+ self._current = "all"
+
+ self.setFixedWidth(self.EXPANDED_W)
+
+ root = QVBoxLayout(self)
+ root.setContentsMargins(8, 10, 8, 10)
+ root.setSpacing(6)
+
+ # Üst: logo + isim + collapse
+ header = QHBoxLayout()
+ header.setSpacing(8)
+ self.logo = QLabel()
+ self.logo.setFixedSize(28, 28)
+ try:
+ p = resource_path("assets/icons/logo.png")
+ if p and p.exists():
+ from PySide6.QtGui import QPixmap
+ pm = QPixmap(str(p)).scaled(28, 28, Qt.KeepAspectRatio, Qt.SmoothTransformation)
+ self.logo.setPixmap(pm)
+ else:
+ self.logo.setPixmap(svg_icon("assets/icons/clipboard.svg").pixmap(QSize(24, 24)))
+ except Exception:
+ self.logo.setText("TC")
+ header.addWidget(self.logo)
+
+ self.app_title = QLabel(__app_name__)
+ self.app_title.setObjectName("SidebarAppTitle")
+ f = QFont()
+ f.setBold(True)
+ f.setPointSize(12)
+ self.app_title.setFont(f)
+ header.addWidget(self.app_title, 1)
+
+ self.btn_collapse = QToolButton()
+ self.btn_collapse.setObjectName("SidebarCollapseBtn")
+ self.btn_collapse.setAutoRaise(True)
+ self.btn_collapse.setCursor(Qt.PointingHandCursor)
+ try:
+ self.btn_collapse.setIcon(svg_icon("assets/icons/chevron_left.svg"))
+ except Exception:
+ self.btn_collapse.setText("«")
+ self.btn_collapse.clicked.connect(self.toggle_collapse)
+ header.addWidget(self.btn_collapse)
+ root.addLayout(header)
+
+ # Nav scroll
+ scroll = QScrollArea()
+ scroll.setObjectName("SidebarScroll")
+ scroll.setWidgetResizable(True)
+ scroll.setFrameShape(QFrame.Shape.NoFrame)
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+
+ nav_host = QWidget()
+ self.nav_layout = QVBoxLayout(nav_host)
+ self.nav_layout.setContentsMargins(0, 6, 0, 6)
+ self.nav_layout.setSpacing(2)
+
+ for key, label, icon in NAV_ITEMS:
+ btn = SidebarNavButton(key, label, icon)
+ btn.clicked.connect(lambda checked=False, k=key: self._on_nav(k))
+ self.nav_layout.addWidget(btn)
+ self._buttons[key] = btn
+
+ self.nav_layout.addStretch(1)
+ scroll.setWidget(nav_host)
+ root.addWidget(scroll, 1)
+
+ # Ayarlar
+ self.btn_settings = QPushButton()
+ self.btn_settings.setObjectName("SidebarSettingsBtn")
+ self.btn_settings.setCursor(Qt.PointingHandCursor)
+ self.btn_settings.setMinimumHeight(36)
+ settings_lay = QHBoxLayout(self.btn_settings)
+ settings_lay.setContentsMargins(10, 4, 8, 4)
+ settings_lay.setSpacing(10)
+ self.settings_icon = QLabel()
+ self.settings_icon.setFixedSize(20, 20)
+ self.settings_icon.setAttribute(Qt.WA_TransparentForMouseEvents)
+ try:
+ self.settings_icon.setPixmap(svg_icon("assets/icons/gear.svg").pixmap(QSize(18, 18)))
+ except Exception:
+ pass
+ settings_lay.addWidget(self.settings_icon)
+ self.settings_text = QLabel("Ayarlar")
+ self.settings_text.setAttribute(Qt.WA_TransparentForMouseEvents)
+ settings_lay.addWidget(self.settings_text, 1)
+ self.btn_settings.setText("")
+ self.btn_settings.setFlat(True)
+ self.btn_settings.clicked.connect(self.settings_clicked.emit)
+ root.addWidget(self.btn_settings)
+
+ # Sürüm
+ self.version_lbl = QLabel(f"{__app_name__} v{__version__}")
+ self.version_lbl.setObjectName("SidebarVersion")
+ self.version_lbl.setAlignment(Qt.AlignCenter)
+ root.addWidget(self.version_lbl)
+
+ self._buttons["all"].setChecked(True)
+
+ def _on_nav(self, key: str):
+ self.set_current(key)
+ self.nav_changed.emit(key)
+
+ def set_current(self, key: str):
+ self._current = key
+ for k, btn in self._buttons.items():
+ btn.setChecked(k == key)
+
+ def set_counts(self, counts: Dict[str, int]):
+ for k, btn in self._buttons.items():
+ btn.set_count(int(counts.get(k, 0)))
+
+ def toggle_collapse(self):
+ self.set_collapsed(not self._collapsed)
+
+ def set_collapsed(self, collapsed: bool):
+ self._collapsed = collapsed
+ self.setFixedWidth(self.COLLAPSED_W if collapsed else self.EXPANDED_W)
+ self.app_title.setVisible(not collapsed)
+ self.version_lbl.setVisible(not collapsed)
+ self.settings_text.setVisible(not collapsed)
+ for btn in self._buttons.values():
+ btn.set_collapsed(collapsed)
+ try:
+ icon = "assets/icons/chevron_right.svg" if collapsed else "assets/icons/chevron_left.svg"
+ self.btn_collapse.setIcon(svg_icon(icon))
+ except Exception:
+ pass
+ self.collapse_toggled.emit(collapsed)
+ # Ayarlara yaz (varsa)
+ try:
+ win = self.window()
+ settings = getattr(win, "settings", None)
+ if settings is not None:
+ settings.set("sidebar_collapsed", collapsed)
+ settings.save()
+ except Exception:
+ pass
+
+ @property
+ def current_key(self) -> str:
+ return self._current
diff --git a/clipstack/ui/skeleton_card.py b/clipstack/ui/skeleton_card.py
new file mode 100644
index 0000000..fd80c87
--- /dev/null
+++ b/clipstack/ui/skeleton_card.py
@@ -0,0 +1,66 @@
+from __future__ import annotations
+
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QFrame
+
+
+class SkeletonCard(QWidget):
+ """Kart yüklenirken gösterilen iskelet placeholder."""
+
+ CARD_W = 260
+ CARD_H = 168
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setObjectName("SkeletonCard")
+ self.setFixedSize(self.CARD_W, self.CARD_H)
+ self.setAttribute(Qt.WA_StyledBackground, True)
+ self._view_mode = "grid"
+
+ lay = QVBoxLayout(self)
+ lay.setContentsMargins(12, 12, 12, 12)
+ lay.setSpacing(10)
+
+ top = QHBoxLayout()
+ self.b1 = self._bar(28, 28, radius=6)
+ top.addWidget(self.b1)
+ top.addStretch(1)
+ self.b2 = self._bar(20, 20, radius=10)
+ top.addWidget(self.b2)
+ self.b3 = self._bar(20, 20, radius=10)
+ top.addWidget(self.b3)
+ lay.addLayout(top)
+
+ self.line1 = self._bar(0, 12, stretch=True)
+ lay.addWidget(self.line1)
+ self.line2 = self._bar(0, 12, stretch=True)
+ lay.addWidget(self.line2)
+ self.line3 = self._bar(0, 12, stretch=True, width_ratio=0.55)
+ lay.addWidget(self.line3)
+ lay.addStretch(1)
+
+ bottom = QHBoxLayout()
+ self.b4 = self._bar(90, 10)
+ bottom.addWidget(self.b4)
+ bottom.addStretch(1)
+ self.b5 = self._bar(22, 22, radius=6)
+ bottom.addWidget(self.b5)
+ lay.addLayout(bottom)
+
+ # QGraphicsOpacityEffect, Windows/Qt raster backend'inde rounded child
+ # widget'ları önceki frame ile birlikte tekrar boyayabiliyor. Bu da tek
+ # skeleton kartı birkaç piksel kaymış iki kart gibi gösteriyordu.
+ # Skeleton kısa süre göründüğü için statik çizim daha temiz ve ucuz.
+
+ def _bar(self, w: int, h: int, radius: int = 4, stretch: bool = False, width_ratio: float = 1.0) -> QFrame:
+ f = QFrame()
+ f.setObjectName("SkeletonBar")
+ f.setFixedHeight(h)
+ if w:
+ f.setFixedWidth(w)
+ elif stretch:
+ f.setMinimumWidth(int(180 * width_ratio))
+ f.setStyleSheet(
+ f"background: #1e293b; border-radius: {radius}px; border: none;"
+ )
+ return f
diff --git a/clipstack/ui/snippet_card_widget.py b/clipstack/ui/snippet_card_widget.py
index b916147..fefe032 100644
--- a/clipstack/ui/snippet_card_widget.py
+++ b/clipstack/ui/snippet_card_widget.py
@@ -1,297 +1,126 @@
-"""
-Enhanced Snippet Card Widget - ItemWidget tarzı tasarım
-"""
-from PySide6.QtCore import Qt, Signal, QTimer
-from PySide6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
- QTextEdit, QFrame, QTabWidget, QToolButton
-)
-from PySide6.QtGui import QFont, QCursor
-from ..utils import resource_path, svg_icon
+"""Snippet kartı — ItemCard düzeni + renkli kod."""
+from __future__ import annotations
+
+from PySide6.QtCore import Qt, Signal
+from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QToolButton
-try:
- from pygments import highlight
- from pygments.lexers import get_lexer_by_name
- from pygments.formatters import HtmlFormatter
- PYGMENTS_AVAILABLE = True
-except ImportError:
- PYGMENTS_AVAILABLE = False
+from .card_common import (
+ CARD_W, CARD_H, LIST_H, format_card_date, make_type_badge, make_tool_button, highlight_code,
+ apply_card_view_mode,
+)
-class SnippetCardWidget(QFrame):
- """Snippet kartı - ItemWidget tarzı"""
-
+class SnippetCardWidget(QWidget):
on_copy_requested = Signal(str)
on_delete_requested = Signal(int)
on_favorite_toggled = Signal(int)
on_edit_requested = Signal(int)
-
- CARD_W = 260
- CARD_H = 160
-
+
+ CARD_W = CARD_W
+ CARD_H = CARD_H
+ LIST_H = LIST_H
+
def __init__(self, snippet: dict, storage, parent=None):
super().__init__(parent)
self.snippet = snippet
self.snippet_id = snippet["id"]
self.storage = storage
- self.content_loaded = False
self.is_multi_file = bool(snippet.get("is_multi_file", 0))
-
+ self._view_mode = "grid"
+
self.setObjectName("ItemCard")
- self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
+ self.setAttribute(Qt.WA_StyledBackground, True)
self.setFixedSize(self.CARD_W, self.CARD_H)
-
+ self.setCursor(Qt.PointingHandCursor)
+
layout = QVBoxLayout(self)
- layout.setContentsMargins(10, 10, 10, 10)
- layout.setSpacing(6)
-
- # Üst kısım: Başlık
+ layout.setContentsMargins(12, 12, 12, 10)
+ layout.setSpacing(8)
+
top = QHBoxLayout()
- top.setSpacing(4)
-
- # Multi-file badge
- if self.is_multi_file:
- badge = QLabel("📁")
- badge.setStyleSheet("font-size: 14px;")
- top.addWidget(badge)
-
- self.lbl_title = QLabel(self._shorten(snippet["title"], 30))
- self.lbl_title.setStyleSheet("font-weight: bold; font-size: 11px;")
+ top.setSpacing(6)
+ top.addWidget(make_type_badge("assets/icons/type_code.svg", ">"))
+
+ title = snippet.get("title") or "Snippet"
+ self.lbl_title = QLabel(title[:36] + ("…" if len(title) > 36 else ""))
+ self.lbl_title.setObjectName("ItemTitle")
+ self.lbl_title.setStyleSheet("font-weight: 600; font-size: 11px; background: transparent; border: none;")
top.addWidget(self.lbl_title, 1)
-
+
+ self.btn_fav = make_tool_button("FavButton", tip="Favori")
+ self.btn_fav.setCheckable(True)
+ self.btn_fav.setChecked(bool(snippet.get("favorite")))
+ self._apply_fav()
+ self.btn_fav.toggled.connect(self._fav)
+ top.addWidget(self.btn_fav)
+
+ self.btn_more = make_tool_button("MoreButton", "assets/icons/more_vert.svg", "Diğer", "⋮")
+ self.btn_more.clicked.connect(lambda: self.on_edit_requested.emit(self.snippet_id))
+ top.addWidget(self.btn_more)
layout.addLayout(top)
-
- # Dil etiketi
- if not self.is_multi_file:
- lang_label = QLabel(snippet["language"].upper())
- lang_label.setStyleSheet("""
- background: #4A90E2;
- color: white;
- padding: 2px 6px;
- border-radius: 3px;
- font-size: 9px;
- font-weight: bold;
- """)
- lang_label.setMaximumWidth(60)
- layout.addWidget(lang_label)
-
- # Preview - kod satırları görünsün
- self.lbl_preview = QLabel()
- self.lbl_preview.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
- self.lbl_preview.setWordWrap(True)
- self.lbl_preview.setTextFormat(Qt.TextFormat.PlainText)
- self.lbl_preview.setStyleSheet("color: #666; font-size: 9px; font-family: Consolas;")
-
- if not self.is_multi_file:
- code = snippet.get("code", "")
- # İlk 80 karakter veya 2 satır
- lines = code.split('\n')[:2]
- preview_text = '\n'.join(lines)
- if len(preview_text) > 80:
- preview_text = preview_text[:80] + "..."
- elif len(code.split('\n')) > 2:
- preview_text += "\n..."
- self.lbl_preview.setText(preview_text if preview_text else "(boş)")
+
+ self.preview = QLabel()
+ self.preview.setObjectName("ItemPreview")
+ self.preview.setAlignment(Qt.AlignLeft | Qt.AlignTop)
+ self.preview.setWordWrap(True)
+ self.preview.setTextFormat(Qt.RichText)
+ self.preview.setStyleSheet("background: transparent; border: none;")
+ if self.is_multi_file:
+ self.preview.setTextFormat(Qt.PlainText)
+ self.preview.setText("Çok dosyalı snippet")
else:
- self.lbl_preview.setText("🗂️ Multi-file snippet")
-
- layout.addWidget(self.lbl_preview, 1)
-
- # Alt bar: tarih (note_widget gibi)
- self.bottom = QHBoxLayout()
- self.bottom.setContentsMargins(0, 0, 0, 0)
- self.bottom.setSpacing(6)
-
- created_at = snippet.get("created_at", "")
- # Tarih format: "2024-11-15 10:30" -> "15 Kas 10:30"
- date_str = created_at[:16] if len(created_at) >= 16 else created_at
- self.lbl_meta = QLabel(date_str)
+ code = snippet.get("code", "") or ""
+ self.preview.setText(highlight_code(code, 320))
+ layout.addWidget(self.preview, 1)
+
+ bottom = QHBoxLayout()
+ lang = (snippet.get("language") or "").upper()
+ meta = format_card_date(str(snippet.get("created_at", "") or ""))
+ if lang:
+ meta = f"{meta} · {lang}" if meta else lang
+ code_len = len(snippet.get("code") or "")
+ if code_len:
+ meta = f"{meta} · {code_len} kr" if meta else f"{code_len} kr"
+ self.lbl_meta = QLabel(meta)
self.lbl_meta.setObjectName("MetaLabel")
- self.bottom.addWidget(self.lbl_meta, 1)
-
- # Favori ikonu
- if snippet.get("favorite"):
- self.lbl_favorite = QLabel("⭐")
- self.lbl_favorite.setStyleSheet("font-size: 12px;")
- self.bottom.addWidget(self.lbl_favorite)
-
- layout.addLayout(self.bottom)
-
- # Hover overlay
- self.hover_overlay = QWidget(self)
- self.hover_overlay.setObjectName("HoverHighlight")
- self.hover_overlay.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
- self.hover_overlay.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
- self.hover_overlay.setStyleSheet("background-color: rgba(0,0,0,0.10); border-radius: 12px;")
- self.hover_overlay.hide()
-
- # Hover toolbar
- self.toolbar = QWidget(self)
- self.toolbar.setObjectName("HoverToolbar")
- self.toolbar.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
- self.toolbar.setStyleSheet("background-color: rgba(0,0,0,0.22); border-top-left-radius: 12px; border-top-right-radius: 12px;")
- self.toolbar.hide()
-
- toolbar_layout = QHBoxLayout(self.toolbar)
- toolbar_layout.setContentsMargins(6, 4, 6, 4)
- toolbar_layout.setSpacing(6)
-
- # Copy butonu
- self.btn_copy = QToolButton()
- try:
- self.btn_copy.setIcon(svg_icon("assets/icons/copy.svg"))
- except:
- self.btn_copy.setText("📋")
- self.btn_copy.setAutoRaise(True)
- self.btn_copy.setToolTip("Kopyala")
- self.btn_copy.clicked.connect(self._copy_code)
- toolbar_layout.addWidget(self.btn_copy)
-
- # Expand butonu
- self.btn_expand = QToolButton()
- try:
- self.btn_expand.setIcon(svg_icon("assets/icons/expand.svg"))
- except:
- self.btn_expand.setText("👁️")
- self.btn_expand.setAutoRaise(True)
- self.btn_expand.setToolTip("Görüntüle")
- self.btn_expand.clicked.connect(self._expand_content)
- toolbar_layout.addWidget(self.btn_expand)
-
- # VSCode butonu
- self.btn_vscode = QToolButton()
- try:
- self.btn_vscode.setIcon(svg_icon("assets/icons/vscode.svg"))
- except:
- self.btn_vscode.setText("VS")
- self.btn_vscode.setAutoRaise(True)
- self.btn_vscode.setToolTip("VSCode'da Aç")
- self.btn_vscode.clicked.connect(self._open_in_vscode)
- toolbar_layout.addWidget(self.btn_vscode)
-
- # Edit butonu
- self.btn_edit = QToolButton()
+ self.lbl_meta.setStyleSheet("font-size: 10px; color: #94a3b8; background: transparent; border: none;")
+ bottom.addWidget(self.lbl_meta, 1)
+
+ self.btn_copy = make_tool_button("CopyButton", "assets/icons/copy.svg", "Kopyala")
+ self.btn_copy.clicked.connect(self._copy)
+ bottom.addWidget(self.btn_copy)
+ layout.addLayout(bottom)
try:
- self.btn_edit.setIcon(svg_icon("assets/icons/edit.svg"))
- except:
- self.btn_edit.setText("✏️")
- self.btn_edit.setAutoRaise(True)
- self.btn_edit.setToolTip("Düzenle")
- self.btn_edit.clicked.connect(lambda: self.on_edit_requested.emit(self.snippet_id))
- toolbar_layout.addWidget(self.btn_edit)
-
- # Delete butonu
- self.btn_delete = QToolButton()
+ mode = getattr(getattr(self.window(), "chip_bar", None), "view_mode", "grid")
+ self.set_view_mode(mode)
+ except Exception:
+ pass
+
+ def set_view_mode(self, mode: str):
+ apply_card_view_mode(self, mode, getattr(self, "preview", None))
+ self.updateGeometry()
+
+ def _apply_fav(self):
+ path = "assets/icons/star_on.svg" if self.btn_fav.isChecked() else "assets/icons/star_off.svg"
try:
- self.btn_delete.setIcon(svg_icon("assets/icons/delete.svg"))
- except:
- self.btn_delete.setText("🗑️")
- self.btn_delete.setAutoRaise(True)
- self.btn_delete.setToolTip("Sil")
- self.btn_delete.clicked.connect(lambda: self.on_delete_requested.emit(self.snippet_id))
- toolbar_layout.addWidget(self.btn_delete)
-
- def _shorten(self, text: str, max_len: int) -> str:
- """Metni kısalt"""
- if len(text) > max_len:
- return text[:max_len] + "..."
- return text
-
- def _expand_content(self):
- """İçeriği modal'da göster"""
- from .snippet_modal import SnippetModal
-
- modal = SnippetModal(self.snippet, self.storage, self)
- modal.exec()
-
- def _copy_code(self):
- """Kodu kopyala"""
+ from ..utils import svg_icon
+ self.btn_fav.setIcon(svg_icon(path))
+ except Exception:
+ pass
+
+ def _fav(self, _checked=False):
+ self._apply_fav()
+ self.on_favorite_toggled.emit(self.snippet_id)
+
+ def _copy(self):
if self.is_multi_file:
- files = self.storage.get_snippet_files(self.snippet_id)
- if files:
- code = files[0].get("content", "")
- self.on_copy_requested.emit(code)
+ self.on_copy_requested.emit(self.snippet.get("title") or "")
else:
- code = self.snippet.get("code", "")
- self.on_copy_requested.emit(code)
-
- def _open_in_vscode(self):
- """Kodu VSCode'da aç"""
- import tempfile
- import subprocess
- import os
-
- try:
- # Dil uzantısı mapping
- ext_map = {
- "python": ".py",
- "javascript": ".js",
- "typescript": ".ts",
- "java": ".java",
- "cpp": ".cpp",
- "c": ".c",
- "csharp": ".cs",
- "go": ".go",
- "rust": ".rs",
- "ruby": ".rb",
- "php": ".php",
- "swift": ".swift",
- "kotlin": ".kt",
- "html": ".html",
- "css": ".css",
- "json": ".json",
- "yaml": ".yaml",
- "xml": ".xml",
- "sql": ".sql",
- "shell": ".sh",
- "bash": ".sh",
- "powershell": ".ps1",
- }
-
- if self.is_multi_file:
- # Multi-file: geçici klasör oluştur
- files = self.storage.get_snippet_files(self.snippet_id)
- if files:
- temp_dir = tempfile.mkdtemp(prefix="taxclip_snippet_")
- for f in files:
- filename = f.get("filename", "file.txt")
- content = f.get("content", "")
- file_path = os.path.join(temp_dir, filename)
- with open(file_path, "w", encoding="utf-8") as fp:
- fp.write(content)
- subprocess.Popen(["code", temp_dir], shell=False)
- else:
- # Tek dosya
- lang = self.snippet.get("language", "txt").lower()
- ext = ext_map.get(lang, ".txt")
- title = self.snippet.get("title", "snippet")
- # Dosya adından geçersiz karakterleri kaldır
- safe_title = "".join(c for c in title if c.isalnum() or c in "._- ")[:30]
-
- with tempfile.NamedTemporaryFile(
- mode="w",
- suffix=ext,
- prefix=f"{safe_title}_",
- delete=False,
- encoding="utf-8"
- ) as f:
- f.write(self.snippet.get("code", ""))
- temp_path = f.name
-
- subprocess.Popen(["code", temp_path], shell=False)
- except Exception as e:
- print(f"VSCode açma hatası: {e}")
-
- def enterEvent(self, event):
- """Hover başlangıç"""
- self.hover_overlay.setGeometry(self.rect())
- self.hover_overlay.show()
- self.toolbar.setGeometry(0, 0, self.width(), 32)
- self.toolbar.show()
- super().enterEvent(event)
-
- def leaveEvent(self, event):
- """Hover bitiş"""
- self.hover_overlay.hide()
- self.toolbar.hide()
- super().leaveEvent(event)
+ self.on_copy_requested.emit(self.snippet.get("code") or "")
+
+ def mousePressEvent(self, event):
+ if event.button() == Qt.LeftButton:
+ child = self.childAt(event.pos())
+ if not isinstance(child, QToolButton):
+ self.on_edit_requested.emit(self.snippet_id)
+ super().mousePressEvent(event)
diff --git a/clipstack/ui/toast.py b/clipstack/ui/toast.py
index 7a646bb..a0d05af 100644
--- a/clipstack/ui/toast.py
+++ b/clipstack/ui/toast.py
@@ -1,47 +1,52 @@
-from PySide6.QtWidgets import QWidget, QLabel
-from PySide6.QtCore import Qt, QTimer, QRect
-
-class Toast(QWidget):
- def __init__(self, parent=None):
- # Child widget olarak; pencere içinde sağ üstte konumlanır.
- super().__init__(parent)
- self.setObjectName("Toast")
- self.setAttribute(Qt.WA_TransparentForMouseEvents, True)
- self.setAttribute(Qt.WA_StyledBackground, True)
-
- self._label = QLabel("", self)
- self._label.setObjectName("ToastLabel")
- self._label.setAlignment(Qt.AlignCenter)
-
- self._timer = QTimer(self)
- self._timer.setSingleShot(True)
- self._timer.timeout.connect(self.hide)
-
- self.resize(280, 44)
- self.hide()
-
- def show_message(self, text: str, ms: int = 1500):
- # Önce varsa eski zamanlayıcıyı durdur
- if self._timer.isActive():
- self._timer.stop()
- self._label.setText(text)
- self._label.setGeometry(self.rect().adjusted(10, 8, -10, -8))
- self._reposition()
- self.show()
- self.raise_()
- self._timer.start(ms)
-
- def dismiss(self):
- if self._timer.isActive():
- self._timer.stop()
- self.hide()
-
- def _reposition(self):
- # Ebeveyn pencere içinde sağ üst köşe
- if not self.parent():
- return
- margin = 16
- pw = self.parent().width()
- x = pw - self.width() - margin
- y = margin
- self.setGeometry(QRect(x, y, self.width(), self.height()))
\ No newline at end of file
+from PySide6.QtWidgets import QWidget, QLabel
+from PySide6.QtCore import Qt, QTimer, QRect
+
+
+class Toast(QWidget):
+ def __init__(self, parent=None):
+ # Child widget — ana pencere içinde alt ortada gösterilir (üst UI ile karışmasın).
+ super().__init__(parent)
+ self.setObjectName("Toast")
+ self.setAttribute(Qt.WA_TransparentForMouseEvents, True)
+ self.setAttribute(Qt.WA_StyledBackground, True)
+
+ self._label = QLabel("", self)
+ self._label.setObjectName("ToastLabel")
+ self._label.setAlignment(Qt.AlignCenter)
+ self._label.setWordWrap(True)
+
+ self._timer = QTimer(self)
+ self._timer.setSingleShot(True)
+ self._timer.timeout.connect(self.hide)
+
+ self.resize(300, 44)
+ self.hide()
+
+ def show_message(self, text: str, ms: int = 1500):
+ if self._timer.isActive():
+ self._timer.stop()
+ self._label.setText(text)
+ self._label.adjustSize()
+ needed_h = max(44, self._label.sizeHint().height() + 16)
+ self.resize(max(280, min(420, self._label.sizeHint().width() + 40)), needed_h)
+ self._label.setGeometry(self.rect().adjusted(12, 8, -12, -8))
+ self._reposition()
+ self.show()
+ self.raise_()
+ self._timer.start(ms)
+
+ def dismiss(self):
+ if self._timer.isActive():
+ self._timer.stop()
+ self.hide()
+
+ def _reposition(self):
+ # Alt ortada — başlık çubuğu / sağ üst kontrollerle çakışmaz
+ if not self.parent():
+ return
+ margin = 20
+ pw = self.parent().width()
+ ph = self.parent().height()
+ x = max(margin, (pw - self.width()) // 2)
+ y = max(margin, ph - self.height() - margin - 48) # pagination üstünde kalsın
+ self.setGeometry(QRect(x, y, self.width(), self.height()))
diff --git a/clipstack/ui/todo_card_widget_v2.py b/clipstack/ui/todo_card_widget_v2.py
index 00c22b5..f2311c0 100644
--- a/clipstack/ui/todo_card_widget_v2.py
+++ b/clipstack/ui/todo_card_widget_v2.py
@@ -1,226 +1,150 @@
-"""
-Todo Card Widget v2 - ItemWidget tarzı 260x160 format
-"""
+"""Todo liste kartı — ItemCard düzeni."""
+from __future__ import annotations
+
+from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import (
- QFrame, QVBoxLayout, QHBoxLayout, QLabel, QCheckBox, QToolButton, QWidget, QMessageBox
+ QWidget, QVBoxLayout, QHBoxLayout, QLabel, QToolButton, QMessageBox, QInputDialog,
)
-from PySide6.QtCore import Qt, Signal
-from PySide6.QtGui import QFont
-from ..utils import resource_path, svg_icon
+from .card_common import CARD_W, CARD_H, LIST_H, format_card_date, make_type_badge, make_tool_button, apply_card_view_mode
-CARD_W = 260
-CARD_H = 160
+class TodoCardWidgetV2(QWidget):
+ delete_requested = Signal(int)
+ LIST_H = LIST_H
-class TodoCardWidgetV2(QFrame):
- """Todo liste kartı - 260x160 format"""
-
- delete_requested = Signal(int) # list_id
-
def __init__(self, list_id: int, list_data: dict, storage, parent=None):
super().__init__(parent)
self.list_id = list_id
self.storage = storage
self.list_data = dict(list_data or {})
-
- self.setObjectName("TodoCard")
- self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
+ self._view_mode = "grid"
+
+ self.setObjectName("ItemCard")
+ self.setAttribute(Qt.WA_StyledBackground, True)
self.setFixedSize(CARD_W, CARD_H)
-
+ self.setCursor(Qt.PointingHandCursor)
+
layout = QVBoxLayout(self)
- layout.setContentsMargins(10, 10, 10, 10)
+ layout.setContentsMargins(12, 12, 12, 10)
layout.setSpacing(8)
-
- # Başlık
- self.title_lbl = QLabel(self.list_data.get("name", "Todo Listesi"))
- self.title_lbl.setFont(QFont("Segoe UI", 11, QFont.Weight.Bold))
- self.title_lbl.setStyleSheet("color: white;")
- layout.addWidget(self.title_lbl)
-
- # Todo items preview (max 3)
- self.items_container = QFrame()
- self.items_layout = QVBoxLayout(self.items_container)
- self.items_layout.setContentsMargins(0, 0, 0, 0)
- self.items_layout.setSpacing(4)
- layout.addWidget(self.items_container)
-
- layout.addStretch()
-
- # Load items
+
+ top = QHBoxLayout()
+ top.setSpacing(6)
+ top.addWidget(make_type_badge("assets/icons/nav_todo.svg", "✓"))
+
+ self.title_lbl = QLabel(self.list_data.get("name", "Liste"))
+ self.title_lbl.setObjectName("ItemTitle")
+ self.title_lbl.setStyleSheet("font-weight: 600; font-size: 11px; background: transparent; border: none;")
+ top.addWidget(self.title_lbl, 1)
+
+ self.btn_more = make_tool_button("MoreButton", "assets/icons/more_vert.svg", "Diğer", "⋮")
+ self.btn_more.clicked.connect(self._more_menu)
+ top.addWidget(self.btn_more)
+ layout.addLayout(top)
+
+ self.preview = QLabel()
+ self.preview.setObjectName("ItemPreview")
+ self.preview.setAlignment(Qt.AlignLeft | Qt.AlignTop)
+ self.preview.setWordWrap(True)
+ self.preview.setStyleSheet("background: transparent; border: none; color: #cbd5e1; font-size: 11px;")
+ layout.addWidget(self.preview, 1)
+
+ bottom = QHBoxLayout()
+ self.lbl_meta = QLabel("")
+ self.lbl_meta.setObjectName("MetaLabel")
+ self.lbl_meta.setStyleSheet("font-size: 10px; color: #94a3b8; background: transparent; border: none;")
+ bottom.addWidget(self.lbl_meta, 1)
+
+ self.btn_copy = make_tool_button("CopyButton", "assets/icons/expand.svg", "Aç")
+ self.btn_copy.clicked.connect(self._expand_list)
+ bottom.addWidget(self.btn_copy)
+ layout.addLayout(bottom)
+
self._load_items()
-
- # Hover overlay
- self.hover_overlay = QWidget(self)
- self.hover_overlay.setObjectName("HoverHighlight")
- self.hover_overlay.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
- self.hover_overlay.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
- self.hover_overlay.setStyleSheet("background-color: rgba(0,0,0,0.10); border-radius: 12px;")
- self.hover_overlay.hide()
-
- # Hover toolbar - notlar gibi üstte ortalı
- self.toolbar = QWidget(self)
- self.toolbar.setObjectName("HoverToolbar")
- self.toolbar.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
- self.toolbar.setStyleSheet("background-color: rgba(0,0,0,0.22); border-top-left-radius: 12px; border-top-right-radius: 12px;")
- self.toolbar.hide()
-
- toolbar_layout = QHBoxLayout(self.toolbar)
- toolbar_layout.setContentsMargins(6, 4, 6, 4)
- toolbar_layout.setSpacing(6)
-
- # Görüntüle butonu
- self.btn_expand = QToolButton()
try:
- self.btn_expand.setIcon(svg_icon("assets/icons/expand.svg"))
- except:
- self.btn_expand.setText("👁️")
- self.btn_expand.setAutoRaise(True)
- self.btn_expand.setStyleSheet("QToolButton { color: white; }")
- self.btn_expand.setToolTip("Görüntüle")
- self.btn_expand.clicked.connect(self._expand_list)
- toolbar_layout.addWidget(self.btn_expand)
-
- # Düzenle butonu
- self.btn_edit = QToolButton()
- try:
- self.btn_edit.setIcon(svg_icon("assets/icons/edit.svg"))
- except:
- self.btn_edit.setText("✏️")
- self.btn_edit.setAutoRaise(True)
- self.btn_edit.setStyleSheet("QToolButton { color: white; }")
- self.btn_edit.setToolTip("Düzenle")
- self.btn_edit.clicked.connect(self._edit_list)
- toolbar_layout.addWidget(self.btn_edit)
-
- # Sil butonu
- self.btn_delete = QToolButton()
- try:
- self.btn_delete.setIcon(svg_icon("assets/icons/delete.svg"))
- except:
- self.btn_delete.setText("🗑️")
- self.btn_delete.setAutoRaise(True)
- self.btn_delete.setStyleSheet("QToolButton { color: white; }")
- self.btn_delete.setToolTip("Sil")
- self.btn_delete.clicked.connect(self._delete_list)
- toolbar_layout.addWidget(self.btn_delete)
-
+ mode = getattr(getattr(self.window(), "chip_bar", None), "view_mode", "grid")
+ self.set_view_mode(mode)
+ except Exception:
+ pass
+
+ def set_view_mode(self, mode: str):
+ apply_card_view_mode(self, mode, getattr(self, "preview", None))
+ self.updateGeometry()
+
def _load_items(self):
- """Load todos (max 3 preview)"""
- # Clear existing
- while self.items_layout.count():
- item = self.items_layout.takeAt(0)
- if item.widget():
- item.widget().deleteLater()
-
- todos = self.storage.get_todos_by_list(self.list_id)
-
- # Show max 3
- for i, todo in enumerate(todos[:3]):
- checkbox = QCheckBox(self._shorten(todo.get("content", ""), 30))
- checkbox.setFont(QFont("Segoe UI", 9))
- checkbox.setChecked(bool(todo.get("completed", False)))
- todo_id = todo.get("id")
- checkbox.stateChanged.connect(lambda state, tid=todo_id: self._toggle_todo(tid, state))
-
- if todo.get("completed"):
- checkbox.setStyleSheet("""
- QCheckBox {
- color: #888;
- text-decoration: line-through;
- }
- QCheckBox::indicator {
- width: 16px;
- height: 16px;
- border: 2px solid #888;
- border-radius: 3px;
- background: #28a745;
- }
- """)
+ try:
+ items = self.storage.get_todos_by_list(self.list_id) if hasattr(self.storage, "get_todos_by_list") else []
+ if not items and hasattr(self.storage, "list_todos_by_list"):
+ items = self.storage.list_todos_by_list(self.list_id)
+ except Exception:
+ items = []
+ if not items:
+ try:
+ # Fallback: list_data içindeki todos
+ items = self.list_data.get("todos") or self.list_data.get("items") or []
+ except Exception:
+ items = []
+
+ lines = []
+ done = 0
+ for it in items[:4]:
+ try:
+ text = it.get("content") or it.get("text") or it.get("title") or str(it)
+ completed = bool(it.get("completed") or it.get("done"))
+ except Exception:
+ text, completed = str(it), False
+ if completed:
+ done += 1
+ lines.append(f"✓ {text}")
else:
- checkbox.setStyleSheet("""
- QCheckBox {
- color: white;
- }
- QCheckBox::indicator {
- width: 16px;
- height: 16px;
- border: 2px solid white;
- border-radius: 3px;
- background: transparent;
- }
- """)
-
- self.items_layout.addWidget(checkbox)
-
- # Show "..." if more items
- if len(todos) > 3:
- more_lbl = QLabel(f"... +{len(todos) - 3} daha")
- more_lbl.setFont(QFont("Segoe UI", 8))
- more_lbl.setStyleSheet("color: #aaa; font-style: italic;")
- self.items_layout.addWidget(more_lbl)
-
- def _shorten(self, text: str, max_len: int) -> str:
- """Metni kısalt"""
- if len(text) <= max_len:
- return text
- return text[:max_len] + "..."
-
- def _toggle_todo(self, todo_id: int, state: int):
- """Todo durumunu değiştir"""
+ lines.append(f"○ {text}")
+ total = len(items)
+ self.preview.setText("\n".join(lines) if lines else "Boş liste")
+ created = format_card_date(str(self.list_data.get("created_at", "") or ""))
+ self.lbl_meta.setText(f"{created} · {done}/{total}" if created else f"{done}/{total} görev")
+
+ def _expand_list(self):
try:
- completed = (state == Qt.CheckState.Checked.value)
- self.storage.toggle_todo(todo_id)
- self._load_items() # Refresh görünümü
+ from .todo_modal import TodoModal
+ dlg = TodoModal(self.list_id, self.storage, self)
+ dlg.exec()
+ self._load_items()
except Exception as e:
- print(f"Toggle todo error: {e}")
-
- def _expand_list(self):
- """Liste detayını göster (modal)"""
- from clipstack.ui.todo_modal import TodoModal
-
- dialog = TodoModal(self.list_id, self.storage, self)
- if dialog.exec():
- self._refresh_from_storage()
-
- def _edit_list(self):
- """Listeyi düzenle."""
- self._expand_list()
-
- def _refresh_from_storage(self):
- """Liste adını ve preview içeriğini veritabanından yenile."""
- list_data = self.storage.get_todo_list_by_id(self.list_id)
- if list_data:
- self.list_data = dict(list_data)
- self.title_lbl.setText(self.list_data.get("name", "Todo Listesi"))
- self._load_items()
-
- def _delete_list(self):
- """Liste sil"""
- reply = QMessageBox.question(
- self, "Liste Sil",
- f"'{self.title_lbl.text()}' listesini silmek istediğinize emin misiniz?",
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
- )
-
- if reply == QMessageBox.StandardButton.Yes:
+ QMessageBox.information(self, "Liste", f"Açılamadı: {e}")
+
+ def _more_menu(self):
+ from PySide6.QtWidgets import QMenu
+ menu = QMenu(self)
+ menu.addAction("Aç", self._expand_list)
+ menu.addAction("Yeniden adlandır", self._rename_list)
+ menu.addSeparator()
+ menu.addAction("Sil", self._delete_list)
+ menu.exec(self.btn_more.mapToGlobal(self.btn_more.rect().bottomLeft()))
+
+ def _rename_list(self):
+ name, ok = QInputDialog.getText(self, "Liste", "Liste adı:", text=self.list_data.get("name", ""))
+ if ok and name.strip():
try:
- self.storage.delete_todo_list(self.list_id)
- self.delete_requested.emit(self.list_id)
- except Exception as e:
- QMessageBox.critical(self, "Hata", f"Silme hatası: {e}")
-
- def enterEvent(self, event):
- """Hover başlangıç"""
- self.hover_overlay.setGeometry(self.rect())
- self.hover_overlay.show()
- # Üstte tam genişlik, küçük yükseklik
- self.toolbar.setGeometry(0, 0, self.width(), 32)
- self.toolbar.show()
- super().enterEvent(event)
-
- def leaveEvent(self, event):
- """Hover bitiş"""
- self.hover_overlay.hide()
- self.toolbar.hide()
- super().leaveEvent(event)
+ if hasattr(self.storage, "update_todo_list_name"):
+ self.storage.update_todo_list_name(self.list_id, name.strip())
+ self.list_data["name"] = name.strip()
+ self.title_lbl.setText(name.strip())
+ except Exception:
+ pass
+
+ def _delete_list(self):
+ mb = QMessageBox(self)
+ mb.setWindowTitle("Liste")
+ mb.setText("Bu listeyi silmek ister misiniz?")
+ mb.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
+ mb.setDefaultButton(QMessageBox.No)
+ if mb.exec() == QMessageBox.Yes:
+ self.delete_requested.emit(self.list_id)
+
+ def mousePressEvent(self, event):
+ if event.button() == Qt.LeftButton:
+ child = self.childAt(event.pos())
+ if not isinstance(child, QToolButton):
+ self._expand_list()
+ super().mousePressEvent(event)
diff --git a/clipstack/ui/todo_widget.py b/clipstack/ui/todo_widget.py
index cce12df..4159ece 100644
--- a/clipstack/ui/todo_widget.py
+++ b/clipstack/ui/todo_widget.py
@@ -223,7 +223,7 @@ def _on_todo_deleted(self, todo_id: int):
if widget:
self.todo_layout.removeWidget(widget)
self.todo_widgets.remove(widget)
- widget.setParent(None)
+ widget.hide()
widget.deleteLater()
self._update_stats()
diff --git a/clipstack/ui/widgets/__pycache__/toggle_switch.cpython-313.pyc b/clipstack/ui/widgets/__pycache__/toggle_switch.cpython-313.pyc
deleted file mode 100644
index 943b099..0000000
Binary files a/clipstack/ui/widgets/__pycache__/toggle_switch.cpython-313.pyc and /dev/null differ
diff --git a/clipstack/ui/widgets/toggle_switch.py b/clipstack/ui/widgets/toggle_switch.py
index dc88eef..cd94cac 100644
--- a/clipstack/ui/widgets/toggle_switch.py
+++ b/clipstack/ui/widgets/toggle_switch.py
@@ -1,69 +1,83 @@
-from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve, Property, QRectF
-from PySide6.QtGui import QPainter, QColor, QPen, QBrush
-from PySide6.QtWidgets import QWidget
-
-class ToggleSwitch(QWidget):
- def __init__(self, parent=None, checked=False):
- super().__init__(parent)
- self._checked = checked
- self._progress = 1.0 if checked else 0.0
- self.setFixedSize(44, 24)
- self._anim = QPropertyAnimation(self, b"progress", self)
- self._anim.setDuration(160)
- self._anim.setEasingCurve(QEasingCurve.OutCubic)
-
- def isChecked(self) -> bool:
- return self._checked
-
- def setChecked(self, v: bool):
- if self._checked == v:
- return
- self._checked = v
- self._anim.stop()
- self._anim.setStartValue(self._progress)
- self._anim.setEndValue(1.0 if v else 0.0)
- self._anim.start()
- self.update()
- self.toggled(self._checked)
-
- def mousePressEvent(self, e):
- if e.button() == Qt.LeftButton:
- self.setChecked(not self._checked)
- super().mousePressEvent(e)
-
- def paintEvent(self, e):
- p = QPainter(self)
- p.setRenderHint(QPainter.Antialiasing)
- # Track
- on_col = QColor("#60a5fa")
- off_col = QColor("#475569")
- bg = on_col if self._checked else off_col
- bg.setAlpha(200)
- p.setBrush(QBrush(bg))
- p.setPen(Qt.NoPen)
- rect = self.rect().adjusted(1, 1, -1, -1)
- p.drawRoundedRect(rect, rect.height()/2, rect.height()/2)
- # Knob
- knob_r = rect.height() - 6
- x = 3 + (rect.width() - knob_r - 6) * self._progress
- knob_rect = QRectF(x, 3, knob_r, knob_r)
- p.setBrush(QBrush(QColor("#ffffff")))
- p.setPen(QPen(QColor(0, 0, 0, 30)))
- p.drawEllipse(knob_rect)
-
- def getProgress(self):
- return self._progress
-
- def setProgress(self, v: float):
- self._progress = v
- self.update()
-
- progress = Property(float, getProgress, setProgress)
-
- # basit sinyal pattern'i (Qt Signal yerine callback)
- def onToggled(self, fn):
- self._cb = fn
-
- def toggled(self, state: bool):
- if hasattr(self, "_cb") and callable(self._cb):
- self._cb(state)
\ No newline at end of file
+from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve, Property, QRectF
+from PySide6.QtGui import QPainter, QColor, QPen, QBrush
+from PySide6.QtWidgets import QWidget
+
+
+class ToggleSwitch(QWidget):
+ def __init__(
+ self,
+ parent=None,
+ checked=False,
+ on_color="#3B82F6",
+ off_color="#334155",
+ knob_color="#FFFFFF",
+ ):
+ super().__init__(parent)
+ self._checked = checked
+ self._progress = 1.0 if checked else 0.0
+ self._on_color = QColor(on_color)
+ self._off_color = QColor(off_color)
+ self._knob_color = QColor(knob_color)
+ self.setFixedSize(36, 20)
+ self.setCursor(Qt.PointingHandCursor)
+ self._anim = QPropertyAnimation(self, b"progress", self)
+ self._anim.setDuration(160)
+ self._anim.setEasingCurve(QEasingCurve.OutCubic)
+
+ def isChecked(self) -> bool:
+ return self._checked
+
+ def setColors(self, on_color, off_color, knob_color="#FFFFFF"):
+ self._on_color = QColor(on_color)
+ self._off_color = QColor(off_color)
+ self._knob_color = QColor(knob_color)
+ self.update()
+
+ def setChecked(self, v: bool):
+ if self._checked == v:
+ return
+ self._checked = v
+ self._anim.stop()
+ self._anim.setStartValue(self._progress)
+ self._anim.setEndValue(1.0 if v else 0.0)
+ self._anim.start()
+ self.update()
+ self.toggled(self._checked)
+
+ def mousePressEvent(self, e):
+ if e.button() == Qt.LeftButton:
+ self.setChecked(not self._checked)
+ super().mousePressEvent(e)
+
+ def paintEvent(self, e):
+ p = QPainter(self)
+ p.setRenderHint(QPainter.Antialiasing)
+ on_col = QColor(self._on_color)
+ off_col = QColor(self._off_color)
+ bg = QColor(on_col if self._checked else off_col)
+ p.setBrush(QBrush(bg))
+ p.setPen(Qt.NoPen)
+ rect = self.rect().adjusted(1, 1, -1, -1)
+ p.drawRoundedRect(rect, rect.height() / 2, rect.height() / 2)
+ knob_r = rect.height() - 4
+ x = 3 + (rect.width() - knob_r - 6) * self._progress
+ knob_rect = QRectF(x, 3, knob_r, knob_r)
+ p.setBrush(QBrush(self._knob_color))
+ p.setPen(QPen(QColor(0, 0, 0, 40)))
+ p.drawEllipse(knob_rect)
+
+ def getProgress(self):
+ return self._progress
+
+ def setProgress(self, v: float):
+ self._progress = v
+ self.update()
+
+ progress = Property(float, getProgress, setProgress)
+
+ def onToggled(self, fn):
+ self._cb = fn
+
+ def toggled(self, state: bool):
+ if hasattr(self, "_cb") and callable(self._cb):
+ self._cb(state)
diff --git a/styles/theme_default.qss b/styles/theme_default.qss
index 9272b85..e83cdd2 100644
--- a/styles/theme_default.qss
+++ b/styles/theme_default.qss
@@ -1,249 +1,411 @@
-/* Default tema: koyu lacivert tonlar, modern kontroller */
-/* Diyalog/pencere kenarlığı */
-QDialog, QWidget { background: #0b1220; color: #e6edf3; }
-QGroupBox { border: none; margin-top: 8px; }
-QTabWidget::pane { border: none; }
-QScrollArea { border: none; }
-
-/* Kart widget'ları için çerçeve kaldır */
-#ItemCard {
- border: none;
-}
-
-/* Giriş ve butonlar */
-QLineEdit, QComboBox, QSpinBox {
- background: #0f172a;
- color: #e6edf3;
- border: 1px solid #1f2937;
- border-radius: 8px;
- padding: 6px 10px;
-}
-QComboBox {
- padding: 6px 30px 6px 10px;
-}
-QLineEdit:focus, QComboBox:focus, QSpinBox:focus { border: 1px solid #7aa2ff; }
-
-QComboBox::drop-down {
- width: 0;
- border: none;
-}
-QComboBox::down-arrow {
- width: 0;
- height: 0;
-}
-
-QComboBox QAbstractItemView {
- background: #0f172a;
- color: #e6edf3;
- border: 1px solid #1f2937;
- selection-background-color: #15233b;
- selection-color: #ffffff;
- outline: none;
- border-radius: 8px;
- padding: 4px;
-}
-
-QPushButton {
- background: #0f172a;
- color: #e6edf3;
- border: 1px solid #1f2937;
- border-radius: 8px;
- padding: 6px 12px;
-}
-QPushButton:hover { background: #15233b; }
-QPushButton:pressed { background: #0b1626; }
-
-QPushButton[class~="accent"] {
- background: #1d4ed8;
- border-color: #3b82f6;
- color: #f8fafc;
- font-weight: 600;
-}
-QPushButton[class~="accent"]:hover {
- background: #2563eb;
- border-color: #60a5fa;
-}
-QPushButton[class~="accent"]:pressed {
- background: #1e3a8a;
- border-color: #3b82f6;
-}
-
-/* Sekmeler */
-QTabBar::tab {
- background: transparent;
- padding: 8px 12px;
- margin-right: 2px;
- color: #cbd5e1;
-}
-QTabBar::tab:selected { color: #e6edf3; border-bottom: 2px solid #7aa2ff; }
-
-/* Menü */
-QMenu { background: #0f172a; color: #e6edf3; border: 1px solid #1f2937; border-radius: 8px; padding: 6px; }
-QMenu::item { padding: 6px 12px; border-radius: 6px; }
-QMenu::item:selected { background: #15233b; }
-
-/* Scrollbar – oval ve modern */
-QScrollBar:vertical { background: transparent; width: 12px; margin: 4px 2px 4px 0; }
-QScrollBar::handle:vertical {
- background: #35507a;
- border-radius: 6px;
- min-height: 28px;
-}
-QScrollBar::handle:vertical:hover { background: #40649b; }
-QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; border: none; background: transparent; }
-QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: transparent; }
-
-QScrollBar:horizontal { background: transparent; height: 12px; margin: 0 4px 2px 4px; }
-QScrollBar::handle:horizontal {
- background: #35507a;
- border-radius: 6px;
- min-width: 28px;
-}
-QScrollBar::handle:horizontal:hover { background: #40649b; }
-QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0; border: none; background: transparent; }
-QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { background: transparent; }
-
-
-#HoverHighlight { background: rgba(0,0,0,0.10); border-radius: 12px; }
-#HoverToolbar { background: rgba(0,0,0,0.22); border-top-left-radius: 12px; border-top-right-radius: 12px; }
-
-/* === HATIRLATMA KARTI - KOMPAKT MODERN TASARIM === */
-#ReminderCard {
- background: rgba(15, 23, 42, 0.72);
- border: 1px solid rgba(96, 165, 250, 0.28);
- border-radius: 16px;
- min-height: 72px;
-}
-
-#ReminderCard:hover {
- background: rgba(22, 34, 56, 0.82);
- border: 1px solid rgba(96, 165, 250, 0.42);
-}
-
-#ReminderTitle {
- font-size: 11pt;
- font-weight: 600;
- color: #f8fafc;
- background: transparent;
-}
-
-#ReminderDescription {
- font-size: 9pt;
- color: #cbd5e1;
- background: transparent;
-}
-
-#ReminderTime {
- font-size: 9pt;
- color: #93a8c1;
- font-weight: 500;
- background: transparent;
-}
-
-#ReminderRepeat {
- font-size: 8.5pt;
- color: #60a5fa;
- font-weight: 500;
- background: transparent;
-}
-
-#ReminderEditBtn,
-#ReminderDeleteBtn {
- min-width: 32px;
- min-height: 32px;
- padding: 0;
- border-radius: 8px;
- font-size: 11pt;
-}
-
-#ReminderEditBtn {
- background: rgba(59, 130, 246, 0.18);
- border: 1px solid rgba(59, 130, 246, 0.36);
- color: #bfdbfe;
-}
-
-#ReminderEditBtn:hover {
- background: rgba(59, 130, 246, 0.26);
- border: 1px solid rgba(59, 130, 246, 0.48);
- color: #e0f2fe;
-}
-
-#ReminderDeleteBtn {
- background: rgba(239, 68, 68, 0.18);
- border: 1px solid rgba(239, 68, 68, 0.36);
- color: #fca5a5;
-}
-
-#ReminderDeleteBtn:hover {
- background: rgba(239, 68, 68, 0.26);
- border: 1px solid rgba(239, 68, 68, 0.48);
- color: #fecaca;
-}
-
-/* === HATIRLATMA BİLDİRİM POPUP === */
-#ReminderNotificationContent {
- background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
- stop:0 #1e293b,
- stop:1 #0f172a);
- border: 2px solid rgba(96, 165, 250, 0.4);
- border-radius: 16px;
-}
-
-#ReminderNotificationTitle {
- color: #f1f5f9;
-}
-
-#ReminderNotificationDesc {
- color: #cbd5e1;
-}
-
-#ReminderNotificationSeparator {
- background: rgba(148, 163, 184, 0.25);
- border: none;
-}
-
-#ReminderSnoozeBtn {
- background: rgba(251, 191, 36, 0.15);
- color: #fbbf24;
- border: 1px solid rgba(251, 191, 36, 0.3);
- border-radius: 8px;
- padding: 8px 16px;
- font-weight: bold;
-}
-
-#ReminderSnoozeBtn:hover {
- background: rgba(251, 191, 36, 0.25);
- border: 1px solid rgba(251, 191, 36, 0.5);
-}
-
-#ReminderSnoozeBtn:pressed {
- background: rgba(251, 191, 36, 0.35);
-}
-
-#ReminderDismissBtn {
- background: rgba(239, 68, 68, 0.15);
- color: #ef4444;
- border: 1px solid rgba(239, 68, 68, 0.3);
- border-radius: 8px;
- padding: 8px 16px;
- font-weight: bold;
-}
-
-#ReminderDismissBtn:hover {
- background: rgba(239, 68, 68, 0.25);
- border: 1px solid rgba(239, 68, 68, 0.5);
-}
-
-#ReminderDismissBtn:pressed {
- background: rgba(239, 68, 68, 0.35);
-}
-
-/* Tooltip */
-QToolTip {
- background: #0f172a;
- color: #e6edf3;
- border: 1px solid #1f2937;
- border-radius: 4px;
- padding: 6px 10px;
- font-size: 12px;
-}
\ No newline at end of file
+/* Default tema: koyu lacivert tonlar, modern kontroller */
+/* Diyalog/pencere kenarlığı — çocuk widget'lara opak arka plan basma */
+QDialog {
+ background: #0b1220;
+ color: #e6edf3;
+}
+QWidget {
+ color: #e6edf3;
+ background: transparent;
+}
+#HistoryWindow {
+ background: #0b1220;
+}
+QMenu {
+ background: #0f172a;
+ color: #e6edf3;
+}
+QGroupBox { border: none; margin-top: 8px; }
+QTabWidget::pane { border: none; background: transparent; }
+QScrollArea { border: none; background: transparent; }
+QScrollArea > QWidget > QWidget { background: transparent; }
+
+/* Kart widget'ları için çerçeve kaldır */
+#ItemCard {
+ border: none;
+}
+
+QScrollArea {
+ border: none;
+ background: transparent;
+}
+QScrollArea > QWidget > QWidget {
+ background: transparent;
+}
+
+/* Giriş ve butonlar */
+QLineEdit, QComboBox, QSpinBox {
+ background: #0f172a;
+ color: #e6edf3;
+ border: 1px solid rgba(31, 41, 55, 0.7);
+ border-radius: 8px;
+ padding: 6px 10px;
+}
+QComboBox {
+ padding: 6px 30px 6px 10px;
+}
+QLineEdit:focus, QComboBox:focus, QSpinBox:focus { border: 1px solid #7aa2ff; }
+
+QComboBox::drop-down {
+ width: 0;
+ border: none;
+}
+QComboBox::down-arrow {
+ width: 0;
+ height: 0;
+}
+
+QComboBox QAbstractItemView {
+ background: #0f172a;
+ color: #e6edf3;
+ border: 1px solid #1f2937;
+ selection-background-color: #15233b;
+ selection-color: #ffffff;
+ outline: none;
+ border-radius: 8px;
+ padding: 4px;
+}
+
+QPushButton {
+ background: #0f172a;
+ color: #e6edf3;
+ border: none;
+ border-radius: 8px;
+ padding: 6px 12px;
+}
+QPushButton:hover { background: #15233b; }
+QPushButton:pressed { background: #0b1626; }
+
+QPushButton[class~="accent"] {
+ background: #1d4ed8;
+ border: none;
+ color: #f8fafc;
+ font-weight: 600;
+}
+QPushButton[class~="accent"]:hover {
+ background: #2563eb;
+}
+QPushButton[class~="accent"]:pressed {
+ background: #1e3a8a;
+}
+
+/* Sekmeler */
+QTabBar::tab {
+ background: transparent;
+ padding: 8px 12px;
+ margin-right: 2px;
+ color: #cbd5e1;
+}
+QTabBar::tab:selected { color: #e6edf3; border-bottom: 2px solid #7aa2ff; }
+
+/* Menü */
+QMenu { background: #0f172a; color: #e6edf3; border: 1px solid #1f2937; border-radius: 8px; padding: 6px; }
+QMenu::item { padding: 6px 12px; border-radius: 6px; }
+QMenu::item:selected { background: #15233b; }
+
+/* Scrollbar – oval ve modern */
+QScrollBar:vertical { background: transparent; width: 12px; margin: 4px 2px 4px 0; }
+QScrollBar::handle:vertical {
+ background: #35507a;
+ border-radius: 6px;
+ min-height: 28px;
+}
+QScrollBar::handle:vertical:hover { background: #40649b; }
+QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; border: none; background: transparent; }
+QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: transparent; }
+
+QScrollBar:horizontal { background: transparent; height: 12px; margin: 0 4px 2px 4px; }
+QScrollBar::handle:horizontal {
+ background: #35507a;
+ border-radius: 6px;
+ min-width: 28px;
+}
+QScrollBar::handle:horizontal:hover { background: #40649b; }
+QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0; border: none; background: transparent; }
+QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { background: transparent; }
+
+
+#HoverHighlight { background: rgba(0,0,0,0.10); border-radius: 12px; }
+#HoverToolbar { background: rgba(0,0,0,0.22); border-top-left-radius: 12px; border-top-right-radius: 12px; }
+
+/* === HATIRLATMA KARTI - KOMPAKT MODERN TASARIM === */
+#ReminderCard {
+ background: rgba(15, 23, 42, 0.72);
+ border: 1px solid rgba(96, 165, 250, 0.28);
+ border-radius: 16px;
+ min-height: 72px;
+}
+
+#ReminderCard:hover {
+ background: rgba(22, 34, 56, 0.82);
+ border: 1px solid rgba(96, 165, 250, 0.42);
+}
+
+#ReminderTitle {
+ font-size: 11pt;
+ font-weight: 600;
+ color: #f8fafc;
+ background: transparent;
+}
+
+#ReminderDescription {
+ font-size: 9pt;
+ color: #cbd5e1;
+ background: transparent;
+}
+
+#ReminderTime {
+ font-size: 9pt;
+ color: #93a8c1;
+ font-weight: 500;
+ background: transparent;
+}
+
+#ReminderRepeat {
+ font-size: 8.5pt;
+ color: #60a5fa;
+ font-weight: 500;
+ background: transparent;
+}
+
+#ReminderEditBtn,
+#ReminderDeleteBtn {
+ min-width: 32px;
+ min-height: 32px;
+ padding: 0;
+ border-radius: 8px;
+ font-size: 11pt;
+}
+
+#ReminderEditBtn {
+ background: rgba(59, 130, 246, 0.18);
+ border: 1px solid rgba(59, 130, 246, 0.36);
+ color: #bfdbfe;
+}
+
+#ReminderEditBtn:hover {
+ background: rgba(59, 130, 246, 0.26);
+ border: 1px solid rgba(59, 130, 246, 0.48);
+ color: #e0f2fe;
+}
+
+#ReminderDeleteBtn {
+ background: rgba(239, 68, 68, 0.18);
+ border: 1px solid rgba(239, 68, 68, 0.36);
+ color: #fca5a5;
+}
+
+#ReminderDeleteBtn:hover {
+ background: rgba(239, 68, 68, 0.26);
+ border: 1px solid rgba(239, 68, 68, 0.48);
+ color: #fecaca;
+}
+
+/* === HATIRLATMA BİLDİRİM POPUP === */
+#ReminderNotificationContent {
+ background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
+ stop:0 #1e293b,
+ stop:1 #0f172a);
+ border: 2px solid rgba(96, 165, 250, 0.4);
+ border-radius: 16px;
+}
+
+#ReminderNotificationTitle {
+ color: #f1f5f9;
+}
+
+#ReminderNotificationDesc {
+ color: #cbd5e1;
+}
+
+#ReminderNotificationSeparator {
+ background: rgba(148, 163, 184, 0.25);
+ border: none;
+}
+
+#ReminderSnoozeBtn {
+ background: rgba(251, 191, 36, 0.15);
+ color: #fbbf24;
+ border: 1px solid rgba(251, 191, 36, 0.3);
+ border-radius: 8px;
+ padding: 8px 16px;
+ font-weight: bold;
+}
+
+#ReminderSnoozeBtn:hover {
+ background: rgba(251, 191, 36, 0.25);
+ border: 1px solid rgba(251, 191, 36, 0.5);
+}
+
+#ReminderSnoozeBtn:pressed {
+ background: rgba(251, 191, 36, 0.35);
+}
+
+#ReminderDismissBtn {
+ background: rgba(239, 68, 68, 0.15);
+ color: #ef4444;
+ border: 1px solid rgba(239, 68, 68, 0.3);
+ border-radius: 8px;
+ padding: 8px 16px;
+ font-weight: bold;
+}
+
+#ReminderDismissBtn:hover {
+ background: rgba(239, 68, 68, 0.25);
+ border: 1px solid rgba(239, 68, 68, 0.5);
+}
+
+#ReminderDismissBtn:pressed {
+ background: rgba(239, 68, 68, 0.35);
+}
+
+/* Tooltip */
+QToolTip {
+ background: #0f172a;
+ color: #e6edf3;
+ border: 1px solid #1f2937;
+ border-radius: 4px;
+ padding: 6px 10px;
+ font-size: 12px;
+}
+
+/* === SIDEBAR (default tema override) === */
+#AppSidebar {
+ background: #0a1220;
+ border: none;
+ border-right: 1px solid rgba(31, 41, 55, 0.55);
+}
+#SidebarAppTitle { color: #e6edf3; background: transparent; border: none; }
+#SidebarNavButton, #SidebarSettingsBtn, #SidebarQuickBtn {
+ background: transparent;
+ border: none;
+ border-radius: 10px;
+}
+#SidebarNavButton:hover, #SidebarSettingsBtn:hover, #SidebarQuickBtn:hover {
+ background: #15233b;
+}
+#SidebarNavButton:checked {
+ background: #1d4ed8;
+}
+#SidebarNavText { color: #e6edf3; background: transparent; border: none; }
+#SidebarCountBadge {
+ background: rgba(21, 35, 59, 0.9);
+ color: #93c5fd;
+ border: none;
+ border-radius: 8px;
+ padding: 2px 7px;
+ font-size: 10px;
+}
+#SidebarNavButton:checked #SidebarCountBadge {
+ background: rgba(255,255,255,0.16);
+ color: #fff;
+}
+#SidebarSectionHeader { color: #64748b; background: transparent; border: none; }
+#SidebarShortcut {
+ color: #64748b;
+ background: rgba(15, 23, 42, 0.8);
+ border: none;
+ border-radius: 4px;
+ padding: 1px 6px;
+}
+#SidebarVersion { color: #64748b; background: transparent; border: none; }
+
+/* === ITEM CARD — çerçevesiz soft yüzey === */
+#ItemCard {
+ background: #111827;
+ border: none;
+ border-radius: 14px;
+}
+#ItemCard:hover {
+ background: #15233b;
+ border: none;
+}
+#ItemCard QLabel,
+#ItemCard QToolButton,
+#ItemCard QFrame {
+ background: transparent;
+ border: none;
+}
+#ItemPreview, #ItemTitle, #MetaLabel {
+ background: transparent;
+ border: none;
+}
+#MetaLabel { color: #94a3b8; }
+#ItemTypeBadge {
+ background: rgba(30, 41, 59, 0.95);
+ border: none;
+ border-radius: 7px;
+}
+#SkeletonCard {
+ background: #111827;
+ border: none;
+ border-radius: 14px;
+}
+#SearchSidePanel {
+ background: #0a1220;
+ border: none;
+ border-left: 1px solid rgba(31, 41, 55, 0.55);
+}
+#PaginationBar {
+ background: transparent;
+ border: none;
+}
+#PaginationBar QPushButton {
+ background: transparent;
+ border: none;
+ border-radius: 8px;
+}
+#PaginationBar QPushButton:hover {
+ background: #15233b;
+}
+#FilterToggleBtn {
+ background: #111827;
+ border: none;
+ border-radius: 8px;
+}
+#FilterToggleBtn:hover {
+ background: #15233b;
+}
+#SearchBox {
+ background: #0f172a;
+ border: 1px solid rgba(51, 65, 85, 0.7);
+ border-radius: 10px;
+}
+#SearchShortcutBadge {
+ background: rgba(30, 41, 59, 0.95);
+ color: #94a3b8;
+ border: none;
+ border-radius: 6px;
+ padding: 2px 8px;
+ font-size: 10px;
+ font-weight: 600;
+}
+#FilterChipBar { background: transparent; border: none; }
+#FilterChip {
+ background: #111827;
+ color: #e2e8f0;
+ border: none;
+ border-radius: 8px;
+ padding: 6px 12px;
+}
+#FilterChip:hover { background: #15233b; }
+#FilterChip:checked { background: #1d4ed8; color: #fff; }
+#ItemCard[viewMode="list"] {
+ /* liste satırı */
+}
+#ViewModeBtn {
+ background: #111827;
+ border: none;
+ border-radius: 8px;
+ padding: 6px 10px;
+ color: #cbd5e1;
+}
+#ViewModeBtn:checked { background: #1d4ed8; color: #fff; }
+#FilterChip {
+ padding-left: 10px;
+ padding-right: 12px;
+}
+#PaginationBar QPushButton {
+ padding-left: 10px;
+ padding-right: 10px;
+}
diff --git a/styles/widgets/sidebar.qss b/styles/widgets/sidebar.qss
new file mode 100644
index 0000000..b5676a7
--- /dev/null
+++ b/styles/widgets/sidebar.qss
@@ -0,0 +1,194 @@
+/* Ortak UI — sidebar, kart, pagination (tüm temalara eklenir) */
+
+#AppSidebar {
+ background: rgba(0, 0, 0, 0.12);
+ border: none;
+ border-right: 1px solid rgba(128, 128, 128, 0.12);
+}
+#AppSidebar QLabel,
+#AppSidebar QPushButton,
+#AppSidebar QToolButton,
+#AppSidebar QScrollArea,
+#AppSidebar QWidget {
+ background: transparent;
+ border: none;
+}
+#SidebarAppTitle { background: transparent; }
+#SidebarNavButton, #SidebarSettingsBtn, #SidebarQuickBtn {
+ background: transparent;
+ border: none;
+ border-radius: 10px;
+ text-align: left;
+ padding: 0;
+}
+#SidebarNavButton:hover, #SidebarSettingsBtn:hover, #SidebarQuickBtn:hover {
+ background: rgba(127, 127, 127, 0.14);
+}
+#SidebarNavButton:checked {
+ background: rgba(59, 130, 246, 0.42);
+}
+#SidebarNavText { background: transparent; border: none; }
+#SidebarCountBadge {
+ background: rgba(127, 127, 127, 0.16);
+ border: none;
+ border-radius: 8px;
+ padding: 2px 7px;
+ font-size: 10px;
+ min-width: 22px;
+}
+#SidebarSectionHeader {
+ font-size: 10px;
+ font-weight: 600;
+ padding: 8px 10px 4px 10px;
+ background: transparent;
+ border: none;
+ opacity: 0.65;
+}
+#SidebarShortcut {
+ font-size: 10px;
+ border: none;
+ background: rgba(127, 127, 127, 0.14);
+ border-radius: 4px;
+ padding: 1px 6px;
+ opacity: 0.75;
+}
+#SidebarVersion {
+ font-size: 10px;
+ padding-top: 4px;
+ background: transparent;
+ border: none;
+ opacity: 0.55;
+}
+#SidebarCollapseBtn { border: none; background: transparent; }
+
+/* Kartlar — çerçevesiz, sadece soft yüzey */
+#ItemCard {
+ background: rgba(148, 163, 184, 0.07);
+ border: none;
+ border-radius: 14px;
+}
+#ItemCard:hover {
+ background: rgba(148, 163, 184, 0.13);
+ border: none;
+}
+#ItemCard QLabel,
+#ItemCard QToolButton,
+#ItemCard QFrame {
+ background: transparent;
+ border: none;
+}
+#ItemTitle {
+ background: transparent;
+ border: none;
+}
+#ItemPreview {
+ background: transparent;
+ border: none;
+}
+#MetaLabel {
+ background: transparent;
+ border: none;
+ color: #94a3b8;
+}
+#ItemTypeBadge {
+ background: rgba(148, 163, 184, 0.14);
+ border: none;
+ border-radius: 7px;
+}
+#FavButton, #MoreButton, #CopyButton {
+ background: transparent;
+ border: none;
+ border-radius: 6px;
+ padding: 2px;
+}
+#FavButton:hover, #MoreButton:hover, #CopyButton:hover {
+ background: rgba(148, 163, 184, 0.16);
+}
+
+#SkeletonCard {
+ background: rgba(148, 163, 184, 0.07);
+ border: none;
+ border-radius: 14px;
+}
+#SkeletonCard QFrame {
+ border: none;
+}
+
+#SearchSidePanel {
+ border: none;
+ border-left: 1px solid rgba(128, 128, 128, 0.12);
+}
+#PaginationBar {
+ background: transparent;
+ border: none;
+}
+#PaginationBar QPushButton {
+ background: transparent;
+ border: none;
+ border-radius: 8px;
+ padding: 6px 14px;
+}
+#PaginationBar QPushButton:hover {
+ background: rgba(148, 163, 184, 0.12);
+}
+#PaginationBar QPushButton:disabled {
+ color: rgba(148, 163, 184, 0.35);
+}
+#PaginationBar QLabel {
+ background: transparent;
+ border: none;
+ color: #94a3b8;
+}
+
+/* In-app toast — alt orta */
+#Toast {
+ background: rgba(15, 23, 42, 0.94);
+ border: none;
+ border-radius: 10px;
+}
+#ToastLabel {
+ color: #e6edf3;
+ font-size: 12px;
+ background: transparent;
+ border: none;
+}
+
+#SearchBox {
+ background: rgba(148, 163, 184, 0.08);
+ border: 1px solid rgba(128, 128, 128, 0.22);
+ border-radius: 10px;
+}
+#SearchShortcutBadge {
+ background: rgba(127, 127, 127, 0.18);
+ border: none;
+ border-radius: 6px;
+ padding: 2px 8px;
+ font-size: 10px;
+ font-weight: 600;
+}
+#FilterChip {
+ background: rgba(148, 163, 184, 0.08);
+ border: none;
+ border-radius: 8px;
+ padding: 6px 12px;
+}
+#FilterChip:checked {
+ background: rgba(59, 130, 246, 0.45);
+}
+#ViewModeBtn {
+ background: rgba(148, 163, 184, 0.08);
+ border: none;
+ border-radius: 8px;
+ padding: 6px 10px;
+}
+#ViewModeBtn:checked {
+ background: rgba(59, 130, 246, 0.45);
+}
+
+/* Üst araç çubuğu butonları daha yumuşak */
+QPushButton#FilterToggleBtn,
+#FilterToggleBtn {
+ background: rgba(148, 163, 184, 0.08);
+ border: none;
+ border-radius: 8px;
+}
diff --git a/tests/test_clipboard_watcher.py b/tests/test_clipboard_watcher.py
index df91367..085fff7 100644
--- a/tests/test_clipboard_watcher.py
+++ b/tests/test_clipboard_watcher.py
@@ -35,7 +35,7 @@ class _FakeStorage:
def __init__(self):
self.added = []
- def add_item(self, item_type, text, image_bytes, html, created_at):
+ def add_item(self, item_type, text, image_bytes, html, created_at, source_app=None):
self.added.append((item_type, text, image_bytes, html, created_at))
return {"id": len(self.added)}
diff --git a/version.txt b/version.txt
index 23aa839..0495c4a 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-1.2.2
+1.2.3
diff --git a/version_info.txt b/version_info.txt
index 36c68dd..727c5cc 100644
--- a/version_info.txt
+++ b/version_info.txt
@@ -1,7 +1,7 @@
VSVersionInfo(
ffi=FixedFileInfo(
- filevers=(1, 2, 2, 0),
- prodvers=(1, 2, 2, 0),
+ filevers=(1, 2, 3, 0),
+ prodvers=(1, 2, 3, 0),
mask=0x3F,
flags=0x0,
OS=0x40004,
@@ -17,11 +17,11 @@ VSVersionInfo(
[
StringStruct('CompanyName', 'Miyotu'),
StringStruct('FileDescription', 'TaxClip — Modern Windows Clipboard Manager'),
- StringStruct('FileVersion', '1.2.2'),
+ StringStruct('FileVersion', '1.2.3'),
StringStruct('InternalName', 'TaxClip'),
StringStruct('OriginalFilename', 'TaxClip.exe'),
StringStruct('ProductName', 'TaxClip'),
- StringStruct('ProductVersion', '1.2.2'),
+ StringStruct('ProductVersion', '1.2.3'),
]
)
]