chronocatalog-desktop 0.1.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.
- chronocatalog_desktop-0.1.0/.github/workflows/ci.yml +61 -0
- chronocatalog_desktop-0.1.0/.github/workflows/release.yml +139 -0
- chronocatalog_desktop-0.1.0/.gitignore +11 -0
- chronocatalog_desktop-0.1.0/CHANGELOG.md +67 -0
- chronocatalog_desktop-0.1.0/CONTRIBUTING.md +42 -0
- chronocatalog_desktop-0.1.0/ChronoCatalog.spec +35 -0
- chronocatalog_desktop-0.1.0/LICENSE +21 -0
- chronocatalog_desktop-0.1.0/PKG-INFO +102 -0
- chronocatalog_desktop-0.1.0/README.md +66 -0
- chronocatalog_desktop-0.1.0/assets/make_icon.py +144 -0
- chronocatalog_desktop-0.1.0/demo/make_demo.py +111 -0
- chronocatalog_desktop-0.1.0/pyproject.toml +87 -0
- chronocatalog_desktop-0.1.0/src/chronocatalog_desktop/__init__.py +3 -0
- chronocatalog_desktop-0.1.0/src/chronocatalog_desktop/__main__.py +8 -0
- chronocatalog_desktop-0.1.0/src/chronocatalog_desktop/app.py +259 -0
- chronocatalog_desktop-0.1.0/src/chronocatalog_desktop/base.py +305 -0
- chronocatalog_desktop-0.1.0/src/chronocatalog_desktop/buckets.py +88 -0
- chronocatalog_desktop-0.1.0/src/chronocatalog_desktop/history.py +176 -0
- chronocatalog_desktop-0.1.0/src/chronocatalog_desktop/import_view.py +346 -0
- chronocatalog_desktop-0.1.0/src/chronocatalog_desktop/organize.py +229 -0
- chronocatalog_desktop-0.1.0/src/chronocatalog_desktop/overview.py +59 -0
- chronocatalog_desktop-0.1.0/src/chronocatalog_desktop/rename.py +312 -0
- chronocatalog_desktop-0.1.0/src/chronocatalog_desktop/resources/icon.png +0 -0
- chronocatalog_desktop-0.1.0/src/chronocatalog_desktop/settings.py +330 -0
- chronocatalog_desktop-0.1.0/src/chronocatalog_desktop/theme.py +156 -0
- chronocatalog_desktop-0.1.0/src/chronocatalog_desktop/verify.py +181 -0
- chronocatalog_desktop-0.1.0/src/chronocatalog_desktop/worker.py +88 -0
- chronocatalog_desktop-0.1.0/tests/__init__.py +0 -0
- chronocatalog_desktop-0.1.0/tests/conftest.py +27 -0
- chronocatalog_desktop-0.1.0/tests/support.py +82 -0
- chronocatalog_desktop-0.1.0/tests/test_app.py +84 -0
- chronocatalog_desktop-0.1.0/tests/test_cli_panel.py +69 -0
- chronocatalog_desktop-0.1.0/tests/test_history_view.py +97 -0
- chronocatalog_desktop-0.1.0/tests/test_import_view.py +125 -0
- chronocatalog_desktop-0.1.0/tests/test_organize_view.py +118 -0
- chronocatalog_desktop-0.1.0/tests/test_rename_dam.py +115 -0
- chronocatalog_desktop-0.1.0/tests/test_rename_view.py +72 -0
- chronocatalog_desktop-0.1.0/tests/test_scale.py +97 -0
- chronocatalog_desktop-0.1.0/tests/test_settings_view.py +102 -0
- chronocatalog_desktop-0.1.0/tests/test_theme.py +18 -0
- chronocatalog_desktop-0.1.0/tests/test_verify_view.py +60 -0
- chronocatalog_desktop-0.1.0/tests/test_worker.py +69 -0
- chronocatalog_desktop-0.1.0/uv.lock +566 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
env:
|
|
16
|
+
QT_QPA_PLATFORM: offscreen
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
lint:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v7.0.0
|
|
23
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.13"
|
|
26
|
+
- name: Install package
|
|
27
|
+
run: python -m pip install -e ".[dev]"
|
|
28
|
+
- name: Ruff lint
|
|
29
|
+
run: ruff check .
|
|
30
|
+
- name: Ruff format
|
|
31
|
+
run: ruff format --check .
|
|
32
|
+
- name: Mypy
|
|
33
|
+
run: mypy
|
|
34
|
+
|
|
35
|
+
test:
|
|
36
|
+
strategy:
|
|
37
|
+
fail-fast: false
|
|
38
|
+
matrix:
|
|
39
|
+
os: [ubuntu-latest, macos-latest]
|
|
40
|
+
python-version: ["3.11", "3.13"]
|
|
41
|
+
runs-on: ${{ matrix.os }}
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v7.0.0
|
|
44
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
|
45
|
+
with:
|
|
46
|
+
python-version: ${{ matrix.python-version }}
|
|
47
|
+
- name: Install Qt runtime libraries (Linux)
|
|
48
|
+
if: runner.os == 'Linux'
|
|
49
|
+
run: sudo apt-get update && sudo apt-get install -y libegl1 libgl1 libxkbcommon0
|
|
50
|
+
- name: Install ExifTool (Linux)
|
|
51
|
+
if: runner.os == 'Linux'
|
|
52
|
+
run: sudo apt-get install -y libimage-exiftool-perl
|
|
53
|
+
- name: Install ExifTool (macOS)
|
|
54
|
+
if: runner.os == 'macOS'
|
|
55
|
+
run: brew install exiftool
|
|
56
|
+
- name: Show ExifTool version
|
|
57
|
+
run: exiftool -ver
|
|
58
|
+
- name: Install package
|
|
59
|
+
run: python -m pip install -e ".[dev]"
|
|
60
|
+
- name: Run tests
|
|
61
|
+
run: pytest --cov=chronocatalog_desktop --cov-report=term-missing --cov-fail-under=85
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
env:
|
|
11
|
+
QT_QPA_PLATFORM: offscreen
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
18
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.13"
|
|
21
|
+
- name: Install Qt runtime libraries
|
|
22
|
+
run: sudo apt-get update && sudo apt-get install -y libegl1 libgl1 libxkbcommon0
|
|
23
|
+
- name: Install ExifTool
|
|
24
|
+
run: sudo apt-get install -y libimage-exiftool-perl
|
|
25
|
+
- name: Run checks
|
|
26
|
+
run: |
|
|
27
|
+
python -m pip install -e ".[dev]"
|
|
28
|
+
ruff check .
|
|
29
|
+
ruff format --check .
|
|
30
|
+
mypy
|
|
31
|
+
pytest -q
|
|
32
|
+
- name: Tag must match the package version
|
|
33
|
+
run: |
|
|
34
|
+
version=$(python -c "import chronocatalog_desktop; print(chronocatalog_desktop.__version__)")
|
|
35
|
+
test "v$version" = "$GITHUB_REF_NAME" || {
|
|
36
|
+
echo "tag $GITHUB_REF_NAME does not match version $version" >&2
|
|
37
|
+
exit 1
|
|
38
|
+
}
|
|
39
|
+
- name: Build distributions
|
|
40
|
+
run: |
|
|
41
|
+
python -m pip install build twine
|
|
42
|
+
python -m build
|
|
43
|
+
twine check dist/*
|
|
44
|
+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/
|
|
48
|
+
|
|
49
|
+
publish:
|
|
50
|
+
needs: build
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
environment: pypi
|
|
53
|
+
permissions:
|
|
54
|
+
# PyPI Trusted Publishing: configure this repository as a trusted
|
|
55
|
+
# publisher on pypi.org (project: chronocatalog-desktop, workflow:
|
|
56
|
+
# release.yml, environment: pypi). No stored tokens.
|
|
57
|
+
id-token: write
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
60
|
+
with:
|
|
61
|
+
name: dist
|
|
62
|
+
path: dist/
|
|
63
|
+
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
|
|
64
|
+
|
|
65
|
+
github-release:
|
|
66
|
+
needs: build
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
permissions:
|
|
69
|
+
contents: write
|
|
70
|
+
steps:
|
|
71
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
72
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
73
|
+
with:
|
|
74
|
+
name: dist
|
|
75
|
+
path: dist/
|
|
76
|
+
- name: Extract this version's changelog section
|
|
77
|
+
run: |
|
|
78
|
+
awk -v version="${GITHUB_REF_NAME#v}" '
|
|
79
|
+
$0 ~ "^## \\[" version "\\]" { found = 1; next }
|
|
80
|
+
found && /^## \[/ { exit }
|
|
81
|
+
found { print }
|
|
82
|
+
' CHANGELOG.md > release-notes.md
|
|
83
|
+
test -s release-notes.md || {
|
|
84
|
+
echo "no changelog section for $GITHUB_REF_NAME" >&2
|
|
85
|
+
exit 1
|
|
86
|
+
}
|
|
87
|
+
- name: Create GitHub release
|
|
88
|
+
env:
|
|
89
|
+
GH_TOKEN: ${{ github.token }}
|
|
90
|
+
run: |
|
|
91
|
+
gh release create "$GITHUB_REF_NAME" dist/* \
|
|
92
|
+
--repo "$GITHUB_REPOSITORY" \
|
|
93
|
+
--title "$GITHUB_REF_NAME" \
|
|
94
|
+
--notes-file release-notes.md
|
|
95
|
+
|
|
96
|
+
macos-app:
|
|
97
|
+
# Unsigned ad-hoc build: macOS blocks the first launch until the user
|
|
98
|
+
# approves it under System Settings > Privacy & Security > Open Anyway.
|
|
99
|
+
# Signing and notarization land when there is a Developer ID to sign with.
|
|
100
|
+
needs: github-release
|
|
101
|
+
runs-on: macos-15 # arm64
|
|
102
|
+
permissions:
|
|
103
|
+
contents: write
|
|
104
|
+
steps:
|
|
105
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
106
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
|
107
|
+
with:
|
|
108
|
+
python-version: "3.13"
|
|
109
|
+
- name: Build the app bundle
|
|
110
|
+
run: |
|
|
111
|
+
python -m pip install -e . pyinstaller
|
|
112
|
+
python assets/make_icon.py
|
|
113
|
+
pyinstaller --noconfirm ChronoCatalog.spec
|
|
114
|
+
- name: App must start
|
|
115
|
+
env:
|
|
116
|
+
QT_QPA_PLATFORM: offscreen
|
|
117
|
+
run: |
|
|
118
|
+
dist/ChronoCatalog.app/Contents/MacOS/ChronoCatalog &
|
|
119
|
+
sleep 8
|
|
120
|
+
kill -0 $! || { echo "bundled app exited early" >&2; exit 1; }
|
|
121
|
+
kill $!
|
|
122
|
+
- name: Package the disk image
|
|
123
|
+
run: |
|
|
124
|
+
brew install create-dmg
|
|
125
|
+
create-dmg \
|
|
126
|
+
--volname ChronoCatalog \
|
|
127
|
+
--window-size 540 380 \
|
|
128
|
+
--icon-size 128 \
|
|
129
|
+
--icon ChronoCatalog.app 140 180 \
|
|
130
|
+
--app-drop-link 400 180 \
|
|
131
|
+
"dist/ChronoCatalog-$GITHUB_REF_NAME-arm64.dmg" \
|
|
132
|
+
dist/ChronoCatalog.app
|
|
133
|
+
- name: Attach to the release
|
|
134
|
+
env:
|
|
135
|
+
GH_TOKEN: ${{ github.token }}
|
|
136
|
+
run: |
|
|
137
|
+
gh release upload "$GITHUB_REF_NAME" \
|
|
138
|
+
"dist/ChronoCatalog-$GITHUB_REF_NAME-arm64.dmg" \
|
|
139
|
+
--repo "$GITHUB_REPOSITORY"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-07-10
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Files excluded by import ignore patterns are counted in the Organize
|
|
13
|
+
summary, and a folder where every file is excluded says so explicitly
|
|
14
|
+
in Organize and Import instead of rendering as empty.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Groups replace families throughout, matching chronocatalog 0.2
|
|
19
|
+
vocabulary; requires chronocatalog >= 0.2.
|
|
20
|
+
- Depends on PySide6-Essentials instead of the full PySide6
|
|
21
|
+
metapackage — the app uses no Addons module, and the packaged
|
|
22
|
+
bundle is roughly half the size.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- App icon: an aperture iris with clock hands in the opening, drawn
|
|
27
|
+
from geometry at every size (small sizes are redrawn bolder, not
|
|
28
|
+
downscaled).
|
|
29
|
+
- macOS app bundle: releases attach an Apple Silicon .dmg built with
|
|
30
|
+
PyInstaller; the bundle is ad-hoc signed until there is a Developer
|
|
31
|
+
ID, and the README documents the one-time Open Anyway step.
|
|
32
|
+
- Dark theme: graphite palette with a safelight-amber accent.
|
|
33
|
+
- Background worker: library calls run off the UI thread with
|
|
34
|
+
throttled progress events and a cooperative stop flag.
|
|
35
|
+
- Application shell: archive-centric window with a sidebar of views
|
|
36
|
+
and an Overview of the archive and its trees.
|
|
37
|
+
- Verify view: findings in library order colored by library severity,
|
|
38
|
+
plain-language explanations, structured date-mismatch details, live
|
|
39
|
+
progress and Stop.
|
|
40
|
+
- History view: every run against the archive with its originating
|
|
41
|
+
command, timestamp and status; Undo for applied runs, Resume for
|
|
42
|
+
interrupted ones.
|
|
43
|
+
- Rename view: the plan as old → new with the changed span
|
|
44
|
+
highlighted, groups kept whole, apply with confirm and Stop.
|
|
45
|
+
- Terminal transparency: a quiet >_ toggle reveals each action's
|
|
46
|
+
exact CLI command with Copy, and confirmation dialogs carry the
|
|
47
|
+
command under Show Details.
|
|
48
|
+
- Demo archive generator for a safe tour of every view.
|
|
49
|
+
- Import view: card to archive with live progress, problem list and
|
|
50
|
+
the safe-to-format verdict — green only when the library itself
|
|
51
|
+
issues it.
|
|
52
|
+
- Organize view: report-only triage of messy folders with a hand-off
|
|
53
|
+
to Import for confirmed batches.
|
|
54
|
+
- DAM hand-off in the Rename view: masters the DAM must rename itself
|
|
55
|
+
are listed with their tokens and the in-DAM checklist; writing tokens
|
|
56
|
+
is its own confirmed action, verified by reading each token back.
|
|
57
|
+
- Hardening for huge archives: capped rendering with exact counts
|
|
58
|
+
everywhere, accurate post-apply import summaries (failed groups are
|
|
59
|
+
never reported as copied), History capped and guarded against
|
|
60
|
+
double-clicks.
|
|
61
|
+
- Settings view: edit the archive configuration with validation before
|
|
62
|
+
every save and hand-written comments preserved; the naming pattern is
|
|
63
|
+
shown read-only. First-run flow can create a new archive config.
|
|
64
|
+
- Design pass: one clean surface per card (no label striping), sidebar
|
|
65
|
+
wordmark, empty states on every action view, tinted verdict banner,
|
|
66
|
+
severity stripes, status pills, calm undo, styled form controls, and
|
|
67
|
+
per-view status messages that can no longer go stale.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Development setup
|
|
4
|
+
|
|
5
|
+
```console
|
|
6
|
+
$ python3 -m venv .venv
|
|
7
|
+
$ .venv/bin/pip install -e ".[dev]"
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
To develop against a local `chronocatalog` checkout (changing library
|
|
11
|
+
and app in lockstep), install it editable first:
|
|
12
|
+
|
|
13
|
+
```console
|
|
14
|
+
$ .venv/bin/pip install -e ../chronocatalog -e ".[dev]"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
[ExifTool](https://exiftool.org/) must be on `PATH`; integration tests
|
|
18
|
+
skip without it, but CI always runs them. Tests run Qt headless
|
|
19
|
+
(`QT_QPA_PLATFORM=offscreen`); no display is needed.
|
|
20
|
+
|
|
21
|
+
## Checks
|
|
22
|
+
|
|
23
|
+
Everything CI runs, locally:
|
|
24
|
+
|
|
25
|
+
```console
|
|
26
|
+
$ ruff format --check .
|
|
27
|
+
$ ruff check .
|
|
28
|
+
$ mypy
|
|
29
|
+
$ pytest --cov=chronocatalog_desktop
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
All four must pass; coverage is gated at 85%.
|
|
33
|
+
|
|
34
|
+
## Expectations
|
|
35
|
+
|
|
36
|
+
- Every change ships with tests and any needed documentation in the same
|
|
37
|
+
commit.
|
|
38
|
+
- The app adds no behavior of its own: every screen renders the
|
|
39
|
+
library's plans and reports, and every mutation goes through the same
|
|
40
|
+
validated, journaled engine as the CLI. Treat a change that would
|
|
41
|
+
bypass that as a design discussion, not a patch.
|
|
42
|
+
- Conventional Commits (`feat:`, `fix:`, `docs:`, `ci:`, `chore:`).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# PyInstaller build for the macOS app bundle.
|
|
2
|
+
# Build with: pyinstaller --noconfirm ChronoCatalog.spec
|
|
3
|
+
|
|
4
|
+
a = Analysis(
|
|
5
|
+
["src/chronocatalog_desktop/__main__.py"],
|
|
6
|
+
datas=[("src/chronocatalog_desktop/resources", "chronocatalog_desktop/resources")],
|
|
7
|
+
)
|
|
8
|
+
pyz = PYZ(a.pure)
|
|
9
|
+
|
|
10
|
+
exe = EXE(
|
|
11
|
+
pyz,
|
|
12
|
+
a.scripts,
|
|
13
|
+
exclude_binaries=True,
|
|
14
|
+
name="ChronoCatalog",
|
|
15
|
+
console=False,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
coll = COLLECT(
|
|
19
|
+
exe,
|
|
20
|
+
a.binaries,
|
|
21
|
+
a.datas,
|
|
22
|
+
name="ChronoCatalog",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
app = BUNDLE(
|
|
26
|
+
coll,
|
|
27
|
+
name="ChronoCatalog.app",
|
|
28
|
+
icon="assets/icon.icns",
|
|
29
|
+
bundle_identifier="org.chronocatalog.desktop",
|
|
30
|
+
info_plist={
|
|
31
|
+
"CFBundleDisplayName": "ChronoCatalog",
|
|
32
|
+
"NSHighResolutionCapable": True,
|
|
33
|
+
"LSMinimumSystemVersion": "12.0",
|
|
34
|
+
},
|
|
35
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jakub Stefanski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chronocatalog-desktop
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Desktop app for ChronoCatalog photo and video archives
|
|
5
|
+
Project-URL: Homepage, https://github.com/chronocatalog/chronocatalog-desktop
|
|
6
|
+
Project-URL: Repository, https://github.com/chronocatalog/chronocatalog-desktop
|
|
7
|
+
Project-URL: Changelog, https://github.com/chronocatalog/chronocatalog-desktop/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Issues, https://github.com/chronocatalog/chronocatalog-desktop/issues
|
|
9
|
+
Author-email: Jakub Stefanski <js@jakubstefanski.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: archive,exif,gui,photography,qt,rename,video
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: MacOS X
|
|
15
|
+
Classifier: Environment :: X11 Applications :: Qt
|
|
16
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
24
|
+
Classifier: Topic :: System :: Archiving
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: chronocatalog>=0.2
|
|
27
|
+
Requires-Dist: pyside6-essentials>=6.7
|
|
28
|
+
Requires-Dist: tomlkit>=0.12
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
31
|
+
Requires-Dist: pyside6>=6.7; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# ChronoCatalog Desktop
|
|
38
|
+
|
|
39
|
+
Desktop app for [ChronoCatalog](https://github.com/chronocatalog/chronocatalog)
|
|
40
|
+
photo and video archives.
|
|
41
|
+
|
|
42
|
+
The design premise: the CLI already plans everything before touching
|
|
43
|
+
anything, so the app is a renderer of those plans and reports — every
|
|
44
|
+
view is a dry run, and Apply goes through the same validated, journaled
|
|
45
|
+
engine. Views are named after the commands they wrap (Verify, Rename,
|
|
46
|
+
History), and the exact terminal equivalent of every action is one
|
|
47
|
+
click away.
|
|
48
|
+
|
|
49
|
+
## Status
|
|
50
|
+
|
|
51
|
+
Early development.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
**macOS (Apple Silicon):** download `ChronoCatalog-<version>-arm64.dmg`
|
|
56
|
+
from [Releases](https://github.com/chronocatalog/chronocatalog-desktop/releases)
|
|
57
|
+
and drag the app to Applications. The build is not yet signed with an
|
|
58
|
+
Apple Developer ID, so the first launch is blocked: open **System
|
|
59
|
+
Settings → Privacy & Security**, scroll to Security and click
|
|
60
|
+
**Open Anyway**. This is needed once.
|
|
61
|
+
|
|
62
|
+
**Any platform (Python 3.11+):**
|
|
63
|
+
|
|
64
|
+
```console
|
|
65
|
+
$ uv tool install chronocatalog-desktop
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
or `pipx install chronocatalog-desktop`. Either way you also need
|
|
69
|
+
[ExifTool](https://exiftool.org/) on `PATH` (macOS:
|
|
70
|
+
`brew install exiftool`).
|
|
71
|
+
|
|
72
|
+
## Run
|
|
73
|
+
|
|
74
|
+
```console
|
|
75
|
+
$ chronocatalog-desktop [archive.toml]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Without an argument the app asks for an archive configuration and
|
|
79
|
+
remembers it. Everything is a dry run until Apply, which confirms
|
|
80
|
+
first; every applied change lands in History with Undo (and Resume,
|
|
81
|
+
for interrupted runs).
|
|
82
|
+
|
|
83
|
+
## Demo archive
|
|
84
|
+
|
|
85
|
+
Builds a small throwaway archive under `demo/archive` (macOS: uses
|
|
86
|
+
`sips`), imports it through the real engine, then plants a date edit,
|
|
87
|
+
an unnamed file and an orphan sidecar so every view has something to
|
|
88
|
+
show:
|
|
89
|
+
|
|
90
|
+
```console
|
|
91
|
+
$ python demo/make_demo.py
|
|
92
|
+
$ chronocatalog-desktop demo/demo.toml
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Requirements
|
|
96
|
+
|
|
97
|
+
- Python 3.11+
|
|
98
|
+
- [ExifTool](https://exiftool.org/) on `PATH`
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# ChronoCatalog Desktop
|
|
2
|
+
|
|
3
|
+
Desktop app for [ChronoCatalog](https://github.com/chronocatalog/chronocatalog)
|
|
4
|
+
photo and video archives.
|
|
5
|
+
|
|
6
|
+
The design premise: the CLI already plans everything before touching
|
|
7
|
+
anything, so the app is a renderer of those plans and reports — every
|
|
8
|
+
view is a dry run, and Apply goes through the same validated, journaled
|
|
9
|
+
engine. Views are named after the commands they wrap (Verify, Rename,
|
|
10
|
+
History), and the exact terminal equivalent of every action is one
|
|
11
|
+
click away.
|
|
12
|
+
|
|
13
|
+
## Status
|
|
14
|
+
|
|
15
|
+
Early development.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
**macOS (Apple Silicon):** download `ChronoCatalog-<version>-arm64.dmg`
|
|
20
|
+
from [Releases](https://github.com/chronocatalog/chronocatalog-desktop/releases)
|
|
21
|
+
and drag the app to Applications. The build is not yet signed with an
|
|
22
|
+
Apple Developer ID, so the first launch is blocked: open **System
|
|
23
|
+
Settings → Privacy & Security**, scroll to Security and click
|
|
24
|
+
**Open Anyway**. This is needed once.
|
|
25
|
+
|
|
26
|
+
**Any platform (Python 3.11+):**
|
|
27
|
+
|
|
28
|
+
```console
|
|
29
|
+
$ uv tool install chronocatalog-desktop
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
or `pipx install chronocatalog-desktop`. Either way you also need
|
|
33
|
+
[ExifTool](https://exiftool.org/) on `PATH` (macOS:
|
|
34
|
+
`brew install exiftool`).
|
|
35
|
+
|
|
36
|
+
## Run
|
|
37
|
+
|
|
38
|
+
```console
|
|
39
|
+
$ chronocatalog-desktop [archive.toml]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Without an argument the app asks for an archive configuration and
|
|
43
|
+
remembers it. Everything is a dry run until Apply, which confirms
|
|
44
|
+
first; every applied change lands in History with Undo (and Resume,
|
|
45
|
+
for interrupted runs).
|
|
46
|
+
|
|
47
|
+
## Demo archive
|
|
48
|
+
|
|
49
|
+
Builds a small throwaway archive under `demo/archive` (macOS: uses
|
|
50
|
+
`sips`), imports it through the real engine, then plants a date edit,
|
|
51
|
+
an unnamed file and an orphan sidecar so every view has something to
|
|
52
|
+
show:
|
|
53
|
+
|
|
54
|
+
```console
|
|
55
|
+
$ python demo/make_demo.py
|
|
56
|
+
$ chronocatalog-desktop demo/demo.toml
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Requirements
|
|
60
|
+
|
|
61
|
+
- Python 3.11+
|
|
62
|
+
- [ExifTool](https://exiftool.org/) on `PATH`
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Render the app icon: an aperture iris with clock hands in the opening.
|
|
2
|
+
|
|
3
|
+
Regenerates every size from geometry — there is no source image. Small
|
|
4
|
+
sizes are redrawn with heavier strokes and less detail rather than
|
|
5
|
+
downscaled, so the mark stays legible in the Dock and the menu bar.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python assets/make_icon.py
|
|
9
|
+
|
|
10
|
+
Writes the window icon into the package resources, and on macOS also
|
|
11
|
+
builds assets/icon.icns via iconutil.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import math
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|
24
|
+
|
|
25
|
+
from PySide6 import QtCore, QtGui, QtWidgets
|
|
26
|
+
|
|
27
|
+
GRAPHITE = QtGui.QColor("#1e2128")
|
|
28
|
+
GRAPHITE_EDGE = QtGui.QColor("#16181d")
|
|
29
|
+
AMBER = QtGui.QColor("#e8a33d")
|
|
30
|
+
HANDS = QtGui.QColor("#e9e7e2")
|
|
31
|
+
|
|
32
|
+
#: side of the generated square, one render per icns slot
|
|
33
|
+
ICONSET_SIZES = (16, 32, 64, 128, 256, 512, 1024)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _pen(color: QtGui.QColor, width: float) -> QtGui.QPen:
|
|
37
|
+
pen = QtGui.QPen(color, width)
|
|
38
|
+
pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap)
|
|
39
|
+
return pen
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def draw(painter: QtGui.QPainter, size: int) -> None:
|
|
43
|
+
center = QtCore.QPointF(size / 2, size / 2)
|
|
44
|
+
small = size <= 64
|
|
45
|
+
tiny = size <= 16
|
|
46
|
+
|
|
47
|
+
tile = QtCore.QRectF(size * 0.06, size * 0.06, size * 0.88, size * 0.88)
|
|
48
|
+
gradient = QtGui.QLinearGradient(0, tile.top(), 0, tile.bottom())
|
|
49
|
+
gradient.setColorAt(0.0, GRAPHITE)
|
|
50
|
+
gradient.setColorAt(1.0, GRAPHITE_EDGE)
|
|
51
|
+
painter.setPen(QtCore.Qt.PenStyle.NoPen)
|
|
52
|
+
painter.setBrush(gradient)
|
|
53
|
+
painter.drawRoundedRect(tile, size * 0.20, size * 0.20)
|
|
54
|
+
|
|
55
|
+
ring_r = size * 0.315
|
|
56
|
+
painter.setPen(_pen(AMBER, size * (0.055 if small else 0.036)))
|
|
57
|
+
painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
|
|
58
|
+
painter.drawEllipse(center, ring_r, ring_r)
|
|
59
|
+
|
|
60
|
+
if not tiny:
|
|
61
|
+
# aperture iris: six pinwheel blades from the rim to the opening
|
|
62
|
+
outer_r = ring_r * 0.92
|
|
63
|
+
open_r = ring_r * 0.54
|
|
64
|
+
painter.setPen(_pen(AMBER, size * (0.045 if small else 0.028)))
|
|
65
|
+
twist = math.radians(62.0)
|
|
66
|
+
for i in range(6):
|
|
67
|
+
start = math.tau * i / 6
|
|
68
|
+
painter.drawLine(
|
|
69
|
+
QtCore.QPointF(
|
|
70
|
+
center.x() + outer_r * math.cos(start),
|
|
71
|
+
center.y() + outer_r * math.sin(start),
|
|
72
|
+
),
|
|
73
|
+
QtCore.QPointF(
|
|
74
|
+
center.x() + open_r * math.cos(start + twist),
|
|
75
|
+
center.y() + open_r * math.sin(start + twist),
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# clock hands at 10:08; the aperture opening is the dial
|
|
80
|
+
hands = ((-60.0, 0.26, 0.046), (35.0, 0.40, 0.032))
|
|
81
|
+
if tiny:
|
|
82
|
+
hands = ((-60.0, 0.42, 0.085), (35.0, 0.62, 0.065))
|
|
83
|
+
elif small:
|
|
84
|
+
hands = ((-60.0, 0.26, 0.060), (35.0, 0.40, 0.048))
|
|
85
|
+
for angle_deg, length, width in hands:
|
|
86
|
+
angle = math.radians(angle_deg - 90.0)
|
|
87
|
+
painter.setPen(_pen(HANDS, size * width))
|
|
88
|
+
painter.drawLine(
|
|
89
|
+
center,
|
|
90
|
+
QtCore.QPointF(
|
|
91
|
+
center.x() + ring_r * length * math.cos(angle),
|
|
92
|
+
center.y() + ring_r * length * math.sin(angle),
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if not small:
|
|
97
|
+
painter.setPen(QtCore.Qt.PenStyle.NoPen)
|
|
98
|
+
painter.setBrush(AMBER)
|
|
99
|
+
painter.drawEllipse(center, size * 0.024, size * 0.024)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def render(size: int) -> QtGui.QImage:
|
|
103
|
+
image = QtGui.QImage(size, size, QtGui.QImage.Format.Format_ARGB32)
|
|
104
|
+
image.fill(QtCore.Qt.GlobalColor.transparent)
|
|
105
|
+
painter = QtGui.QPainter(image)
|
|
106
|
+
painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
|
|
107
|
+
draw(painter, size)
|
|
108
|
+
painter.end()
|
|
109
|
+
return image
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main() -> None:
|
|
113
|
+
QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
|
|
114
|
+
assets = Path(__file__).resolve().parent
|
|
115
|
+
package = assets.parent / "src" / "chronocatalog_desktop"
|
|
116
|
+
|
|
117
|
+
resources = package / "resources"
|
|
118
|
+
resources.mkdir(exist_ok=True)
|
|
119
|
+
render(256).save(str(resources / "icon.png"))
|
|
120
|
+
|
|
121
|
+
iconset = assets / "icon.iconset"
|
|
122
|
+
if iconset.exists():
|
|
123
|
+
shutil.rmtree(iconset)
|
|
124
|
+
iconset.mkdir()
|
|
125
|
+
for size in ICONSET_SIZES:
|
|
126
|
+
image = render(size)
|
|
127
|
+
if size < 1024:
|
|
128
|
+
image.save(str(iconset / f"icon_{size}x{size}.png"))
|
|
129
|
+
if size > 16:
|
|
130
|
+
image.save(str(iconset / f"icon_{size // 2}x{size // 2}@2x.png"))
|
|
131
|
+
|
|
132
|
+
if sys.platform == "darwin" and shutil.which("iconutil"):
|
|
133
|
+
subprocess.run(
|
|
134
|
+
["iconutil", "-c", "icns", str(iconset), "-o", str(assets / "icon.icns")],
|
|
135
|
+
check=True,
|
|
136
|
+
)
|
|
137
|
+
shutil.rmtree(iconset)
|
|
138
|
+
print(f"wrote {resources / 'icon.png'} and {assets / 'icon.icns'}")
|
|
139
|
+
else:
|
|
140
|
+
print(f"wrote {resources / 'icon.png'} and {iconset}/ (no iconutil; icns skipped)")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
main()
|