clutterxp 1.1.1__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clutterxp-1.1.1 → clutterxp-1.2.0}/PKG-INFO +1 -1
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/__init__.py +1 -1
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/app.py +9 -2
- clutterxp-1.2.0/src/clutterxp/gui/theme.py +378 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/views/dashboard.py +45 -23
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/views/duplicates.py +23 -5
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/views/folders.py +18 -3
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/views/history.py +29 -6
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/views/preview.py +35 -9
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/views/rules.py +74 -12
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/views/settings.py +12 -2
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/workers.py +11 -3
- {clutterxp-1.1.1 → clutterxp-1.2.0}/.github/workflows/ci.yml +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/.gitignore +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/CHANGELOG.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/README.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/config.example.yaml +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/00_PROJECT_PLAN.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/01_ARCHITECTURE.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/02_RULE_ENGINE.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/03_CONFIG.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/04_SAFE_MOVER.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/05_HISTORY_UNDO.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/06_WATCHER.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/07_DUPLICATES.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/08_CLI.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/09_SAFETY.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/10_TESTING.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/11_PACKAGING.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/12_GUI.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/13_ROADMAP.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/14_DECISIONS.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/15_ACCEPTANCE.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/16_RISKS.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/reviews/.gitkeep +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/reviews/acceptance_matrix.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/reviews/perf_report.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/reviews/security_review.md +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/packaging/clutterxp.service +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/pyproject.toml +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/scripts/.gitkeep +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/scripts/check_contracts.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/scripts/check_ownership.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/scripts/contracts.lock.json +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/scripts/e2e.sh +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/scripts/smoke_readme.sh +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/app.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_config.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_doctor.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_duplicates.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_gui.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_history.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_init.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_organize.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_rules.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_scan.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_service.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_undo.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_watch.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/errors.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/render.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/config/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/config/jsonschema.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/config/loader.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/config/placeholders.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/config/runtime.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/config/schema.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/config/validate.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/config/writer.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/duplicates/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/duplicates/detector.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/duplicates/hashcache.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/duplicates/namenorm.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/duplicates/quarantine.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/duplicates/report.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/api.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/models_qt.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/views/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/history/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/history/db.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/history/pathcodec.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/history/recovery.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/history/schema.sql +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/history/service.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/history/undo.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/interfaces.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/locking.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/logging_setup.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/models.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/mover/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/mover/collision.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/mover/fsops.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/mover/safe_move.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/organizer.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/paths.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/planner.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/py.typed +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/rules/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/rules/engine.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/rules/matcher.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/rules/template.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/rules/tokenizer.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/safety.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/scanner.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/service_install.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/units.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/watch/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/watch/debounce.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/watch/events.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/watch/ignore.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/watch/service.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/watch/settle.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/.gitkeep +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/conftest.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/schemas/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/schemas/doctor.json +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/schemas/duplicates.json +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/schemas/history.json +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/schemas/organize.json +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/schemas/rules_list.json +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/schemas/rules_test.json +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/schemas/scan.json +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_config.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_doctor.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_duplicates.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_general.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_gui.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_history.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_init.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_organize.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_rules.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_scan.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_service.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_undo.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_watch.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/conftest.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/test_jsonschema_and_logging.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/test_loader.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/test_paths.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/test_placeholders.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/test_runtime.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/test_validate.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/test_writer.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/conftest.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/duplicates/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/duplicates/test_detector.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/duplicates/test_hashcache.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/duplicates/test_namenorm.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/duplicates/test_quarantine.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/conftest.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A81_gui_launches_and_cli_never_imports_qt.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A82_dashboard_shows_watcher_and_recent_operations.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A83_folders_view_edits_watch_folders_and_settings.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A84_rules_editor_includes_live_trace.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A85_preview_data_equals_cli_json_plan.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A86_history_view_undo_shows_exact_refusal_reasons.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A87_duplicates_view_progress_and_cancel.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A88_duplicates_quarantine_gui.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A89_ui_thread_never_blocks_worker_cancellation.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A90_gui_extra_documented.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_api_extra.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_models_qt.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/helpers.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/history/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/history/test_db.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/history/test_pathcodec.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/history/test_recovery.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/history/test_service.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/history/test_undo.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/integration/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/integration/test_acceptance_sweep.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/integration/test_fault_injection.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/integration/test_properties.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/integration/test_scenarios.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/mover/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/mover/conftest.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/mover/test_collision.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/mover/test_fsops.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/mover/test_locking.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/mover/test_safe_move.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/organizer/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/organizer/conftest.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/organizer/test_organizer.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/organizer/test_planner.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/organizer/test_scanner.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/perf/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/perf/test_perf_budgets.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/rules/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/rules/conftest.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/rules/test_engine.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/rules/test_matcher.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/rules/test_template.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/rules/test_tokenizer.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/rules/test_vectors.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/safety/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/safety/test_invariants_full.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/safety/test_safety_module.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/test_harness.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/test_service_install.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/__init__.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/conftest.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/test_acceptance.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/test_debounce.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/test_events.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/test_ignore.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/test_invariants.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/test_service.py +0 -0
- {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/test_settle.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: clutterxp
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Configurable, safe, lightweight file organizer: rules, preview, undo, watch, duplicates.
|
|
5
5
|
Keywords: automation,cli,downloads,file-organizer,linux
|
|
6
6
|
Classifier: Environment :: Console
|
|
@@ -28,6 +28,7 @@ from PySide6.QtWidgets import (
|
|
|
28
28
|
|
|
29
29
|
from clutterxp import paths
|
|
30
30
|
from clutterxp.gui.api import GuiApi
|
|
31
|
+
from clutterxp.gui.theme import apply_theme
|
|
31
32
|
from clutterxp.gui.views import (
|
|
32
33
|
DashboardView,
|
|
33
34
|
DuplicatesView,
|
|
@@ -45,8 +46,13 @@ class ClutterXPMainWindow(QMainWindow):
|
|
|
45
46
|
def __init__(self, api: GuiApi | None = None, parent: QWidget | None = None) -> None:
|
|
46
47
|
super().__init__(parent)
|
|
47
48
|
self.api = api if api is not None else GuiApi()
|
|
48
|
-
self.setWindowTitle("ClutterXP - Intelligent File Organizer")
|
|
49
|
-
self.resize(
|
|
49
|
+
self.setWindowTitle("ClutterXP - Safe, Intelligent File Organizer")
|
|
50
|
+
self.resize(1080, 720)
|
|
51
|
+
self.setMinimumSize(880, 580)
|
|
52
|
+
|
|
53
|
+
app_instance = QApplication.instance()
|
|
54
|
+
if app_instance is not None:
|
|
55
|
+
apply_theme(app_instance)
|
|
50
56
|
|
|
51
57
|
self._init_ui()
|
|
52
58
|
|
|
@@ -181,6 +187,7 @@ def main() -> None:
|
|
|
181
187
|
if app is None:
|
|
182
188
|
app = QApplication(sys.argv)
|
|
183
189
|
app.setApplicationName("ClutterXP")
|
|
190
|
+
apply_theme(app)
|
|
184
191
|
|
|
185
192
|
# Hook up SIGINT to cleanly exit
|
|
186
193
|
signal.signal(signal.SIGINT, lambda *_: app.quit())
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""Modern, minimalist, elegant dark theme for ClutterXP GUI.
|
|
2
|
+
|
|
3
|
+
Provides a unified design system with clean typography, balanced spacing,
|
|
4
|
+
sleek card surfaces, modern button hierarchy, and custom scrollbars.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from PySide6.QtGui import QColor, QPalette
|
|
10
|
+
from PySide6.QtWidgets import QApplication
|
|
11
|
+
|
|
12
|
+
MODERN_STYLESHEET = """
|
|
13
|
+
/* ====================================================================
|
|
14
|
+
ClutterXP Modern Minimalist Theme (Obsidian & Indigo)
|
|
15
|
+
==================================================================== */
|
|
16
|
+
|
|
17
|
+
* {
|
|
18
|
+
outline: none;
|
|
19
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
QMainWindow, QDialog, QWidget {
|
|
23
|
+
background-color: #0f1117;
|
|
24
|
+
color: #f1f5f9;
|
|
25
|
+
font-size: 13px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* ==========================================
|
|
29
|
+
Tab Widget & Segmented Tab Bar
|
|
30
|
+
========================================== */
|
|
31
|
+
QTabWidget::pane {
|
|
32
|
+
border: 1px solid #1e2433;
|
|
33
|
+
border-radius: 8px;
|
|
34
|
+
background-color: #121620;
|
|
35
|
+
padding: 6px;
|
|
36
|
+
margin-top: -1px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
QTabBar {
|
|
40
|
+
background-color: transparent;
|
|
41
|
+
qproperty-drawBase: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
QTabBar::tab {
|
|
45
|
+
background-color: #171b26;
|
|
46
|
+
color: #94a3b8;
|
|
47
|
+
border: 1px solid #262d3d;
|
|
48
|
+
border-radius: 6px;
|
|
49
|
+
padding: 7px 18px;
|
|
50
|
+
margin: 3px 5px 3px 0px;
|
|
51
|
+
font-size: 13px;
|
|
52
|
+
font-weight: 500;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
QTabBar::tab:hover {
|
|
56
|
+
background-color: #1f2637;
|
|
57
|
+
color: #f8fafc;
|
|
58
|
+
border-color: #3b455c;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
QTabBar::tab:selected {
|
|
62
|
+
background-color: #6366f1;
|
|
63
|
+
color: #ffffff;
|
|
64
|
+
border: 1px solid #818cf8;
|
|
65
|
+
font-weight: 600;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ==========================================
|
|
69
|
+
Cards & Group Boxes
|
|
70
|
+
========================================== */
|
|
71
|
+
QGroupBox {
|
|
72
|
+
background-color: #161a25;
|
|
73
|
+
border: 1px solid #232a3b;
|
|
74
|
+
border-radius: 8px;
|
|
75
|
+
margin-top: 20px;
|
|
76
|
+
padding: 16px 14px 14px 14px;
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
font-size: 13px;
|
|
79
|
+
color: #f8fafc;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
QGroupBox::title {
|
|
83
|
+
subcontrol-origin: margin;
|
|
84
|
+
subcontrol-position: top left;
|
|
85
|
+
left: 14px;
|
|
86
|
+
top: 2px;
|
|
87
|
+
padding: 0 6px;
|
|
88
|
+
background-color: #161a25;
|
|
89
|
+
color: #a5b4fc;
|
|
90
|
+
font-size: 12px;
|
|
91
|
+
font-weight: 600;
|
|
92
|
+
text-transform: uppercase;
|
|
93
|
+
letter-spacing: 0.5px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ==========================================
|
|
97
|
+
Buttons
|
|
98
|
+
========================================== */
|
|
99
|
+
QPushButton {
|
|
100
|
+
background-color: #1e2433;
|
|
101
|
+
color: #f1f5f9;
|
|
102
|
+
border: 1px solid #2d374d;
|
|
103
|
+
border-radius: 6px;
|
|
104
|
+
padding: 6px 14px;
|
|
105
|
+
font-weight: 500;
|
|
106
|
+
font-size: 13px;
|
|
107
|
+
min-height: 22px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
QPushButton:hover {
|
|
111
|
+
background-color: #273044;
|
|
112
|
+
border-color: #404f6d;
|
|
113
|
+
color: #ffffff;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
QPushButton:pressed {
|
|
117
|
+
background-color: #181d29;
|
|
118
|
+
border-color: #6366f1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
QPushButton:disabled {
|
|
122
|
+
background-color: #141720;
|
|
123
|
+
color: #475569;
|
|
124
|
+
border-color: #1d222e;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Specialized Button Variants via Property */
|
|
128
|
+
QPushButton[primary="true"] {
|
|
129
|
+
background-color: #6366f1;
|
|
130
|
+
color: #ffffff;
|
|
131
|
+
border: 1px solid #818cf8;
|
|
132
|
+
font-weight: 600;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
QPushButton[primary="true"]:hover {
|
|
136
|
+
background-color: #4f46e5;
|
|
137
|
+
border-color: #a5b4fc;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
QPushButton[primary="true"]:pressed {
|
|
141
|
+
background-color: #4338ca;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
QPushButton[success="true"] {
|
|
145
|
+
background-color: #10b981;
|
|
146
|
+
color: #ffffff;
|
|
147
|
+
border: 1px solid #34d399;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
QPushButton[success="true"]:hover {
|
|
152
|
+
background-color: #059669;
|
|
153
|
+
border-color: #6ee7b7;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
QPushButton[danger="true"] {
|
|
157
|
+
background-color: #e11d48;
|
|
158
|
+
color: #ffffff;
|
|
159
|
+
border: 1px solid #fb7185;
|
|
160
|
+
font-weight: 600;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
QPushButton[danger="true"]:hover {
|
|
164
|
+
background-color: #be123c;
|
|
165
|
+
border-color: #fda4af;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* ==========================================
|
|
169
|
+
Input Controls
|
|
170
|
+
========================================== */
|
|
171
|
+
QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox {
|
|
172
|
+
background-color: #0c0f16;
|
|
173
|
+
border: 1px solid #232a3b;
|
|
174
|
+
border-radius: 6px;
|
|
175
|
+
padding: 6px 10px;
|
|
176
|
+
color: #f8fafc;
|
|
177
|
+
font-size: 13px;
|
|
178
|
+
selection-background-color: #6366f1;
|
|
179
|
+
selection-color: #ffffff;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QComboBox:focus {
|
|
183
|
+
border: 1px solid #6366f1;
|
|
184
|
+
background-color: #0f131d;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
QComboBox::drop-down {
|
|
188
|
+
border: none;
|
|
189
|
+
width: 24px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
QComboBox QAbstractItemView {
|
|
193
|
+
background-color: #161a25;
|
|
194
|
+
border: 1px solid #232a3b;
|
|
195
|
+
border-radius: 6px;
|
|
196
|
+
selection-background-color: #6366f1;
|
|
197
|
+
selection-color: #ffffff;
|
|
198
|
+
color: #f1f5f9;
|
|
199
|
+
padding: 4px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* ==========================================
|
|
203
|
+
Tables & Lists
|
|
204
|
+
========================================== */
|
|
205
|
+
QTableView, QListWidget, QTextEdit {
|
|
206
|
+
background-color: #0c0f16;
|
|
207
|
+
alternate-background-color: #11151f;
|
|
208
|
+
border: 1px solid #232a3b;
|
|
209
|
+
border-radius: 6px;
|
|
210
|
+
gridline-color: #1a202d;
|
|
211
|
+
color: #f1f5f9;
|
|
212
|
+
selection-background-color: #2e384e;
|
|
213
|
+
selection-color: #ffffff;
|
|
214
|
+
font-size: 13px;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
QHeaderView::section {
|
|
218
|
+
background-color: #161a25;
|
|
219
|
+
color: #94a3b8;
|
|
220
|
+
font-weight: 600;
|
|
221
|
+
font-size: 12px;
|
|
222
|
+
padding: 7px 8px;
|
|
223
|
+
border: none;
|
|
224
|
+
border-bottom: 1px solid #232a3b;
|
|
225
|
+
border-right: 1px solid #1a202d;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
QHeaderView::section:last {
|
|
229
|
+
border-right: none;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* ==========================================
|
|
233
|
+
Progress Bar
|
|
234
|
+
========================================== */
|
|
235
|
+
QProgressBar {
|
|
236
|
+
border: 1px solid #232a3b;
|
|
237
|
+
border-radius: 6px;
|
|
238
|
+
text-align: center;
|
|
239
|
+
background-color: #0c0f16;
|
|
240
|
+
color: #f8fafc;
|
|
241
|
+
font-weight: 600;
|
|
242
|
+
font-size: 11px;
|
|
243
|
+
height: 16px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
QProgressBar::chunk {
|
|
247
|
+
background-color: #6366f1;
|
|
248
|
+
border-radius: 5px;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* ==========================================
|
|
252
|
+
Scroll Bars
|
|
253
|
+
========================================== */
|
|
254
|
+
QScrollBar:vertical {
|
|
255
|
+
background: #0c0f16;
|
|
256
|
+
width: 8px;
|
|
257
|
+
margin: 0;
|
|
258
|
+
border-radius: 4px;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
QScrollBar::handle:vertical {
|
|
262
|
+
background: #2b3345;
|
|
263
|
+
min-height: 24px;
|
|
264
|
+
border-radius: 4px;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
QScrollBar::handle:vertical:hover {
|
|
268
|
+
background: #475569;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
|
|
272
|
+
height: 0px;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
QScrollBar:horizontal {
|
|
276
|
+
background: #0c0f16;
|
|
277
|
+
height: 8px;
|
|
278
|
+
margin: 0;
|
|
279
|
+
border-radius: 4px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
QScrollBar::handle:horizontal {
|
|
283
|
+
background: #2b3345;
|
|
284
|
+
min-width: 24px;
|
|
285
|
+
border-radius: 4px;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
QScrollBar::handle:horizontal:hover {
|
|
289
|
+
background: #475569;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
|
|
293
|
+
width: 0px;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* ==========================================
|
|
297
|
+
Status Bar, Menu Bar & Tooltips
|
|
298
|
+
========================================== */
|
|
299
|
+
QStatusBar {
|
|
300
|
+
background-color: #0c0f16;
|
|
301
|
+
border-top: 1px solid #1e2433;
|
|
302
|
+
color: #94a3b8;
|
|
303
|
+
font-size: 12px;
|
|
304
|
+
padding: 4px 8px;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
QMenuBar {
|
|
308
|
+
background-color: #0f1117;
|
|
309
|
+
border-bottom: 1px solid #1e2433;
|
|
310
|
+
color: #cbd5e1;
|
|
311
|
+
padding: 2px 4px;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
QMenuBar::item {
|
|
315
|
+
background: transparent;
|
|
316
|
+
padding: 4px 10px;
|
|
317
|
+
border-radius: 4px;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
QMenuBar::item:selected {
|
|
321
|
+
background-color: #1e2433;
|
|
322
|
+
color: #f8fafc;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
QMenu {
|
|
326
|
+
background-color: #161a25;
|
|
327
|
+
border: 1px solid #232a3b;
|
|
328
|
+
border-radius: 6px;
|
|
329
|
+
padding: 4px;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
QMenu::item {
|
|
333
|
+
padding: 6px 20px;
|
|
334
|
+
border-radius: 4px;
|
|
335
|
+
color: #f1f5f9;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
QMenu::item:selected {
|
|
339
|
+
background-color: #6366f1;
|
|
340
|
+
color: #ffffff;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
QMenu::separator {
|
|
344
|
+
height: 1px;
|
|
345
|
+
background-color: #232a3b;
|
|
346
|
+
margin: 4px 8px;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
QToolTip {
|
|
350
|
+
background-color: #1e2433;
|
|
351
|
+
color: #f8fafc;
|
|
352
|
+
border: 1px solid #334155;
|
|
353
|
+
border-radius: 4px;
|
|
354
|
+
padding: 4px 8px;
|
|
355
|
+
font-size: 12px;
|
|
356
|
+
}
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def apply_theme(app: QApplication) -> None:
|
|
361
|
+
"""Apply the modern dark theme and palette to the QApplication."""
|
|
362
|
+
# Palette configuration
|
|
363
|
+
palette = QPalette()
|
|
364
|
+
palette.setColor(QPalette.ColorRole.Window, QColor("#0f1117"))
|
|
365
|
+
palette.setColor(QPalette.ColorRole.WindowText, QColor("#f1f5f9"))
|
|
366
|
+
palette.setColor(QPalette.ColorRole.Base, QColor("#0c0f16"))
|
|
367
|
+
palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#121620"))
|
|
368
|
+
palette.setColor(QPalette.ColorRole.ToolTipBase, QColor("#1e2433"))
|
|
369
|
+
palette.setColor(QPalette.ColorRole.ToolTipText, QColor("#f8fafc"))
|
|
370
|
+
palette.setColor(QPalette.ColorRole.Text, QColor("#f1f5f9"))
|
|
371
|
+
palette.setColor(QPalette.ColorRole.Button, QColor("#1e2433"))
|
|
372
|
+
palette.setColor(QPalette.ColorRole.ButtonText, QColor("#f1f5f9"))
|
|
373
|
+
palette.setColor(QPalette.ColorRole.BrightText, QColor("#ffffff"))
|
|
374
|
+
palette.setColor(QPalette.ColorRole.Highlight, QColor("#6366f1"))
|
|
375
|
+
palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
|
|
376
|
+
|
|
377
|
+
app.setPalette(palette)
|
|
378
|
+
app.setStyleSheet(MODERN_STYLESHEET)
|
|
@@ -10,7 +10,6 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
from PySide6.QtCore import Qt, Signal
|
|
12
12
|
from PySide6.QtWidgets import (
|
|
13
|
-
QFrame,
|
|
14
13
|
QGridLayout,
|
|
15
14
|
QGroupBox,
|
|
16
15
|
QHBoxLayout,
|
|
@@ -45,31 +44,43 @@ class DashboardView(QWidget):
|
|
|
45
44
|
main_layout.setContentsMargins(16, 16, 16, 16)
|
|
46
45
|
main_layout.setSpacing(16)
|
|
47
46
|
|
|
48
|
-
#
|
|
49
|
-
|
|
47
|
+
# Header
|
|
48
|
+
header_layout = QVBoxLayout()
|
|
49
|
+
header_layout.setSpacing(2)
|
|
50
|
+
title_label = QLabel("Dashboard")
|
|
50
51
|
title_font = title_label.font()
|
|
51
|
-
title_font.setPointSize(
|
|
52
|
+
title_font.setPointSize(18)
|
|
52
53
|
title_font.setBold(True)
|
|
53
54
|
title_label.setFont(title_font)
|
|
54
|
-
|
|
55
|
+
title_label.setStyleSheet("color: #f8fafc;")
|
|
56
|
+
header_layout.addWidget(title_label)
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
subtitle_label = QLabel(
|
|
59
|
+
"Overview of file activity, background watcher, and quick controls."
|
|
60
|
+
)
|
|
61
|
+
subtitle_label.setStyleSheet("color: #94a3b8; font-size: 12px;")
|
|
62
|
+
header_layout.addWidget(subtitle_label)
|
|
63
|
+
main_layout.addLayout(header_layout)
|
|
64
|
+
|
|
65
|
+
# Top row: Watcher Card & Stats Grid & Quick Actions
|
|
57
66
|
top_row = QHBoxLayout()
|
|
58
67
|
top_row.setSpacing(16)
|
|
59
68
|
|
|
60
69
|
# Watcher State Card
|
|
61
70
|
self.watcher_group = QGroupBox("Watcher Daemon")
|
|
62
71
|
watcher_layout = QVBoxLayout(self.watcher_group)
|
|
63
|
-
watcher_layout.setSpacing(
|
|
72
|
+
watcher_layout.setSpacing(10)
|
|
64
73
|
|
|
65
74
|
self.watcher_status_label = QLabel("Status: Checking...")
|
|
66
75
|
status_font = self.watcher_status_label.font()
|
|
67
76
|
status_font.setBold(True)
|
|
68
77
|
self.watcher_status_label.setFont(status_font)
|
|
78
|
+
self.watcher_status_label.setStyleSheet("color: #a5b4fc; font-size: 13px;")
|
|
69
79
|
watcher_layout.addWidget(self.watcher_status_label)
|
|
70
80
|
|
|
71
81
|
self.watcher_folders_label = QLabel("Watched Folders: None")
|
|
72
82
|
self.watcher_folders_label.setWordWrap(True)
|
|
83
|
+
self.watcher_folders_label.setStyleSheet("color: #94a3b8; font-size: 12px;")
|
|
73
84
|
watcher_layout.addWidget(self.watcher_folders_label)
|
|
74
85
|
|
|
75
86
|
self.watcher_toggle_btn = QPushButton("Toggle Watcher")
|
|
@@ -83,19 +94,31 @@ class DashboardView(QWidget):
|
|
|
83
94
|
stats_group = QGroupBox("Quick Stats")
|
|
84
95
|
stats_layout = QGridLayout(stats_group)
|
|
85
96
|
stats_layout.setSpacing(12)
|
|
97
|
+
stats_layout.setContentsMargins(14, 16, 14, 14)
|
|
86
98
|
|
|
87
|
-
|
|
99
|
+
lbl_folders = QLabel("Watched Folders:")
|
|
100
|
+
lbl_folders.setStyleSheet("color: #94a3b8;")
|
|
101
|
+
stats_layout.addWidget(lbl_folders, 0, 0)
|
|
88
102
|
self.folders_count_label = QLabel("0")
|
|
103
|
+
self.folders_count_label.setStyleSheet(
|
|
104
|
+
"font-size: 16px; font-weight: bold; color: #818cf8;"
|
|
105
|
+
)
|
|
89
106
|
self.folders_count_label.setAlignment(Qt.AlignmentFlag.AlignRight)
|
|
90
107
|
stats_layout.addWidget(self.folders_count_label, 0, 1)
|
|
91
108
|
|
|
92
|
-
|
|
109
|
+
lbl_today = QLabel("Today's Moves:")
|
|
110
|
+
lbl_today.setStyleSheet("color: #94a3b8;")
|
|
111
|
+
stats_layout.addWidget(lbl_today, 1, 0)
|
|
93
112
|
self.today_moves_label = QLabel("0")
|
|
113
|
+
self.today_moves_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #34d399;")
|
|
94
114
|
self.today_moves_label.setAlignment(Qt.AlignmentFlag.AlignRight)
|
|
95
115
|
stats_layout.addWidget(self.today_moves_label, 1, 1)
|
|
96
116
|
|
|
97
|
-
|
|
117
|
+
lbl_rules = QLabel("Active Rules:")
|
|
118
|
+
lbl_rules.setStyleSheet("color: #94a3b8;")
|
|
119
|
+
stats_layout.addWidget(lbl_rules, 2, 0)
|
|
98
120
|
self.rules_count_label = QLabel("0")
|
|
121
|
+
self.rules_count_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #fbbf24;")
|
|
99
122
|
self.rules_count_label.setAlignment(Qt.AlignmentFlag.AlignRight)
|
|
100
123
|
stats_layout.addWidget(self.rules_count_label, 2, 1)
|
|
101
124
|
|
|
@@ -107,15 +130,16 @@ class DashboardView(QWidget):
|
|
|
107
130
|
actions_layout = QVBoxLayout(actions_group)
|
|
108
131
|
actions_layout.setSpacing(8)
|
|
109
132
|
|
|
110
|
-
self.
|
|
111
|
-
self.
|
|
112
|
-
actions_layout.addWidget(self.scan_btn)
|
|
113
|
-
|
|
114
|
-
self.organize_btn = QPushButton("Organize (Preview)")
|
|
133
|
+
self.organize_btn = QPushButton("⚡ Organize (Preview)")
|
|
134
|
+
self.organize_btn.setProperty("primary", True)
|
|
115
135
|
self.organize_btn.clicked.connect(lambda: self.navigate_requested.emit("preview"))
|
|
116
136
|
actions_layout.addWidget(self.organize_btn)
|
|
117
137
|
|
|
118
|
-
self.
|
|
138
|
+
self.scan_btn = QPushButton("🔍 Scan Folders")
|
|
139
|
+
self.scan_btn.clicked.connect(lambda: self.navigate_requested.emit("preview"))
|
|
140
|
+
actions_layout.addWidget(self.scan_btn)
|
|
141
|
+
|
|
142
|
+
self.undo_last_btn = QPushButton("↩️ Undo Last Batch")
|
|
119
143
|
self.undo_last_btn.clicked.connect(self._on_undo_last_batch)
|
|
120
144
|
actions_layout.addWidget(self.undo_last_btn)
|
|
121
145
|
|
|
@@ -124,20 +148,18 @@ class DashboardView(QWidget):
|
|
|
124
148
|
|
|
125
149
|
main_layout.addLayout(top_row)
|
|
126
150
|
|
|
127
|
-
# Separator line
|
|
128
|
-
line = QFrame()
|
|
129
|
-
line.setFrameShape(QFrame.Shape.HLine)
|
|
130
|
-
line.setFrameShadow(QFrame.Shadow.Sunken)
|
|
131
|
-
main_layout.addWidget(line)
|
|
132
|
-
|
|
133
151
|
# Bottom section: Recent Operations
|
|
134
152
|
recent_group = QGroupBox("Recent Operations (Last 5)")
|
|
135
153
|
recent_layout = QVBoxLayout(recent_group)
|
|
154
|
+
recent_layout.setSpacing(10)
|
|
136
155
|
|
|
137
156
|
self.recent_table = QTableView()
|
|
138
157
|
self.recent_table.setModel(self._history_model)
|
|
158
|
+
self.recent_table.setAlternatingRowColors(True)
|
|
159
|
+
self.recent_table.setShowGrid(False)
|
|
139
160
|
self.recent_table.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
|
140
161
|
self.recent_table.setSelectionMode(QTableView.SelectionMode.SingleSelection)
|
|
162
|
+
self.recent_table.verticalHeader().setDefaultSectionSize(32)
|
|
141
163
|
self.recent_table.horizontalHeader().setSectionResizeMode(
|
|
142
164
|
QHeaderView.ResizeMode.ResizeToContents
|
|
143
165
|
)
|
|
@@ -146,7 +168,7 @@ class DashboardView(QWidget):
|
|
|
146
168
|
|
|
147
169
|
btn_row = QHBoxLayout()
|
|
148
170
|
btn_row.addStretch()
|
|
149
|
-
self.view_history_btn = QPushButton("View Full History")
|
|
171
|
+
self.view_history_btn = QPushButton("View Full History →")
|
|
150
172
|
self.view_history_btn.clicked.connect(lambda: self.navigate_requested.emit("history"))
|
|
151
173
|
btn_row.addWidget(self.view_history_btn)
|
|
152
174
|
recent_layout.addLayout(btn_row)
|
|
@@ -106,16 +106,27 @@ class DuplicatesView(QWidget):
|
|
|
106
106
|
main_layout.setSpacing(12)
|
|
107
107
|
|
|
108
108
|
# Header
|
|
109
|
+
header_layout = QVBoxLayout()
|
|
110
|
+
header_layout.setSpacing(2)
|
|
109
111
|
title_label = QLabel("Duplicate File Finder & Quarantine")
|
|
110
112
|
title_font = title_label.font()
|
|
111
|
-
title_font.setPointSize(
|
|
113
|
+
title_font.setPointSize(18)
|
|
112
114
|
title_font.setBold(True)
|
|
113
115
|
title_label.setFont(title_font)
|
|
114
|
-
|
|
116
|
+
title_label.setStyleSheet("color: #f8fafc;")
|
|
117
|
+
header_layout.addWidget(title_label)
|
|
118
|
+
|
|
119
|
+
subtitle_label = QLabel(
|
|
120
|
+
"Find duplicate downloads taking up storage and safely isolate them without deleting."
|
|
121
|
+
)
|
|
122
|
+
subtitle_label.setStyleSheet("color: #94a3b8; font-size: 12px;")
|
|
123
|
+
header_layout.addWidget(subtitle_label)
|
|
124
|
+
main_layout.addLayout(header_layout)
|
|
115
125
|
|
|
116
126
|
# Folder Picker & Scan Controls Box
|
|
117
127
|
scan_box = QGroupBox("Scan Targets & Parameters")
|
|
118
128
|
scan_layout = QVBoxLayout(scan_box)
|
|
129
|
+
scan_layout.setSpacing(10)
|
|
119
130
|
|
|
120
131
|
folders_row = QHBoxLayout()
|
|
121
132
|
self.folders_list = QListWidget()
|
|
@@ -140,6 +151,7 @@ class DuplicatesView(QWidget):
|
|
|
140
151
|
scan_layout.addLayout(folders_row)
|
|
141
152
|
|
|
142
153
|
options_row = QHBoxLayout()
|
|
154
|
+
options_row.setSpacing(10)
|
|
143
155
|
self.recursive_cb = QCheckBox("Recursive")
|
|
144
156
|
self.recursive_cb.setChecked(True)
|
|
145
157
|
options_row.addWidget(self.recursive_cb)
|
|
@@ -155,8 +167,8 @@ class DuplicatesView(QWidget):
|
|
|
155
167
|
|
|
156
168
|
options_row.addStretch()
|
|
157
169
|
|
|
158
|
-
self.scan_btn = QPushButton("Start Scan")
|
|
159
|
-
self.scan_btn.
|
|
170
|
+
self.scan_btn = QPushButton("🔍 Start Scan")
|
|
171
|
+
self.scan_btn.setProperty("primary", True)
|
|
160
172
|
self.scan_btn.clicked.connect(self.start_scan)
|
|
161
173
|
options_row.addWidget(self.scan_btn)
|
|
162
174
|
|
|
@@ -179,9 +191,12 @@ class DuplicatesView(QWidget):
|
|
|
179
191
|
groups_container = QWidget()
|
|
180
192
|
groups_layout = QVBoxLayout(groups_container)
|
|
181
193
|
groups_layout.setContentsMargins(0, 0, 0, 0)
|
|
194
|
+
groups_layout.setSpacing(8)
|
|
182
195
|
|
|
183
196
|
filter_row = QHBoxLayout()
|
|
184
|
-
|
|
197
|
+
lbl_show = QLabel("Show Groups:")
|
|
198
|
+
lbl_show.setStyleSheet("color: #94a3b8;")
|
|
199
|
+
filter_row.addWidget(lbl_show)
|
|
185
200
|
self.kind_combo = QComboBox()
|
|
186
201
|
self.kind_combo.addItem("All Duplicate Groups", "all")
|
|
187
202
|
self.kind_combo.addItem("Exact Duplicates Only", "exact")
|
|
@@ -194,8 +209,11 @@ class DuplicatesView(QWidget):
|
|
|
194
209
|
|
|
195
210
|
self.table = QTableView()
|
|
196
211
|
self.table.setModel(self._model)
|
|
212
|
+
self.table.setAlternatingRowColors(True)
|
|
213
|
+
self.table.setShowGrid(False)
|
|
197
214
|
self.table.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
|
198
215
|
self.table.setSelectionMode(QTableView.SelectionMode.SingleSelection)
|
|
216
|
+
self.table.verticalHeader().setDefaultSectionSize(32)
|
|
199
217
|
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
|
200
218
|
self.table.horizontalHeader().setStretchLastSection(True)
|
|
201
219
|
self.table.selectionModel().selectionChanged.connect(self._on_group_selected)
|
|
@@ -191,16 +191,27 @@ class FoldersView(QWidget):
|
|
|
191
191
|
|
|
192
192
|
# Header with Title and Watcher Control
|
|
193
193
|
top_bar = QHBoxLayout()
|
|
194
|
+
header_layout = QVBoxLayout()
|
|
195
|
+
header_layout.setSpacing(2)
|
|
194
196
|
title_label = QLabel("Watch Folders")
|
|
195
197
|
title_font = title_label.font()
|
|
196
|
-
title_font.setPointSize(
|
|
198
|
+
title_font.setPointSize(18)
|
|
197
199
|
title_font.setBold(True)
|
|
198
200
|
title_label.setFont(title_font)
|
|
199
|
-
|
|
201
|
+
title_label.setStyleSheet("color: #f8fafc;")
|
|
202
|
+
header_layout.addWidget(title_label)
|
|
203
|
+
|
|
204
|
+
subtitle_label = QLabel("Directories monitored by ClutterXP for automatic, quiet sorting.")
|
|
205
|
+
subtitle_label.setStyleSheet("color: #94a3b8; font-size: 12px;")
|
|
206
|
+
header_layout.addWidget(subtitle_label)
|
|
207
|
+
top_bar.addLayout(header_layout)
|
|
200
208
|
|
|
201
209
|
top_bar.addStretch()
|
|
202
210
|
|
|
203
211
|
self.watcher_status_label = QLabel("Watcher: Checking...")
|
|
212
|
+
self.watcher_status_label.setStyleSheet(
|
|
213
|
+
"color: #a5b4fc; font-weight: 500; font-size: 13px; margin-right: 8px;"
|
|
214
|
+
)
|
|
204
215
|
top_bar.addWidget(self.watcher_status_label)
|
|
205
216
|
|
|
206
217
|
self.watcher_toggle_btn = QPushButton("Start Watcher")
|
|
@@ -212,15 +223,19 @@ class FoldersView(QWidget):
|
|
|
212
223
|
# Table of folders
|
|
213
224
|
self.table = QTableView()
|
|
214
225
|
self.table.setModel(self._model)
|
|
226
|
+
self.table.setAlternatingRowColors(True)
|
|
227
|
+
self.table.setShowGrid(False)
|
|
215
228
|
self.table.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
|
216
229
|
self.table.setSelectionMode(QTableView.SelectionMode.SingleSelection)
|
|
230
|
+
self.table.verticalHeader().setDefaultSectionSize(32)
|
|
217
231
|
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
|
218
232
|
self.table.horizontalHeader().setStretchLastSection(True)
|
|
219
233
|
main_layout.addWidget(self.table, stretch=1)
|
|
220
234
|
|
|
221
235
|
# Buttons row
|
|
222
236
|
btn_row = QHBoxLayout()
|
|
223
|
-
self.add_btn = QPushButton("Add Folder...")
|
|
237
|
+
self.add_btn = QPushButton("+ Add Folder...")
|
|
238
|
+
self.add_btn.setProperty("primary", True)
|
|
224
239
|
self.add_btn.clicked.connect(self._on_add_folder)
|
|
225
240
|
btn_row.addWidget(self.add_btn)
|
|
226
241
|
|