Compare commits
446 Commits
Alpha-v9.1
...
v9.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50fc325d80 | ||
|
|
140645943c | ||
|
|
2fc0dd03aa | ||
|
|
90b9af48e3 | ||
|
|
b2dbc5722b | ||
|
|
6490cc905d | ||
|
|
dfa4079b23 | ||
|
|
6ff7303321 | ||
|
|
4d405b5d77 | ||
|
|
bf8816f715 | ||
|
|
8c9b04d1ec | ||
|
|
5995e4d416 | ||
|
|
fc714e02e6 | ||
|
|
85b6d9decc | ||
|
|
3e2fb1282c | ||
|
|
9a78bf3066 | ||
|
|
1c53f05e4f | ||
|
|
1e4883c577 | ||
|
|
3e950a0cbe | ||
|
|
ca08ce57b6 | ||
|
|
7f3b3d06af | ||
|
|
3d427997ea | ||
|
|
65237ed106 | ||
|
|
cb12956309 | ||
|
|
cc2e9f97c4 | ||
|
|
81ddf5366c | ||
|
|
8219ffc416 | ||
|
|
85d62e6519 | ||
|
|
341aa92ecb | ||
|
|
dcb8ded987 | ||
|
|
569390ea72 | ||
|
|
4590226bca | ||
|
|
d8ef54392b | ||
|
|
41087b14c6 | ||
|
|
38626ac690 | ||
|
|
bc38e568fa | ||
|
|
4b35df0924 | ||
|
|
750cbf0f9c | ||
|
|
5ac40f5b11 | ||
|
|
fda6077b20 | ||
|
|
cb4798b715 | ||
|
|
c51f9869b2 | ||
|
|
e1b1ef9888 | ||
|
|
3fcf6022b9 | ||
|
|
938832505b | ||
|
|
b107fb5809 | ||
|
|
ec960f2372 | ||
|
|
30b60a0d31 | ||
|
|
ce87b11fbd | ||
|
|
e463635cc0 | ||
|
|
aa0aad4300 | ||
|
|
bebca634de | ||
|
|
92a6e1130c | ||
|
|
c79086f715 | ||
|
|
9ce07bd369 | ||
|
|
33ee27a84f | ||
|
|
883354b263 | ||
|
|
6862f89d1a | ||
|
|
1204d2b7b5 | ||
|
|
15ee13c7b4 | ||
|
|
49ad8117ef | ||
|
|
4f193613de | ||
|
|
31038711f2 | ||
|
|
cc827108ef | ||
|
|
9fec4822c1 | ||
|
|
501ab1f977 | ||
|
|
b3c01e180a | ||
|
|
4c6ebec529 | ||
|
|
5c25666e67 | ||
|
|
fae65bd9e9 | ||
|
|
8e065ca8ac | ||
|
|
888b674f05 | ||
|
|
82946cb0b8 | ||
|
|
aa2925cde0 | ||
|
|
e5a0e5aa9b | ||
|
|
5bc80a043f | ||
|
|
65d88b9987 | ||
|
|
37ff35fcf6 | ||
|
|
9b13e338bb | ||
|
|
a47b0adb6e | ||
|
|
9f39bf6fdc | ||
|
|
e375166bfe | ||
|
|
7054ffd227 | ||
|
|
6d283d1f2d | ||
|
|
a0baf015db | ||
|
|
58be4cdb4b | ||
|
|
08761d5f8a | ||
|
|
6a680ad3d1 | ||
|
|
b5ec3598e1 | ||
|
|
926dfffebe | ||
|
|
461906c349 | ||
|
|
2dc5197fbd | ||
|
|
11f0c7f9b8 | ||
|
|
fb445e6ab0 | ||
|
|
6e96a0ff61 | ||
|
|
c75aff4db3 | ||
|
|
84a4b2f0cf | ||
|
|
10b90dcc74 | ||
|
|
2d89df620e | ||
|
|
0646508c24 | ||
|
|
0137ed5be8 | ||
|
|
779a251c09 | ||
|
|
868b553670 | ||
|
|
9f630fe315 | ||
|
|
6798ffd0a7 | ||
|
|
2e8678414b | ||
|
|
e1cd46d010 | ||
|
|
9879697c95 | ||
|
|
57e27bb51f | ||
|
|
66ec0913b6 | ||
|
|
6357fea8db | ||
|
|
491ebb6714 | ||
|
|
385b4117db | ||
|
|
be3992f655 | ||
|
|
18becd62a3 | ||
|
|
699ecd367c | ||
|
|
9d7609a8e5 | ||
|
|
e94c4871d7 | ||
|
|
02bf15e080 | ||
|
|
5f60ec1702 | ||
|
|
cdf2581f84 | ||
|
|
af8b4e3872 | ||
|
|
ac9dd5879e | ||
|
|
badcd72bea | ||
|
|
8733c8d301 | ||
|
|
4726f1fc63 | ||
|
|
1461f2ee70 | ||
|
|
1bfc24b70f | ||
|
|
c09f50c568 | ||
|
|
66aecf2030 | ||
|
|
dc188264f9 | ||
|
|
6e56f13eda | ||
|
|
c9ea25b940 | ||
|
|
e814d09c60 | ||
|
|
69115ed9bb | ||
|
|
296aed6575 | ||
|
|
e655fd091d | ||
|
|
8780063e22 | ||
|
|
c6d2a89263 | ||
|
|
ecea6effa4 | ||
|
|
8e11e28561 | ||
|
|
5d85417ce4 | ||
|
|
4b1119ecba | ||
|
|
5d21375e65 | ||
|
|
f60a93f35b | ||
|
|
a71ed7c426 | ||
|
|
06f528f8b5 | ||
|
|
94a0b00007 | ||
|
|
eba7c3e178 | ||
|
|
89b1921e56 | ||
|
|
f35d9c1313 | ||
|
|
6a2199dd2e | ||
|
|
0416fde7f5 | ||
|
|
02d6b22b25 | ||
|
|
851d1fb3b2 | ||
|
|
4616da4e5f | ||
|
|
d43b00bd00 | ||
|
|
e7318c7473 | ||
|
|
74e90df680 | ||
|
|
6566682504 | ||
|
|
b00dbf9548 | ||
|
|
f8d44c5fae | ||
|
|
88b0f70271 | ||
|
|
93526fa73f | ||
|
|
f69f173368 | ||
|
|
92752aef4a | ||
|
|
ad850cba94 | ||
|
|
191d8f995f | ||
|
|
eede5b3600 | ||
|
|
3aa71d6f8a | ||
|
|
94f929d122 | ||
|
|
03a46ae57b | ||
|
|
b8d72a65c8 | ||
|
|
8321f43d6e | ||
|
|
f9ea20e29c | ||
|
|
b6ccb88a95 | ||
|
|
de434872d6 | ||
|
|
7acfecf7b9 | ||
|
|
7f776f4c86 | ||
|
|
e803f6adcb | ||
|
|
b6848bb81f | ||
|
|
7ef2aa6a80 | ||
|
|
48ad4aaad2 | ||
|
|
0e621011fc | ||
|
|
fb7c73d96b | ||
|
|
57a15f651e | ||
|
|
3aa4fa214f | ||
|
|
ec55e92599 | ||
|
|
c27f99e3a4 | ||
|
|
78ea0b3641 | ||
|
|
523f233f4a | ||
|
|
ea05907227 | ||
|
|
089c8dd50c | ||
|
|
77d7559014 | ||
|
|
164aba1bb0 | ||
|
|
fd622ea378 | ||
|
|
b2d87b05d8 | ||
|
|
ac3d7c95cb | ||
|
|
64514db457 | ||
|
|
58c773a0de | ||
|
|
bb1161baa9 | ||
|
|
cd719c6d01 | ||
|
|
9a405dd336 | ||
|
|
afd6e113d2 | ||
|
|
ea8d954548 | ||
|
|
a1fcd23d13 | ||
|
|
99a0bfe919 | ||
|
|
eb5124bd0a | ||
|
|
0e1e9e924b | ||
|
|
85b4be8525 | ||
|
|
08c0704926 | ||
|
|
81f550a543 | ||
|
|
aedc5afefa | ||
|
|
9951c00a45 | ||
|
|
b834166d61 | ||
|
|
61f9a49782 | ||
|
|
f71b947673 | ||
|
|
3d5ed3a948 | ||
|
|
8604a7f08f | ||
|
|
67c18e9a25 | ||
|
|
81e0e7b072 | ||
|
|
ba5a995b5d | ||
|
|
c58590a221 | ||
|
|
f40be005f8 | ||
|
|
f9fc28d5ec | ||
|
|
1030328420 | ||
|
|
1a7316d54c | ||
|
|
92e7e05540 | ||
|
|
d478116094 | ||
|
|
a10478bc8b | ||
|
|
a669fd67c3 | ||
|
|
7cdb26e822 | ||
|
|
16a9d32ef3 | ||
|
|
9b90bfad84 | ||
|
|
6f900ff371 | ||
|
|
d02f068991 | ||
|
|
878a5dfc8a | ||
|
|
030c817323 | ||
|
|
9ad81c4582 | ||
|
|
b1c0ec5817 | ||
|
|
ea904da58b | ||
|
|
0c9dd7f43b | ||
|
|
885b2b0358 | ||
|
|
7139eea4c4 | ||
|
|
41e419c1e7 | ||
|
|
93177dd696 | ||
|
|
d8a11a796d | ||
|
|
c9c399faa9 | ||
|
|
44106e2c59 | ||
|
|
260583cfeb | ||
|
|
eb2a175b75 | ||
|
|
89b159cfa1 | ||
|
|
fb300d0d07 | ||
|
|
3446e0601a | ||
|
|
22e0ef8f4b | ||
|
|
1351002378 | ||
|
|
6e8baced7d | ||
|
|
310fc0d958 | ||
|
|
737be6199f | ||
|
|
ca9735ca86 | ||
|
|
4a9c4de56a | ||
|
|
f7c4e1ccc0 | ||
|
|
f6b131c412 | ||
|
|
a695e222f4 | ||
|
|
0d6746abe0 | ||
|
|
2d558efde7 | ||
|
|
82a53481f7 | ||
|
|
d26b308ae2 | ||
|
|
5431c1996a | ||
|
|
731b6d1568 | ||
|
|
68b075a6c8 | ||
|
|
99cc8dc991 | ||
|
|
cdba60c7d3 | ||
|
|
0495824878 | ||
|
|
6b9b813e26 | ||
|
|
ad85266f98 | ||
|
|
a62dbc3f2d | ||
|
|
237bcd90b0 | ||
|
|
abd36e55b3 | ||
|
|
7b0064d14b | ||
|
|
c848c57cb4 | ||
|
|
ebf2c0bb99 | ||
|
|
53fefb7497 | ||
|
|
a9afb8e2cf | ||
|
|
f0019c7086 | ||
|
|
c8439c976f | ||
|
|
4a3b0c103f | ||
|
|
6168434f3d | ||
|
|
9b7b384183 | ||
|
|
5f6752e067 | ||
|
|
e71ef7f671 | ||
|
|
a1fe57a352 | ||
|
|
0364d3a95c | ||
|
|
63f8268fd4 | ||
|
|
e29b2838c6 | ||
|
|
7caba3b825 | ||
|
|
74c4c6c85d | ||
|
|
abc9f3f4e4 | ||
|
|
f26fd5a63c | ||
|
|
bb6a73e410 | ||
|
|
54b70676f3 | ||
|
|
91c918796d | ||
|
|
fb05d386c1 | ||
|
|
4665fb22ff | ||
|
|
b25c1efc9d | ||
|
|
f160071b27 | ||
|
|
73ba643132 | ||
|
|
effdd28b80 | ||
|
|
875bc34519 | ||
|
|
77bbbe95b3 | ||
|
|
5f31fe4623 | ||
|
|
2f61ac706f | ||
|
|
822aa4df39 | ||
|
|
d67f8b6f0e | ||
|
|
60d1d290a2 | ||
|
|
65a2fe676d | ||
|
|
30998a144a | ||
|
|
9c44d74cb6 | ||
|
|
4de7865aba | ||
|
|
8fed3350f4 | ||
|
|
02542bda7e | ||
|
|
36841da965 | ||
|
|
58b0e94d65 | ||
|
|
4b7b915c68 | ||
|
|
6091916b86 | ||
|
|
273843f9c1 | ||
|
|
da1c3f8fa8 | ||
|
|
aec0786e61 | ||
|
|
9959eb1049 | ||
|
|
4f00a8ac88 | ||
|
|
329c23e23c | ||
|
|
f53a9249f0 | ||
|
|
6097570591 | ||
|
|
8541fc59d1 | ||
|
|
c955fb1589 | ||
|
|
276cfaf635 | ||
|
|
6bba7dafbc | ||
|
|
2110592971 | ||
|
|
15de97cfe7 | ||
|
|
114c1a4481 | ||
|
|
1f9a13fc4d | ||
|
|
e2aba7ddf7 | ||
|
|
f0f8e0ea7e | ||
|
|
d5d9cd3e7a | ||
|
|
ade1fb1c11 | ||
|
|
955d4a0c9f | ||
|
|
31f4022895 | ||
|
|
5b4d35b5c0 | ||
|
|
6831a89392 | ||
|
|
5bd2aaaf9e | ||
|
|
c789d09b07 | ||
|
|
a9cbab40ab | ||
|
|
bcf4453c8d | ||
|
|
18dcedd6a0 | ||
|
|
79e0263e97 | ||
|
|
960b2038ef | ||
|
|
88292f42af | ||
|
|
3cd6fa136f | ||
|
|
0541c9fb01 | ||
|
|
039c574cf4 | ||
|
|
042ed9209f | ||
|
|
992a840dfe | ||
|
|
1e17b6c467 | ||
|
|
a0b017815e | ||
|
|
80d6e09834 | ||
|
|
66fec73136 | ||
|
|
1f7a5d3cbb | ||
|
|
dde7eec946 | ||
|
|
85ae167817 | ||
|
|
5feb3a6d20 | ||
|
|
f23ff1669e | ||
|
|
6b1035b0f6 | ||
|
|
243d786298 | ||
|
|
898ce5fdc7 | ||
|
|
749d7c8fc0 | ||
|
|
1774a00d34 | ||
|
|
9020aaddf4 | ||
|
|
00651e6242 | ||
|
|
b7638046a3 | ||
|
|
c446911b59 | ||
|
|
201a63e273 | ||
|
|
d8faa27dbf | ||
|
|
ff488da6c8 | ||
|
|
2514809e17 | ||
|
|
82bb63191a | ||
|
|
9a076a775f | ||
|
|
5c746a9950 | ||
|
|
00dea27279 | ||
|
|
a0dc34a5ad | ||
|
|
2a46251831 | ||
|
|
92834800e2 | ||
|
|
794401ae58 | ||
|
|
b2fbc4b4a2 | ||
|
|
13c2ca1ea5 | ||
|
|
e823bb5c93 | ||
|
|
7652289fe5 | ||
|
|
b9b23611f7 | ||
|
|
de09da1592 | ||
|
|
956ffd4663 | ||
|
|
8b4b2507fa | ||
|
|
f125e5a50d | ||
|
|
0b1c097f97 | ||
|
|
2b5697ea50 | ||
|
|
4e5b7b1c7d | ||
|
|
0b4ccac5ff | ||
|
|
952ed8f27d | ||
|
|
c0c18dabc1 | ||
|
|
7b48e5e17e | ||
|
|
6e7567a192 | ||
|
|
ad6fefbe2b | ||
|
|
4a55af66ee | ||
|
|
f0148c7f35 | ||
|
|
ac00890c90 | ||
|
|
9e61d45ea5 | ||
|
|
432f4851f3 | ||
|
|
4184848f9c | ||
|
|
bc0f8b991e | ||
|
|
c4e8d40abe | ||
|
|
c7492b78d3 | ||
|
|
e69e4998e5 | ||
|
|
0b9b4dac73 | ||
|
|
844f756dba | ||
|
|
6e341e097b | ||
|
|
def244b732 | ||
|
|
129d336357 | ||
|
|
071100bf90 | ||
|
|
e161290571 | ||
|
|
e8ab8059cd | ||
|
|
7ec29228b5 | ||
|
|
3974c1b031 | ||
|
|
fba2f8f46b | ||
|
|
0529f1fe7e | ||
|
|
59be7c0cf9 | ||
|
|
dfe2b57952 | ||
|
|
45e765862d | ||
|
|
cf1ce6bb24 | ||
|
|
e3e1c55fab | ||
|
|
2e57e0397c | ||
|
|
5e33d07e39 | ||
|
|
fffd9dba6c | ||
|
|
98a01aea80 | ||
|
|
1489d56be7 | ||
|
|
d974aa777c | ||
|
|
f67b26fbd6 | ||
|
|
a074bed540 | ||
|
|
a55e649d83 |
16
.envrc.recommended
Normal file
@@ -0,0 +1,16 @@
|
||||
# vi: ft=bash
|
||||
#
|
||||
# If you wish to use this file, copy or symlink it to `.envrc` for direnv to read it.
|
||||
# This will use the flake development shell.
|
||||
|
||||
if ! has nix_direnv_version || ! nix_direnv_version 3.0.5; then
|
||||
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.5/direnvrc" "sha256-RuwIS+QKFj/T9M2TFXScjBsLR6V3A17YVoEW/Q6AZ1w="
|
||||
fi
|
||||
|
||||
devenv_root_file="$(mktemp -t devenv-root-XXXXXXXX)"
|
||||
printf %s "${PWD}" >"${devenv_root_file}"
|
||||
if ! use flake . --override-input devenv-root "file+file://${devenv_root_file}"; then
|
||||
printf '%s\n' "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2
|
||||
fi
|
||||
rm "${devenv_root_file}"
|
||||
unset devenv_root_file
|
||||
64
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Bug Report
|
||||
description: File a bug or issue report.
|
||||
title: '[Bug]: '
|
||||
labels: ['Type: Bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
*Please add an appropriate title for this issue.*
|
||||
|
||||
Before reporting, read the [documentation](https://github.com/TagStudioDev/TagStudio/blob/main/doc/index.md) and search existing [issues](https://github.com/TagStudioDev/TagStudio/issues).
|
||||
Validate that you are using an up-to-date version[^1], your issue might already be fixed!
|
||||
Questions, guidance, and usage goes in [discussions](https://github.com/TagStudioDev/TagStudio/discussions). Invalid issues will be closed.
|
||||
|
||||
[^1]: This can mean latest release, pre-release, or git commit.
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Make sure you've checked (and actually did) all of the below.
|
||||
options:
|
||||
- label: I am using an up-to-date version.
|
||||
required: true
|
||||
- label: I have read the [documentation](https://github.com/TagStudioDev/TagStudio/blob/main/doc/index.md).
|
||||
required: true
|
||||
- label: I have searched existing [issues](https://github.com/TagStudioDev/TagStudio/issues).
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: TagStudio Version
|
||||
placeholder: Alpha 9.1.0
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Operating System & Version
|
||||
placeholder: TagOS 3.14
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: Clear and concise description of the problem. Attach screenshots if needed, and include any errors you see.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: Clear and concise description of what you think should happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Minimal steps neded for the problem to occur.
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Logs
|
||||
description: Provide any logs, if applicable.
|
||||
40
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature.
|
||||
title: '[Feature Request]: '
|
||||
labels: ['Type: Enhancement']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
*Please add an appropriate title for this feature request.*
|
||||
|
||||
Before suggesting, read the [documentation](https://github.com/TagStudioDev/TagStudio/blob/main/doc/index.md) and search existing [issues](https://github.com/TagStudioDev/TagStudio/issues).
|
||||
Validate that you are using an up-to-date version[^1], your feature might already be implemented!
|
||||
Questions, guidance, and usage goes in [discussions](https://github.com/TagStudioDev/TagStudio/discussions). Invalid issues will be closed.
|
||||
|
||||
[^1]: This can mean latest release, pre-release, or git commit.
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Make sure you've checked (and actually did) all of the below.
|
||||
options:
|
||||
- label: I am using an up-to-date version.
|
||||
required: true
|
||||
- label: I have read the [documentation](https://github.com/TagStudioDev/TagStudio/blob/main/doc/index.md).
|
||||
required: true
|
||||
- label: I have searched existing [issues](https://github.com/TagStudioDev/TagStudio/issues).
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: Clear and concise description of the problem or missing capability. Attach screenshots if needed, and explain your own use case.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Solution
|
||||
description: Clear and concise description of what you think should happen.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Alternatives
|
||||
description: Any considered alternative solutions or workarounds. If undesirable, why?
|
||||
52
.github/workflows/apprun.yaml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: PySide App Test
|
||||
|
||||
on: [ push, pull_request ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
# dont run update, it is slow
|
||||
# sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libxkbcommon-x11-0 \
|
||||
x11-utils \
|
||||
libyaml-dev \
|
||||
libegl1-mesa \
|
||||
libxcb-icccm4 \
|
||||
libxcb-image0 \
|
||||
libxcb-keysyms1 \
|
||||
libxcb-randr0 \
|
||||
libxcb-render-util0 \
|
||||
libxcb-xinerama0 \
|
||||
libopengl0 \
|
||||
libxcb-cursor0 \
|
||||
libpulse0 \
|
||||
ffmpeg
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -Ur requirements.txt
|
||||
|
||||
- name: Run TagStudio app and check exit code
|
||||
run: |
|
||||
xvfb-run --server-args="-screen 0, 1920x1200x24 -ac +extension GLX +render -noreset" python tagstudio/tag_studio.py --ci -o /tmp/
|
||||
exit_code=$?
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo "TagStudio ran successfully"
|
||||
else
|
||||
echo "TagStudio failed with exit code $exit_code"
|
||||
exit 1
|
||||
fi
|
||||
37
.github/workflows/mypy.yaml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: MyPy
|
||||
|
||||
on: [ push, pull_request ]
|
||||
|
||||
|
||||
jobs:
|
||||
mypy:
|
||||
name: Run MyPy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: reviewdog/action-setup@v1
|
||||
with:
|
||||
reviewdog_version: latest
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install mypy==1.10.0
|
||||
mkdir tagstudio/.mypy_cache
|
||||
|
||||
- uses: tsuyoshicho/action-mypy@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
reporter: github-check
|
||||
fail_on_error: true
|
||||
workdir: tagstudio
|
||||
level: error
|
||||
mypy_flags: --config-file ../pyproject.toml
|
||||
22
.github/workflows/pytest.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: pytest
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
pytest:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
pytest tagstudio/tests/
|
||||
106
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v[0-9]+.[0-9]+.[0-9]+*
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
strategy:
|
||||
matrix:
|
||||
build-type: ['', portable]
|
||||
include:
|
||||
- build-type: ''
|
||||
build-flag: ''
|
||||
suffix: ''
|
||||
- build-type: portable
|
||||
build-flag: --portable
|
||||
suffix: _portable
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
- run: pip install -Ur requirements.txt pyinstaller
|
||||
- run: pyinstaller tagstudio.spec -- ${{ matrix.build-flag }}
|
||||
- run: tar czfC dist/tagstudio_linux_x86_64${{ matrix.suffix }}.tar.gz dist tagstudio
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tagstudio_linux_x86_64${{ matrix.suffix }}
|
||||
path: dist/tagstudio_linux_x86_64${{ matrix.suffix }}.tar.gz
|
||||
|
||||
macos:
|
||||
strategy:
|
||||
matrix:
|
||||
os-version: ['12', '14']
|
||||
include:
|
||||
- os-version: '12'
|
||||
arch: x86_64
|
||||
- os-version: '14'
|
||||
arch: aarch64
|
||||
runs-on: macos-${{ matrix.os-version }}
|
||||
env:
|
||||
# even though we run on 12, target towards compatibility
|
||||
MACOSX_DEPLOYMENT_TARGET: '11.0'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
- run: pip install -Ur requirements.txt pyinstaller
|
||||
- run: pyinstaller tagstudio.spec
|
||||
- run: tar czfC dist/tagstudio_macos_${{ matrix.arch }}.tar.gz dist TagStudio.app
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tagstudio_macos_${{ matrix.arch }}
|
||||
path: dist/tagstudio_macos_${{ matrix.arch }}.tar.gz
|
||||
|
||||
windows:
|
||||
strategy:
|
||||
matrix:
|
||||
build-type: ['', portable]
|
||||
include:
|
||||
- build-type: ''
|
||||
build-flag: ''
|
||||
suffix: ''
|
||||
file-end: ''
|
||||
- build-type: portable
|
||||
build-flag: --portable
|
||||
suffix: _portable
|
||||
file-end: .exe
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
- run: pip install -Ur requirements.txt pyinstaller
|
||||
- run: PyInstaller tagstudio.spec -- ${{ matrix.build-flag }}
|
||||
- run: Compress-Archive -Path dist/TagStudio${{ matrix.file-end }} -DestinationPath dist/tagstudio_windows_x86_64${{ matrix.suffix }}.zip
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tagstudio_windows_x86_64${{ matrix.suffix }}
|
||||
path: dist/tagstudio_windows_x86_64${{ matrix.suffix }}.zip
|
||||
|
||||
publish:
|
||||
needs: [linux, macos, windows]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
tagstudio_linux_x86_64/*
|
||||
tagstudio_linux_x86_64_portable/*
|
||||
tagstudio_macos_x86_64/*
|
||||
tagstudio_macos_aarch64/*
|
||||
tagstudio_windows_x86_64/*
|
||||
tagstudio_windows_x86_64_portable/*
|
||||
11
.github/workflows/ruff.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
name: Ruff
|
||||
on: [ push, pull_request ]
|
||||
jobs:
|
||||
ruff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: chartboost/ruff-action@v1
|
||||
with:
|
||||
version: 0.4.2
|
||||
args: 'format --check'
|
||||
8
.gitignore
vendored
@@ -35,6 +35,7 @@ MANIFEST
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
!tagstudio.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
@@ -54,6 +55,7 @@ coverage.xml
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
tagstudio/tests/fixtures/library/*
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
@@ -249,5 +251,9 @@ compile_commands.json
|
||||
|
||||
# TagStudio
|
||||
.TagStudio
|
||||
|
||||
TagStudio.ini
|
||||
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python,qt
|
||||
|
||||
.envrc
|
||||
.direnv
|
||||
.devenv
|
||||
|
||||
6
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.4.2
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
2
.vscode/launch.json
vendored
@@ -8,7 +8,7 @@
|
||||
"name": "TagStudio",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}\\TagStudio\\tagstudio.py",
|
||||
"program": "${workspaceRoot}/tagstudio/tag_studio.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true,
|
||||
"args": []
|
||||
|
||||
69
Build_MacOS_app.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#! /usr/bin/env bash
|
||||
# GETTING BASE DIR
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
# SETTING UP CONSTANTS
|
||||
TAGSTUDIO_NAME="TagStudio"
|
||||
TAGSTUDIO_DIR="$SCRIPT_DIR/tagstudio"
|
||||
TAGSTUDIO_DIR_RESOURCES="$TAGSTUDIO_DIR/resources"
|
||||
TAGSTUDIO_ICON="$TAGSTUDIO_DIR/resources/icon.ico"
|
||||
TAGSTUDIO_SRC="$TAGSTUDIO_DIR/src"
|
||||
TAGSTUDIO_MAIN="$TAGSTUDIO_DIR/tag_studio.py"
|
||||
DIST_PATH="$SCRIPT_DIR/dist"
|
||||
BUILD_PATH="$SCRIPT_DIR/build"
|
||||
LOGS_PATH="$BUILD_PATH/logs"
|
||||
|
||||
printf -- "🏁 Starting Script \n"
|
||||
|
||||
# CREATE VENV AND INSTALL REQUIREMENTS
|
||||
printf -- "🐍 Creating Python virtual env\n"
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
if [ ! -d $LOGS_PATH ]; then
|
||||
printf -- "📁 Creating Logs folder\n"
|
||||
mkdir -p $LOGS_PATH;
|
||||
fi
|
||||
|
||||
printf -- "💻 Installing Requirements \n"
|
||||
pip install -r requirements.txt > "$LOGS_PATH/pip.log" 2>&1
|
||||
pip install PyInstaller > "$LOGS_PATH/pip.log" 2>&1
|
||||
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
printf -- "🍏 MacOS Detected \n"
|
||||
SYS_CMD="--windowed"
|
||||
OS=0
|
||||
fi
|
||||
|
||||
SECONDS=0
|
||||
|
||||
# CREATE COMMAND
|
||||
printf -- "⏳ Building App \n"
|
||||
|
||||
COMMAND=$( python -m PyInstaller \
|
||||
--name "$TAGSTUDIO_NAME" \
|
||||
--icon "$TAGSTUDIO_ICON" \
|
||||
--add-data "$TAGSTUDIO_DIR_RESOURCES:./resources" \
|
||||
--add-data "$TAGSTUDIO_SRC:./src" \
|
||||
--distpath "$DIST_PATH" \
|
||||
-p "$TAGSTUDIO_DIR" \
|
||||
--noconsole \
|
||||
--workpath "$BUILD_PATH" \
|
||||
-y "$SYS_CMD" "$TAGSTUDIO_MAIN" \
|
||||
> "$LOGS_PATH/pyinstaller.log" 2>&1 )
|
||||
|
||||
duration=$SECONDS
|
||||
|
||||
if $COMMAND; then
|
||||
printf -- "✅ Build Successfull \n"
|
||||
printf -- "⌛ $((duration)) seconds of build\n"
|
||||
if [[ "$OS" == 0 ]]; then
|
||||
printf -- "📁 Opening App folder \n"
|
||||
open $DIST_PATH
|
||||
fi
|
||||
else
|
||||
printf -- "❌ Error Building the app\nPlease read the logs\navailable at build/logs\n"
|
||||
fi
|
||||
|
||||
printf -- "🏁 END OF TRANSMISSION"
|
||||
31
Build_win.bat
Normal file
@@ -0,0 +1,31 @@
|
||||
@echo off
|
||||
set TAGSTUDIO_NAME=TagStudio
|
||||
set TAGSTUDIO_DIR=tagstudio
|
||||
set TAGSTUDIO_DIR_RESOURCES=%TAGSTUDIO_DIR%/resources
|
||||
set TAGSTUDIO_ICON=%TAGSTUDIO_DIR%/resources/icon.ico
|
||||
set TAGSTUDIO_SRC=%TAGSTUDIO_DIR%/src
|
||||
set TAGSTUDIO_MAIN=%TAGSTUDIO_DIR%/tag_studio.py
|
||||
set BUILD_MODE=--onedir
|
||||
|
||||
|
||||
if "%1" == "--help" (
|
||||
echo run "%~nx0" for normal Build
|
||||
echo run "%~nx0 --portable" for Build packaged into one file
|
||||
goto end
|
||||
)
|
||||
if "%1" == "--portable" (
|
||||
echo Building portable executable...
|
||||
set BUILD_MODE=--onefile
|
||||
goto run
|
||||
)
|
||||
if not "%1" == "" (
|
||||
echo Invalid argument run "%~nx0 --help" for help
|
||||
goto end
|
||||
)
|
||||
:run
|
||||
echo Building executable...
|
||||
set COMMAND=PyInstaller --name "%TAGSTUDIO_NAME%" --icon "%TAGSTUDIO_ICON%" --add-data "%TAGSTUDIO_DIR_RESOURCES%:./resources" --add-data "%TAGSTUDIO_SRC%:./src" -p "%TAGSTUDIO_DIR%" --console %BUILD_MODE% "%TAGSTUDIO_MAIN%" -y
|
||||
call .venv\Scripts\activate.bat
|
||||
%COMMAND%
|
||||
deactivate
|
||||
:end
|
||||
51
CHANGELOG.md
@@ -5,7 +5,56 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [9.1.0-alpha] - 2024-04-22
|
||||
## [9.2.0] - 2024-05-14
|
||||
|
||||
### Added
|
||||
|
||||
- Full macOS and Linux support
|
||||
- Ability to apply tags to multiple selections at once
|
||||
- Right-click context menu for opening files or their locations
|
||||
- Support for all filetypes inside of the library
|
||||
- Configurable filetype blacklist
|
||||
- Option to automatically open last used library on startup
|
||||
- Tool to convert folder structure to tag tree
|
||||
- SIGTERM handling in console window
|
||||
- Keyboard shortcuts for basic functions
|
||||
- Basic support for plaintext thumbnails
|
||||
- Default icon for files with no thumbnail support
|
||||
- Menu action to close library
|
||||
- All tags now show in the "Add Tag" panel by default
|
||||
- Modal view to view and manage all library tags
|
||||
- Build scripts for Windows and macOS
|
||||
- Help menu option to visit the GitHub repository
|
||||
- Toggleable "Recent Libraries" list in the entry side panel
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed errors when performing actions with no library open
|
||||
- Fixed bug where built-in tags were duplicated upon saving
|
||||
- QThreads are now properly terminated on application exit
|
||||
- Images with rotational EXIF data are now properly displayed
|
||||
- Fixed "truncated" images causing errors
|
||||
- Fixed images with large resolutions causing errors
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated minimum Python version to 3.12
|
||||
- Various UI improvements
|
||||
- Improved legibility of the Light Theme (still a WIP)
|
||||
- Updated Dark Theme
|
||||
- Added hand cursor to several clickable elements
|
||||
- Fixed network paths not being able to load
|
||||
- Various code cleanup and refactoring
|
||||
- New application icons
|
||||
|
||||
### Known Issues
|
||||
- Using and editing multiple entry fields of the same type may result in incorrect field(s) being updated
|
||||
- Adding Favorite or Archived tags via the thumbnail badges may apply the tag(s) to incorrect fields
|
||||
- Searching for tag names with spaces does not currently function as intended
|
||||
- A temporary workaround it to omit spaces in tag names when searching
|
||||
- Sorting fields using the "Sort Fields" macro may result in edit icons being shown for incorrect fields
|
||||
|
||||
## [9.1.0] - 2024-04-22
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
170
CONTRIBUTING.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Contributing to TagStudio
|
||||
|
||||
_Last Updated: June 10th, 2024_
|
||||
|
||||
Thank you so much for showing interest in contributing to TagStudio! Here are a set of instructions and guidelines for contributing code or documentation to the project. This document will change over time, so make sure that your contributions still line up with the requirements here before submitting a pull request.
|
||||
|
||||
## Getting Started
|
||||
|
||||
- Check the [Planned Features](https://github.com/TagStudioDev/TagStudio/blob/main/doc/updates/planned_features.md) page, [FAQ](/README.md/#faq), as well as the open [Issues](https://github.com/TagStudioDev/TagStudio/issues) and [Pull Requests](https://github.com/TagStudioDev/TagStudio/pulls).
|
||||
- If you'd like to add a feature that isn't on the roadmap or doesn't have an open issue, **PLEASE create a feature request** issue for it discussing your intentions so any feedback or important information can be given by the team first.
|
||||
- We don't want you wasting time developing a feature or making a change that can't/won't be added for any reason ranging from pre-existing refactors to design philosophy differences.
|
||||
|
||||
### Contribution Checklist
|
||||
|
||||
- I've read the [Planned Features](https://github.com/TagStudioDev/TagStudio/blob/main/doc/updates/planned_features.md) page
|
||||
- I've read the [FAQ](/README.md/#faq), including the "[Features I Likely Won't Add/Pull](/README.md/#features-i-likely-wont-addpull)" section
|
||||
- I've checked the open [Issues](https://github.com/TagStudioDev/TagStudio/issues) and [Pull Requests](https://github.com/TagStudioDev/TagStudio/pulls)
|
||||
- **I've created a new issue for my feature _before_ starting work on it**, or have at least notified others in the relevant existing issue(s) of my intention to work on it
|
||||
- I've set up my development environment including Ruff and Mypy
|
||||
- I've read the [Code Guidelines](#code-guidelines) and/or [Documentation Guidelines](#documentation-guidelines)
|
||||
- **_I mean it, I've found or created a new issue for my feature!_**
|
||||
|
||||
## Creating a Development Environment
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Python](https://www.python.org/downloads/) 3.12
|
||||
- [Ruff](https://github.com/astral-sh/ruff) (Included in `requirements-dev.txt`)
|
||||
- [Mypy](https://github.com/python/mypy) (Included in `requirements-dev.txt`)
|
||||
- [PyTest](https://docs.pytest.org) (Included in `requirements-dev.txt`)
|
||||
|
||||
### Creating a Python Virtual Environment
|
||||
|
||||
If you wish to launch the source version of TagStudio outside of your IDE:
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Depending on your system, Python may be called `python`, `py`, `python3`, or `py3`. These instructions use the alias `python3` for consistency. You can check to see which alias your system uses and if it's for the correct Python version by typing `python3 --version` (or whichever alias) into your terminal.
|
||||
|
||||
> [!TIP]
|
||||
> On Linux and macOS, you can launch the `tagstudio.sh` script to skip the following process, minus the `requirements-dev.txt` installation step. _Using the script is fine if you just want to launch the program from source._
|
||||
|
||||
1. In the root repository directory, create a python virtual environment:
|
||||
`python3 -m venv .venv`
|
||||
2. Activate your environment:
|
||||
|
||||
- Windows w/Powershell: `.venv\Scripts\Activate.ps1`
|
||||
- Windows w/Command Prompt: `.venv\Scripts\activate.bat`
|
||||
- Linux/macOS: `source .venv/bin/activate`
|
||||
|
||||
3. Install the required packages:
|
||||
|
||||
- `pip install -r requirements.txt`
|
||||
- If developing (includes Ruff and Mypy): `pip install -r requirements-dev.txt`
|
||||
|
||||
_Learn more about setting up a virtual environment [here](https://docs.python.org/3/tutorial/venv.html)._
|
||||
|
||||
### Manually Launching (Outside of an IDE)
|
||||
|
||||
- **Windows** (start_win.bat)
|
||||
|
||||
- To launch TagStudio, launch the `start_win.bat` file. You can modify this .bat file or create a shortcut and add one or more additional arguments if desired.
|
||||
|
||||
- **Linux/macOS** (TagStudio.sh)
|
||||
|
||||
- Run the "TagStudio.sh" script and the program should launch! (Make sure that the script is marked as executable if on Linux). Note that launching from the script from outside of a terminal will not launch a terminal window with any debug or crash information. If you wish to see this information, just launch the shell script directly from your terminal with `./TagStudio.sh`.
|
||||
|
||||
- **NixOS** (Nix Flake)
|
||||
> [!WARNING]
|
||||
> Support for NixOS is still a work in progress.
|
||||
- Use the provided [Flake](https://nixos.wiki/wiki/Flakes) to create and enter a working environment by running `nix develop`. Then, run the program via `python3 tagstudio/tag_studio.py` from the root directory.
|
||||
|
||||
- **Any** (No Scripts)
|
||||
|
||||
- Alternatively, with the virtual environment loaded, run the python file at `tagstudio\tag_studio.py` from your terminal. If you're in the project's root directory, simply run `python3 tagstudio/tag_studio.py`.
|
||||
|
||||
## Workflow Checks
|
||||
|
||||
When pushing your code, several automated workflows will check it against predefined tests and style checks. It's _highly recommended_ that you run these checks locally beforehand to avoid having to fight back-and-forth with the workflow checks inside your pull requests.
|
||||
|
||||
> [!TIP]
|
||||
> To format the code automatically before each commit, there's a configured action available for the `pre-commit` hook. Install it by running `pre-commit install`. The hook will be executed each time on running `git commit`.
|
||||
|
||||
### [Ruff](https://github.com/astral-sh/ruff)
|
||||
|
||||
A Python linter and code formatter. Ruff uses the `pyproject.toml` as its config file and runs whenever code is pushed or pulled into the project.
|
||||
|
||||
#### Running Locally
|
||||
|
||||
- Lint code with by moving into the `/tagstudio` directory with `cd tagstudio` and running `ruff --config ../pyproject.toml`.
|
||||
- Format code with `ruff format` inside the repository directory
|
||||
|
||||
Ruff is also available as a VS Code [extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff), PyCharm [plugin](https://plugins.jetbrains.com/plugin/20574-ruff), and [more](https://docs.astral.sh/ruff/integrations/).
|
||||
|
||||
### [Mypy](https://github.com/python/mypy)
|
||||
|
||||
Mypy is a static type checker for Python. It sure has a lot to say sometimes, but we recommend you take its advice when possible. Mypy also uses the `pyproject.toml` as its config file and runs whenever code is pushed or pulled into the project.
|
||||
|
||||
#### Running Locally
|
||||
|
||||
- **First time only:** Move into the `/tagstudio` directory with `cd tagstudio` and run the following:
|
||||
- `mkdir -p .mypy_cache`
|
||||
- `mypy --install-types --non-interactive`
|
||||
- Check code by moving into the `/tagstudio` directory with `cd tagstudio` _(if you aren't already inside)_ and running `mypy --config-file ../pyproject.toml .`. _(Don't forget the `.` at the end!)_
|
||||
|
||||
> [!CAUTION]
|
||||
> There's a known issue between PySide v6.6.3 and Mypy where Mypy will detect issues with the `.pyi` files inside of PySide and prematurely stop checking files. This issue is not present in PySide v6.6.2, which _should_ be compatible with everything else if you wish to try using that version in the meantime.
|
||||
|
||||
Mypy is also available as a VS Code [extension](https://marketplace.visualstudio.com/items?itemName=matangover.mypy), PyCharm [plugin](https://plugins.jetbrains.com/plugin/11086-mypy), and [more](https://plugins.jetbrains.com/plugin/11086-mypy).
|
||||
|
||||
### PyTest
|
||||
|
||||
- Run all tests by moving into the `/tagstudio` directory with `cd tagstudio` and running `pytest tests/`.
|
||||
|
||||
## Code Guidelines
|
||||
|
||||
### Style
|
||||
|
||||
Most of the style guidelines can be checked, fixed, and enforced via Ruff. Older code may not be adhering to all of these guidelines, in which case _"do as I say, not as I do"..._
|
||||
|
||||
- Do your best to write clear, concise, and modular code.
|
||||
- Try to keep a maximum column with of no more than **100** characters.
|
||||
- Code comments should be used to help describe sections of code that don't speak for themselves.
|
||||
- Use [Google style](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) docstrings for any classes and functions you add.
|
||||
- If you're modifying an existing function that does _not_ have docstrings, you don't _have_ to add docstrings to it... but it would be pretty cool if you did ;)
|
||||
- Imports should be ordered alphabetically (in newly created python files).
|
||||
- When writing text for window titles, form titles, or dropdown options, use "[Title Case](https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case)" capitalization. Your IDE may have a command to format this for you automatically, although some may incorrectly capitalize short prepositions. In a pinch you can use a website such as [capitalizemytitle.com](https://capitalizemytitle.com/) to check.
|
||||
- If it wasn't mentioned above, then stick to [**PEP-8**](https://peps.python.org/pep-0008/)!
|
||||
> [!WARNING]
|
||||
> Column width limits, docstring formatting, and import sorting aren't currently checked in the Ruff workflow but likely will be in the near future.
|
||||
|
||||
### Implementations
|
||||
|
||||
- Avoid direct calls to `os`
|
||||
- Use `Pathlib` library instead of `os.path`
|
||||
- Use `sys.platform` instead of `os.name`
|
||||
- Don't prepend local imports with `tagstudio`, stick to `src`
|
||||
- Use `logging` instead of `print` statements
|
||||
- Avoid nested `f-string`s
|
||||
|
||||
#### Runtime
|
||||
|
||||
- Code must function on supported versions of Windows, macOS, and Linux:
|
||||
- Windows: 10, 11
|
||||
- macOS: 12.0+
|
||||
- Linux: TBD
|
||||
- Avoid use of unnecessary logging statements in final submitted code.
|
||||
- Code should not cause unreasonable slowdowns to the program outside of a progress-indicated task.
|
||||
|
||||
#### Git/GitHub Specifics
|
||||
|
||||
- Use clear and concise commit messages. If your commit does too much, either consider breaking it up into smaller commits or providing extra detail in the commit description.
|
||||
- Use imperative-style present-tense commit messages. Examples:
|
||||
- "Add feature foo"
|
||||
- "Change method bar"
|
||||
- "Fix function foobar"
|
||||
- Pull Requests should have an adequate title and description which clearly outline your intentions and changes/additions. Feel free to provide screenshots, GIFs, or videos, especially for UI changes.
|
||||
|
||||
## Documentation Guidelines
|
||||
|
||||
Documentation contributions include anything inside of the `doc/` folder, as well as the `README.md` and `CONTRIBUTING.md` files.
|
||||
|
||||
- Use "[snake_case](https://developer.mozilla.org/en-US/docs/Glossary/Snake_case)" for file and folder names
|
||||
- Follow the folder structure pattern
|
||||
- Don't add images or other media with excessively large file sizes
|
||||
- Provide alt text for all embedded media
|
||||
- Use "[Title Case](https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case)" for title capitalization
|
||||
|
||||
## Translation Guidelines
|
||||
|
||||
_TBA_
|
||||
111
README.md
@@ -1,4 +1,4 @@
|
||||
# TagStudio (Preview/Alpha): A User-Focused Document Management System
|
||||
# TagStudio (Alpha): A User-Focused Document Management System
|
||||
|
||||
<p align="center">
|
||||
<img width="60%" src="github_header.png">
|
||||
@@ -10,14 +10,18 @@
|
||||
|
||||
TagStudio is a photo & file organization application with an underlying system that focuses on giving freedom and flexibility to the user. No proprietary programs or formats, no sea of sidecar files, and no complete upheaval of your filesystem structure.
|
||||
|
||||
<p align="center">
|
||||
<img width="80%" src="screenshot.jpg">
|
||||
</p>
|
||||
<figure align="center">
|
||||
<img width="80%" src="screenshot.jpg" alt="TagStudio Screenshot" align="center">
|
||||
|
||||
<figcaption><i>TagStudio Alpha v9.1.0 running on Windows 10.</i></figcaption>
|
||||
</figure>
|
||||
|
||||
## Contents
|
||||
|
||||
- [Goals](#goals)
|
||||
- [Priorities](#priorities)
|
||||
- [Current Features](#current-features)
|
||||
- [Contributing](#contributing)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [FAQ](#faq)
|
||||
@@ -44,64 +48,35 @@ TagStudio is a photo & file organization application with an underlying system t
|
||||
- Name, Author, Artist (Single-Line Text Fields)
|
||||
- Description, Notes (Multiline Text Fields)
|
||||
- Tags, Meta Tags, Content Tags (Tag Boxes)
|
||||
- Crete rich tags composed of a name, a list of aliases, and a list of “subtags” - being tags in which these tags inherit values from.
|
||||
- Search for entries based on tags, metadata, or filename (using `filename: <query>`)
|
||||
- Create rich tags composed of a name, a list of aliases, and a list of “subtags” - being tags in which these tags inherit values from.
|
||||
- Search for entries based on tags, ~~metadata~~ (TBA), or filenames/filetypes (using `filename: <query>`)
|
||||
- Special search conditions for entries that are: `untagged`/`no tags` and `empty`/`no fields`.
|
||||
|
||||
> [!NOTE]
|
||||
> For more information on the project itself, please see the [FAQ](#faq) section and other docs.
|
||||
> For more information on the project itself, please see the [FAQ](#faq) section as well as the [documentation](/doc/index.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
If you're interested in contributing to TagStudio, please take a look at the [contribution guidelines](/CONTRIBUTING.md) for how to get started!
|
||||
|
||||
## Installation
|
||||
> [!CAUTION]
|
||||
> TagStudio is only currently verified to work on Windows. I've run into issues with the Qt code running on Linux, but I don't know how severe these issues are. There's also likely bugs regarding filenames and portability of the databases across different OSes.
|
||||
|
||||
### Prerequisites
|
||||
To download TagStudio, visit the [Releases](https://github.com/TagStudioDev/TagStudio/releases) section of the GitHub repository and download the latest release for your system under the "Assets" section. TagStudio is available for **Windows**, **macOS** _(Apple Silicon & Intel)_, and **Linux**. Windows and Linux builds are also available in portable versions if you want a more self-contained executable to move around.
|
||||
|
||||
- Python 3.9.6 - ~3.10 *(Not working on 3.12)*
|
||||
For video thumbnails and playback, you'll also need [FFmpeg](https://ffmpeg.org/download.html) installed on your system.
|
||||
|
||||
### Creating the Virtual Environment
|
||||
|
||||
*Skip this step if launching from the .sh script on Linux.*
|
||||
|
||||
1. In the root repository directory, create a python virtual environment:
|
||||
`python3 -m venv .venv`
|
||||
2. Activate your environment:
|
||||
|
||||
- Windows w/Powershell: `.venv\Scripts\Activate.ps1`
|
||||
- Windows w/Command Prompt: `.venv\Scripts\activate.bat`
|
||||
- Linux/macOS: `source .venv/bin/activate`
|
||||
|
||||
3. Install the required packages:
|
||||
`pip install -r requirements.txt`
|
||||
|
||||
_Learn more about setting up a virtual environment [here](https://docs.python.org/3/tutorial/venv.html)._
|
||||
|
||||
### Launching
|
||||
|
||||
> [!NOTE]
|
||||
> Depending on your system, Python may be called `python`, `py`, `python3`, or `py3`. These instructions use the alias `python3`.
|
||||
> [!IMPORTANT]
|
||||
> On macOS, you may be met with a message saying _""TagStudio" can't be opened because Apple cannot check it for malicious software."_ If you encounter this, then you'll need to go to the "Settings" app, navigate to "Privacy & Security", and scroll down to a section that says _""TagStudio" was blocked from use because it is not from an identified developer."_ Click the "Open Anyway" button to allow TagStudio to run. You should only have to do this once after downloading the application.
|
||||
|
||||
#### Optional Arguments
|
||||
|
||||
Optional arguments to pass to the program.
|
||||
|
||||
> `--open <path>` / `-o <path>`
|
||||
> Path to a TagStudio Library folder to open on start.
|
||||
|
||||
#### Windows
|
||||
|
||||
To launch TagStudio, launch the `start_win.bat` file. You can modify this .bat file or create a shortcut and add one or more additional arguments if desired.
|
||||
|
||||
Alternatively, with the virtual environment loaded, run the python file at `tagstudio\tagstudio.py` from your terminal. If you're in the project's root directory, simply run `python3 tagstudio/tagstudio.py`.
|
||||
|
||||
> [!CAUTION]
|
||||
> TagStudio on Linux & macOS likely won't function correctly at this time. If you're trying to run this in order to help test, debug, and improve compatibility, then charge on ahead!
|
||||
|
||||
#### macOS
|
||||
|
||||
With the virtual environment loaded, run the python file at "tagstudio/tagstudio.py" from your terminal. If you're in the project's root directory, simply run `python3 tagstudio/tagstudio.py`. When launching the program in the future, remember to activate the virtual environment each time before launching *(an easier method is currently being worked on).*
|
||||
|
||||
#### Linux
|
||||
|
||||
Run the "TagStudio.sh" script, and the program should launch! (Make sure that the script is marked as executable). Note that launching from the script from outside of a terminal will not launch a terminal window with any debug or crash information. If you wish to see this information, just launch the shell script directly from your terminal with `sh TagStudio.sh`.
|
||||
> `--config-file <path>` / `-c <path>`
|
||||
> Path to the TagStudio config file to load.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -146,9 +121,6 @@ To create a new tag, click on Edit -> New Tag from the menu bar. From there, ent
|
||||
|
||||
To edit a tag, right-click the tag in the tag field of the preview pane and select “Edit Tag”
|
||||
|
||||
> [!WARNING]
|
||||
> There is currently no method to view all tags that you’ve created in your library. This is a top priority for future releases.
|
||||
|
||||
### Relinking Renamed/Moved Files
|
||||
|
||||
Inevitably, some of the files inside your library will be renamed, moved, or deleted. If a file has been renamed or moved, TagStudio will display the thumbnail as a red tag with a cross through it _(this icon is also used for items with broken thumbnails)._ To relink moved files or delete these entries, go to Tools -> Manage Unlinked Entries. Click the “Refresh” button to scan your library for unlinked entries. Once complete, you can attempt to “Search & Relink” any unlinked entries to their respective files, or “Delete Unlinked Entries” in the event the original files have been deleted and you no longer wish to keep their metadata entries inside your library.
|
||||
@@ -177,7 +149,7 @@ Load in a .dupeguru file generated by [dupeGuru](https://github.com/arsenetar/du
|
||||
Create an image collage of your photos and videos.
|
||||
|
||||
> [!CAUTION]
|
||||
> Collage sizes and options are hardcoded.
|
||||
> Collage sizes and options are hardcoded, and there's no GUI indicating the process of the collage creation.
|
||||
|
||||
#### Macros
|
||||
|
||||
@@ -193,14 +165,21 @@ Import JSON sidecar data generated by [gallery-dl](https://github.com/mikf/galle
|
||||
> [!CAUTION]
|
||||
> This feature is not supported or documented in any official capacity whatsoever. It will likely be rolled-in to a larger and more generalized sidecar importing feature in the future.
|
||||
|
||||
## Launching/Building From Source
|
||||
|
||||
See instructions in the "[Creating Development Environment](/CONTRIBUTING.md/#creating-a-development-environment)" section from the [contribution documentation](/CONTRIBUTING.md).
|
||||
|
||||
## FAQ
|
||||
|
||||
### What State Is the Project Currently In?
|
||||
|
||||
As of writing (Alpha v9.1.0) the project is in a “useable” state, however it lacks proper testing and quality of life features. Currently the program has only been verified to work properly on Windows, and is unlikely to properly run on Linux or macOS in its current state, however this functionality is a priority going forward with testers.
|
||||
As of writing (Alpha v9.3.0) the project is in a useable state, however it lacks proper testing and quality of life features.
|
||||
|
||||
### What Features Are You Planning on Adding?
|
||||
|
||||
> [!IMPORTANT]
|
||||
> See the [Planned Features](/doc/updates/planned_features.md) documentation for the latest feature lists. The lists here are currently being migrated over there with individual pages for larger features.
|
||||
|
||||
Of the several features I have planned for the project, these are broken up into “priority” features and “future” features. Priority features were originally intended for the first public release, however are currently absent from the Alpha v9.x.x builds.
|
||||
|
||||
#### Priority Features
|
||||
@@ -210,18 +189,16 @@ Of the several features I have planned for the project, these are broken up into
|
||||
- Boolean Search
|
||||
- Coexisting Text + Tag Search
|
||||
- Searchable File Metadata
|
||||
- Tag management view
|
||||
- Applying metadata via multi-selection
|
||||
- Comprehensive Tag management tab
|
||||
- Easier ways to apply tags in bulk
|
||||
- Tag Search Panel
|
||||
- Recent Tags Panel
|
||||
- Top Tags Panel
|
||||
- Pinned Tags Panel
|
||||
- Apply tags based on system folders
|
||||
- Better (stable, performant) library grid view
|
||||
- Improved entry relinking
|
||||
- Cached thumbnails
|
||||
- Collations
|
||||
- Tag-like Groups
|
||||
- Resizable thumbnail grid
|
||||
- User-defined metadata fields
|
||||
- Multiple directory support
|
||||
@@ -231,13 +208,13 @@ Of the several features I have planned for the project, these are broken up into
|
||||
- Better internal API for accessing Entries, Tags, Fields, etc. from the library.
|
||||
- Proper testing workflow
|
||||
- Continued code cleanup and modularization
|
||||
- Reassessment of save file structure in order to prioritize portability (leading to exportable tags, presets, etc)
|
||||
- Exportable/importable library data including "Tag Packs"
|
||||
|
||||
#### Future Features
|
||||
|
||||
- Support for multiple simultaneous users/clients
|
||||
- Draggable files outside the program
|
||||
- Ability to ignore specific files
|
||||
- Comprehensive filetype whitelist
|
||||
- A finished “macro system” for automatic tagging based on predetermined criteria.
|
||||
- Different library views
|
||||
- Date and time fields
|
||||
@@ -245,20 +222,20 @@ Of the several features I have planned for the project, these are broken up into
|
||||
- Audio waveform previews
|
||||
- 3D object previews
|
||||
- Additional previews for miscellaneous file types
|
||||
- Exportable/sharable tags and settings
|
||||
- Optional global tags and settings, spanning across libraries
|
||||
- Importing & exporting libraries to/from other programs
|
||||
- Port to a more performant language and modern frontend (Rust?, Tauri?, etc.)
|
||||
- Plugin system
|
||||
- Local OCR search
|
||||
- Support for local machine learning-based tag suggestions for images
|
||||
- Mobile version
|
||||
- Mobile version _(FAR future)_
|
||||
|
||||
#### Features I Likely Won’t Add/Pull
|
||||
- Native Cloud Integration
|
||||
|
||||
- Native Cloud Integration
|
||||
- There are plenty of services already (native or third-party) that allow you to mount your cloud drives as virtual drives on your system. Pointing TagStudio to one of these mounts should function similarly to what native integration would look like.
|
||||
- Native ChatGPT/Non-Local LLM Integration
|
||||
- This could mean different things depending on what you're intending. Whether it's trying to use an LLM to replace the native search, or to trying to use a model for image recognition, I'm not interested in hooking people's TagStudio libraries into non-local LLMs such as ChatGPT and/or turn the program into a "chatbot" interface (see: [Goals/Privacy](#goals)). I wouldn't, however, mind using **locally** hosted models to provide the *optional* ability for additional searching and tagging methods (especially when it comes to facial recognition).
|
||||
- This could mean different things depending on what you're intending. Whether it's trying to use an LLM to replace the native search, or to trying to use a model for image recognition, I'm not interested in hooking people's TagStudio libraries into non-local LLMs such as ChatGPT and/or turn the program into a "chatbot" interface (see: [Goals/Privacy](#goals)). I wouldn't, however, mind using **locally** hosted models to provide the _optional_ ability for additional searching and tagging methods (especially when it comes to facial recognition).
|
||||
|
||||
### Why Is the Version Already v9?
|
||||
|
||||
@@ -266,10 +243,4 @@ I’ve been developing this project over several years in private, and have gone
|
||||
|
||||
### Wait, Is There a CLI Version?
|
||||
|
||||
As of right now, no. However, I _did_ have a CLI version in the recent past before dedicating my efforts to the Qt GUI version. I’ve left in the currently-inoperable CLI code just in case anyone was curious about it. Also yes, it’s just a bunch of glorified print statements (_the outlook for some form of curses on Windows didn’t look great at the time, and I just needed a driver for the newly refactored code...)._
|
||||
|
||||
### Can I Contribute?
|
||||
|
||||
**Yes!!** I recommend taking a look at the [Priority Features](#priority-features), [Future Features](#future-features), and [Features I Won't Pull](#features-i-likely-wont-addpull) lists, as well as the project issues to see what’s currently being worked on. Please do not submit pull requests with new feature additions without opening up an issue with a feature request first.
|
||||
|
||||
As of writing I don’t have a concrete style guide, just try to stay within or close enough to the PEP 8 style guide and/or match the style of the existing code.
|
||||
As of right now, **no**. However, I _did_ have a CLI version in the recent past before dedicating my efforts to the Qt GUI version. I’ve left in the currently-inoperable CLI code just in case anyone was curious about it. Also yes, it’s just a bunch of glorified print statements (_the outlook for some form of curses on Windows didn’t look great at the time, and I just needed a driver for the newly refactored code...)._
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! /bin/bash
|
||||
python3 -m venv .venv
|
||||
#! /usr/bin/env bash
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
! [ -d .venv ] && python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python tagstudio/tagstudio.py
|
||||
python tagstudio/tag_studio.py
|
||||
|
||||
BIN
doc/assets/db_schema.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
doc/assets/tag_override_ex-1.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
doc/assets/tag_override_ex-2.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
@@ -1,189 +0,0 @@
|
||||
# TagStudio Documentation (Alpha v9.1.0)
|
||||
|
||||
## _A User-Focused Document Management System_
|
||||
|
||||
> [!WARNING]
|
||||
> This documentation is still a work in progress, and is intended to aide with deconstructing and understanding of the core mechanics of TagStudio and how it operates.
|
||||
|
||||
## Contents
|
||||
- [Library](#library)
|
||||
- [Fields](#fields)
|
||||
- [Entries](#entries)
|
||||
- [Tags](#tags)
|
||||
- [Retrieving Entries](#retrieving-entries-based-on-tag-cluster)
|
||||
- [Missing File Resolution](#missing-file-resolution)
|
||||
|
||||
## Library
|
||||
|
||||
The Library is how TagStudio represents your chosen directory. In this Library or Vault system, all files within this directory are represented by Entries, which then contain metadata Fields. All TagStudio data for a Library is stored within a `.TagStudio` folder at the root of the Library's directory. Internal Library objects include:
|
||||
|
||||
- Fields (v9+)
|
||||
- Text Line (Title, Author, Artist, URL)
|
||||
- Text Box (Description, Notes)
|
||||
- Tag Box (Tags, Content Tags, Meta Tags)
|
||||
- Datetime (Date Created, Date Modified, Date Taken) [WIP]
|
||||
- Collation (Collation) [WIP]
|
||||
- `name: str`: Collation Name
|
||||
- `page: int`: Page #
|
||||
- Checkbox (Archive, Favorite) [WIP]
|
||||
- Drop Down (Group of Tags to select one from) [WIP]
|
||||
- Entries (v1+)
|
||||
- Tags (v7+)
|
||||
- Macros (v9/10+)
|
||||
|
||||
## Fields
|
||||
|
||||
Fields are the the building blocks of metadata stored in Entires. Fields have several base types for representing different types of information, including:
|
||||
|
||||
- `text_line`
|
||||
- A string of text, displayed as a single line.
|
||||
- Useful for Titles, Authors, URLs, etc.
|
||||
- `text_box`
|
||||
- A long string of text displayed as a box of text.
|
||||
- Useful for descriptions, notes, etc.
|
||||
- `datetime` [WIP]
|
||||
- A date and time value.
|
||||
- `tag_box`
|
||||
- A box of tags added by the user.
|
||||
- Multiple tag boxes can be used to separate classifications of tags, ex. 'Content Tags' and 'Meta Tags'.
|
||||
- `checkbox` [WIP]
|
||||
- A two-state checkbox.
|
||||
- Can be associated with a tag for quick organization.
|
||||
- `collation` [WIP]
|
||||
- A collation is a collection of files that are intended to be displayed and retrieved together. Examples may include pages of a book or document that are spread out across several individual files. If you're intention is to associate files across multiple 'collations', use Tags instead!
|
||||
|
||||
## Entries
|
||||
|
||||
Entries are the representations of your files within the Library. They consist of a reference to the file on your drive, as well as the metadata associated with it.
|
||||
|
||||
### Entry Object Structure (v9):
|
||||
|
||||
- `id`:
|
||||
- ID for the Entry.
|
||||
- Int, Unique, Required
|
||||
- Used for internal processing
|
||||
- `filename`:
|
||||
- The filename with extension of the referenced media file.
|
||||
- String, Required
|
||||
- `path`:
|
||||
- The folder path in which the media file is located in.
|
||||
- String, Required, OS Agnostic
|
||||
- `fields`:
|
||||
- A list of Field ID/Value dicts.
|
||||
- List of dicts, Optional
|
||||
|
||||
NOTE: _Entries currently have several unused optional fields intended for later features._
|
||||
|
||||
## Tags
|
||||
|
||||
**Tags** are small data objects that represent an attribute of something. A person, place, thing, concept, you name it! Tags in TagStudio allow for more sophisticated Entry organization and searching thanks to their ability to contain alternate names and spellings via `aliases`, relational organization thanks to inherent `subtags`, and more! Tags can be as simple or as powerful as you want to make them, and TagStudio aims to provide as much power to you as possible.
|
||||
|
||||
### Tag Object Structure (v9):
|
||||
|
||||
- `id`:
|
||||
- ID for the Tag.
|
||||
- Int, Unique, Required
|
||||
- Used for internal processing
|
||||
- `name`:
|
||||
- The normal name of the Tag, with no shortening or specification.
|
||||
- String, Required
|
||||
- Doesn't have to be unique
|
||||
- Each word analyzed individually
|
||||
- Used for display, searching, and storing
|
||||
- `shorthand`:
|
||||
- The shorthand name for the Tag.
|
||||
- String, Optional
|
||||
- Doesn't have to be unique
|
||||
- Entire string analyzed as-is
|
||||
- Used for display and searching
|
||||
- `aliases`:
|
||||
- Alternate names for the Tag.
|
||||
- List of Strings, Optional
|
||||
- Recommended to be unique to this Tag
|
||||
- Entire string analyzed as-is
|
||||
- Used for searching
|
||||
- `subtags`:
|
||||
- Other Tags that make up properties of this Tag.
|
||||
- List of Strings, Optional
|
||||
- Used for display (first subtag only) and searching.
|
||||
- `color`:
|
||||
- A hex code value for customizing the Tag's display color
|
||||
- String, Optional
|
||||
- Used for display
|
||||
|
||||
### Tag Examples:
|
||||
|
||||
#### League of Legends
|
||||
|
||||
- `name`: "League of Legends"
|
||||
- `shorthand`: "LoL"
|
||||
- `aliases`: ["League"]
|
||||
- `subtags`: ["Game", "Fantasy"]
|
||||
|
||||
#### Arcane
|
||||
|
||||
- `name`: "Arcane"
|
||||
- `shorthand`: ""
|
||||
- `aliases`: []
|
||||
- `subtags`: ["League of Legends", "Cartoon"]
|
||||
|
||||
#### Jinx (LoL)
|
||||
|
||||
- `name`: "Jinx Piltover"
|
||||
- `shorthand`: "Jinx"
|
||||
- `aliases`: ["Jinxy", "Jinxy Poo"]
|
||||
- `subtags`: ["League of Legends", "Arcane", "Character"]
|
||||
|
||||
#### Zander (Arcane)
|
||||
|
||||
- `name`: "Zander Zanderson"
|
||||
- `shorthand`: "Zander"
|
||||
- `aliases`: []
|
||||
- `subtags`: ["Arcane", "Character"]
|
||||
|
||||
#### Mr. Legend (LoL)
|
||||
|
||||
- `name`: "Mr. Legend"
|
||||
- `shorthand`: ""
|
||||
- `aliases`: []
|
||||
- `subtags`: ["League of Legends", "Character"]
|
||||
|
||||
### Query "League of Legends" returns results for:
|
||||
|
||||
- League of Legends [because of "League of Legend"'s name]
|
||||
- Arcane [because of "Arcane"'s subtag]
|
||||
- Jinx (LoL) [because of "Jinx Piltover"'s subtag]
|
||||
- Mr. Legend (LoL) [because of "Mr. Legned (LoL)'s subtag"]
|
||||
- Zander (Arcane) [because of "Zander Zanderson"'s subtag ("Arcane")'s subtag]
|
||||
|
||||
### Query "LoL" returns results for:
|
||||
|
||||
- League of Legends [because of "League of Legend"'s shorthand]
|
||||
- LoL [because of "League of Legend"'s shorthand]
|
||||
- Arcane [because of "Arcane"'s subtag]
|
||||
- Jinx (LoL) [because of "Jinx Piltover"'s subtag]
|
||||
- Mr. Legend (LoL) [because of "Mr. Legned (LoL)'s subtag"]
|
||||
- Zander (Arcane) [because of "Zander Zanderson"'s subtag ("Arcane")'s subtag]
|
||||
|
||||
### Query "Arcane" returns results for:
|
||||
|
||||
- Arcane [because of "Arcane"'s name]
|
||||
- Jinx (LoL) [because of "Jinx Piltover"'s subtag "Arcane"]
|
||||
- Zander (Arcane) [because of "Zander Zanderson"'s subtag]
|
||||
|
||||
## Retrieving Entries based on Tag Cluster
|
||||
|
||||
By default when querying Entries, each Entry's `tags` list (stored in the form of Tag `id`s) is compared against the Tag `id`s in a given Tag cluster (list of Tag `id`s) or appended clusters in the case of multi-term queries. The type of comparison depends on the type of query and whether or not it is an inclusive or exclusive query, or a combination of both. This default searching behavior is done in _O(n)_ time, but can be sped up in the future by building indexes on certain search terms. These indexes can be stored on disk and loaded back into memory in future sessions. These indexes will also need to be updated as new Tags and Entries are added or edited.
|
||||
|
||||
## Missing File Resolution
|
||||
|
||||
1. Refresh missing file list (`refresh missing`) (Automatically run if library has few entries)
|
||||
2. Fix missing files screen (`fix missing`)
|
||||
|
||||
### Fix Missing Files Screen
|
||||
|
||||
0. **Match Search** (Determines if entries can be fixed) Scans for filename in library directory
|
||||
1. **Quick Fixes** (one match found, no existing entry)
|
||||
2. **Match Selection** (multiple matches found)
|
||||
3. **Merge Conflict Resolution** (match has existing entry)
|
||||
Any remaining missing files can be listed, but they probably really are missing at this point. You can update the path and filename to point to new files if you know where they should actually be pointing to.
|
||||
24
doc/index.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Welcome to the TagStudio Documentation!
|
||||
|
||||
> [!WARNING]
|
||||
> This documentation is still a work in progress, and is intended to aide with deconstructing and understanding of the core mechanics of TagStudio and how it operates.
|
||||
|
||||
<div align="center">
|
||||
<img src="../github_header.png" alt="TagStudio Alpha" height="100">
|
||||
<img src="https://i0.wp.com/www.bapl.org/wp-content/uploads/2019/02/old-under-construction-gif.gif" alt="Under Construction" height="100">
|
||||
</div>
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Library](/doc/library/library.md)
|
||||
- [Entries](/doc/library/entry.md)
|
||||
- [Fields](/doc/library/field.md)
|
||||
- [Tags](/doc/library/tag.md)
|
||||
- [Tools & Macros](/doc/utilities/macro.md)
|
||||
- [Planned Features](/doc/updates/planned_features.md)
|
||||
|
||||
---
|
||||
|
||||
### [Database Migration](/doc/updates/db_migration.md)
|
||||
|
||||
The "Database Migration", "DB Migration", or "SQLite Migration" is an upcoming update to TagStudio which will replace the current JSON [library](/doc/library/library.md) with a SQL-based one, and will additionally include some fundamental changes to how some features such as [tags](/doc/library/tag.md) will work.
|
||||
25
doc/library/entry.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Entry
|
||||
|
||||
Entries are the units that fill a [library](/doc/library/library.md). Each one corresponds to a file, holding a reference to it along with the metadata associated with it.
|
||||
|
||||
### Entry Object Structure
|
||||
|
||||
1. `id`:
|
||||
- Int, Unique, **Required**
|
||||
- The ID for the Entry.
|
||||
- Used for internal processing
|
||||
2. `filename`:
|
||||
- String, **Required**
|
||||
- The filename with extension of the referenced media file.
|
||||
3. `path`:
|
||||
- String, **Required**, OS Agnostic
|
||||
- The folder path in which the media file is located in.
|
||||
4. [`fields`](/doc/library/field.md):
|
||||
- List of dicts, Optional
|
||||
- A list of Field ID/Value dicts.
|
||||
|
||||
NOTE: _Entries currently have several unused optional fields intended for later features._
|
||||
|
||||
## Retrieving Entries based on [Tag](/doc/library/tag.md) Cluster
|
||||
|
||||
By default when querying Entries, each Entry's `tags` list (stored in the form of Tag `id`s) is compared against the Tag `id`s in a given Tag cluster (list of Tag `id`s) or appended clusters in the case of multi-term queries. The type of comparison depends on the type of query and whether or not it is an inclusive or exclusive query, or a combination of both. This default searching behavior is done in _O(n)_ time, but can be sped up in the future by building indexes on certain search terms. These indexes can be stored on disk and loaded back into memory in future sessions. These indexes will also need to be updated as new Tags and Entries are added or edited.
|
||||
3
doc/library/entry_groups.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Entry Groups (Upcoming Feature)
|
||||
|
||||
Entries can be grouped via tags marked as “groups” which when applied to different entries will signal TagStudio to treat those entries as a single group inside of searches and browsing.
|
||||
34
doc/library/field.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Field
|
||||
|
||||
Fields are the building blocks of metadata stored in [entries](/doc/library/entry.md). Fields have several base types for representing different kinds of information, including:
|
||||
|
||||
#### `text_line`
|
||||
|
||||
- A string of text, displayed as a single line.
|
||||
- e.g: Title, Author, Artist, URL, etc.
|
||||
|
||||
#### `text_box`
|
||||
|
||||
- A long string of text displayed as a box of text.
|
||||
- e.g: Description, Notes, etc.
|
||||
|
||||
#### `tag_box`
|
||||
|
||||
- A box of [tags](/doc/library/tag.md) defined and added by the user.
|
||||
- Multiple tag boxes can be used to separate classifications of tags.
|
||||
- e.g: Content Tags, Meta Tags, etc.
|
||||
|
||||
#### `datetime` [WIP]
|
||||
|
||||
- A date and time value.
|
||||
- e.g: Date Created, Date Modified, Date Taken, etc.
|
||||
|
||||
#### `checkbox` [WIP]
|
||||
|
||||
- A simple two-state checkbox.
|
||||
- Can be associated with a tag for quick organization.
|
||||
- e.g: Archive, Favorite, etc.
|
||||
|
||||
#### `collation` [obsolete]
|
||||
|
||||
- Previously used for associating files to be used in a [collation](/doc/utilities/macro.md#create-collage), will be removed in favor of a more flexible feature in future updates.
|
||||
11
doc/library/library.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Library
|
||||
|
||||
The library is how TagStudio represents your chosen directory, with every file inside of it being displayed as an [entry](/doc/library/entry.md). You can have as many or few libraries as you wish, since each libraries' data is stored within a "`.TagStudio`" folder at its root.
|
||||
Note that this means [tags](/doc/library/tag.md) you create only exist _per-library_.
|
||||
|
||||
### Library Contents
|
||||
|
||||
- [Entries](/doc/library/entry.md)
|
||||
- [Fields](/doc/library/field.md)
|
||||
- [Tags](/doc/library/tag.md)
|
||||
- [Macros](/doc/utilities/macro.md)
|
||||
85
doc/library/tag.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Tag
|
||||
|
||||
Tags are user-defined attributes made up of one or more keywords, aliases, and relationships to other tags. A person, place, thing, concept, you name it! Tags allow for a more sophisticated way to organize and search [entries](/doc/library/entry.md) thanks to their aliases, parent tags, and more.
|
||||
Tags can be as simple or complex as wanted, so that any user can tune TagStudio to fit their needs.
|
||||
|
||||
Among the things that make tags so useful, aliases give the ability to contain alternate names and spellings, making searches intuitive and expansive. Furthermore, parent-tags/subtags offer relational organization capabilities for the structuring and connection of the [library's](/doc/library/library.md) contents.
|
||||
|
||||
## Tag Object Structure
|
||||
|
||||
#### `id`
|
||||
|
||||
ID for the tag.
|
||||
|
||||
- Int, Unique, Required
|
||||
- Used for internal processing
|
||||
|
||||
#### `name`
|
||||
|
||||
The normal name of the tag, with no shortening or specification.
|
||||
|
||||
- String, Required
|
||||
- Doesn't have to be unique
|
||||
- Used for display, searching, and storing
|
||||
|
||||
#### `shorthand`
|
||||
|
||||
The shorthand name for the tag. Works like an alias but is used for specific display purposes.
|
||||
|
||||
- String, Optional
|
||||
- Doesn't have to be unique
|
||||
- Used for display and searching
|
||||
|
||||
#### `aliases`
|
||||
|
||||
Alternate names for the tag.
|
||||
|
||||
- List of Strings, Optional
|
||||
- Recommended to be unique to this tag
|
||||
- Used for searching
|
||||
|
||||
#### `subtags`
|
||||
|
||||
Other Tags that make up properties of this tag. Also called "parent tags".
|
||||
|
||||
- List of Strings, Optional
|
||||
- Used for display (first parent tag only) and searching.
|
||||
|
||||
#### `color`
|
||||
|
||||
A color name string for customizing the tag's display color
|
||||
|
||||
- String, Optional
|
||||
- Used for display
|
||||
|
||||
## Tag Search Examples:
|
||||
|
||||
Using for example, a library of files including some tagged with the following tags:
|
||||
|
||||
| Tag | `name` | `shorthand` | `aliases` | `parent tags` |
|
||||
| ------------------- | ------------------- | ----------- | ---------------------- | -------------------------------------------- |
|
||||
| _League of Legends_ | "League of Legends" | "LoL" | ["League"] | ["Game", "Fantasy"] |
|
||||
| _Arcane_ | "Arcane" | "" | [] | ["League of Legends", "Cartoon"] |
|
||||
| _Jinx (LoL)_ | "Jinx Piltover" | "Jinx" | ["Jinxy", "Jinxy Poo"] | ["League of Legends", "Arcane", "Character"] |
|
||||
| _Zander (Arcane)_ | "Zander Zanderson" | "Zander" | [] | ["Arcane", "Character"] |
|
||||
| _Mr. Legend (LoL)_ | "Mr. Legend" | "" | [] | ["League of Legends", "Character"] |
|
||||
|
||||
**The query "Arcane" will display results tagged with:**
|
||||
|
||||
| Tag | Cause of Inclusion | Tag Tree Lineage |
|
||||
| --------------- | -------------------------------- | -------------------------- |
|
||||
| Arcane | Direct match of tag name | "Arcane" |
|
||||
| Jinx (LoL) | Search term is set as parent tag | "Jinx (LoL) > Arcane" |
|
||||
| Zander (Arcane) | Search term is set as parent tag | "Zander (Arcane) > Arcane" |
|
||||
|
||||
**The query "League of Legends" will display results tagged with:**
|
||||
|
||||
| Tag | Cause of Inclusion | Tag Tree Lineage |
|
||||
| ----------------- | ------------------------------------------------------ | ---------------------------------------------- |
|
||||
| League of Legends | Direct match of tag name | "League of Legends" |
|
||||
| Arcane | Search term is set as parent tag | "Arcane > League of Legends" |
|
||||
| Jinx (LoL) | Search term is set as parent tag | "Jinx (LoL) > League of Legends" |
|
||||
| Mr. Legend (LoL) | Search term is set as parent tag | "Mr. Legend (LoL) > League of Legends" |
|
||||
| Zander (Arcane) | Search term is a parent tag of a tag set as parent tag | "Zander (Arcane) > Arcane > League of Legends" |
|
||||
|
||||
Note: The query "LoL" will display the same results as the above example since "LoL" is the shorthand for "League of Legends".
|
||||
3
doc/library/tag_categories.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Tag Categories (Upcoming Feature)
|
||||
|
||||
Replaces [Tag Fields](/doc/library/field.md#tag_box). Tags are able to be marked as a “category” which then displays as tag fields currently do, with any tags inheriting from that category being displayed underneath.
|
||||
16
doc/library/tag_overrides.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Tag Overrides (Upcoming Feature)
|
||||
|
||||
Tag overrides are the ability to add or remove [parent tags](/doc/library/tag.md#subtags) from a [tag](/doc/library/tag.md) on a per- [entry](/doc/library/entry.md) basis. Relies on the [Database Migration](/doc/updates/db_migration.md) update being complete.
|
||||
|
||||
## Examples
|
||||
|
||||
<figure>
|
||||
<img src="../assets/tag_override_ex-1.png" alt="Example 1" height="300">
|
||||
<figcaption>Ex. 1 - Comparing standard tag composition vs additive and subtractive inheritance overrides.</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<img src="../assets/tag_override_ex-2.png" alt="Example 2" height="300">
|
||||
|
||||
<figcaption>Ex. 2 - Parent tag swap using tag overrides.</figcaption>
|
||||
</figure>
|
||||
43
doc/updates/db_migration.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Database Migration
|
||||
|
||||
The database migration is an upcoming refactor to TagStudio's library data storage system. The database will be migrated from a JSON-based one to a SQLite-based one. Part of this migration will include a reworked schema, which will allow for several new features and changes to how [tags](/doc/library/tag.md) and [fields](/doc/library/field.md) operate.
|
||||
|
||||
## Schema
|
||||
|
||||
<img src="../assets/db_schema.png" alt="Database Schema" width="500">
|
||||
|
||||
### `alias` Table
|
||||
|
||||
_Description TBA_
|
||||
|
||||
### `entry` Table
|
||||
|
||||
_Description TBA_
|
||||
|
||||
### `entry_attribute` Table
|
||||
|
||||
_Description TBA_
|
||||
|
||||
### `entry_page` Table
|
||||
|
||||
_Description TBA_
|
||||
|
||||
### `location` Table
|
||||
|
||||
_Description TBA_
|
||||
|
||||
### `tag` Table
|
||||
|
||||
_Description TBA_
|
||||
|
||||
### `tag_relation` Table
|
||||
|
||||
_Description TBA_
|
||||
|
||||
## Resulting New Features and Changes
|
||||
|
||||
- Multiple Directory Support
|
||||
- [Tag Categories](/doc/library/tag_categories.md) (Replaces [Tag Fields](/doc/library/field.md#tag_box))
|
||||
- [Tag Overrides](/doc/library/tag_overrides.md)
|
||||
- User-Defined [Fields](/doc/library/field.md)
|
||||
- Tag Icons
|
||||
59
doc/updates/planned_features.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Planned Features
|
||||
|
||||
The following lists outline the planned major and minor features for TagStudio, in no particular order.
|
||||
|
||||
# Major Features
|
||||
|
||||
- [SQL Database Migration](/doc/updates/db_migration.md)
|
||||
- Multiple Directory Support
|
||||
- [Tags Categories](/doc/library/tag_categories.md)
|
||||
- [Entry Groups](/doc/library/entry_groups.md)
|
||||
- [Tag Overrides](/doc/library/tag_overrides.md)
|
||||
- Tagging Panel
|
||||
- Top Tags
|
||||
- Recent Tags
|
||||
- Tag Search
|
||||
- Pinned Tags
|
||||
- Configurable Default Fields (May be part of [Macros](/doc/utilities/macro.md))
|
||||
- Deep File Extension Control
|
||||
- Settings Menu
|
||||
- Custom User Colors
|
||||
- Search Engine Rework
|
||||
- Boolean Search
|
||||
- Tag Objects In Search
|
||||
- Search For Fields
|
||||
- Sortable Search Results
|
||||
- Automatic Entry Relinking
|
||||
- Detect Renames
|
||||
- Detect Moves
|
||||
- Thumbnail Caching
|
||||
- User-Defined Fields
|
||||
- Exportable Library/Tag Data
|
||||
- Exportable Human-Readable Library
|
||||
- Exportable/Importable Human-Readable “Tag Packs”
|
||||
- Exportable/Importable Color Palettes
|
||||
- Configurable Thumbnail Labels
|
||||
- Toggle Extension Label
|
||||
- Toggle File Size Label
|
||||
- Configurable Thumbnail Tag Badges
|
||||
- Customize tags that appear instead of just “Archive” and “Favorite”
|
||||
- OCR Search
|
||||
|
||||
## Minor Features
|
||||
|
||||
- Deleting Tags
|
||||
- Merging Tags
|
||||
- Tag Icons
|
||||
- Tag/Field Copy + Paste
|
||||
- Collage UI
|
||||
- Resizable Thumbnail Grid
|
||||
- Draggable Files Outside The Program
|
||||
- File Property Caching
|
||||
- 3D Previews
|
||||
- Audio Waveform Previews
|
||||
- Toggle Between Waveform And Album Artwork
|
||||
- PDF Previews
|
||||
- SVG Previews
|
||||
- Full Video Player
|
||||
- Duration Properties For Video + Audio Files
|
||||
- Optional Starter Tag Packs
|
||||
43
doc/utilities/macro.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Tools & Macros
|
||||
|
||||
Tools and macros are features that serve to create a more fluid [library](/doc/library/library.md)-managing process, or provide some extra functionality. Please note that some are still in active development and will be fleshed out in future updates.
|
||||
|
||||
## Tools
|
||||
|
||||
### Fix Unlinked Entries
|
||||
|
||||
This tool displays the number of unlinked [entries](/doc/library/entry.md), and some options for their resolution.
|
||||
|
||||
1. Refresh
|
||||
- Scans through the library and updates the unlinked entry count.
|
||||
2. Search & Relink
|
||||
- Attempts to automatically find and reassign missing files.
|
||||
3. Delete Unlinked Entries
|
||||
- Displays a confirmation prompt containing the list of all missing files to be deleted before committing to or cancelling the operation.
|
||||
|
||||
### Fix Duplicate Files
|
||||
|
||||
This tool allows for management of duplicate files in the library using a [DupeGuru](https://dupeguru.voltaicideas.net/) file.
|
||||
|
||||
1. Load DupeGuru File
|
||||
- load the "results" file created from a DupeGuru scan
|
||||
2. Mirror Entries
|
||||
- Duplicate entries will have their contents mirrored across all instances. This allows for duplicate files to then be deleted with DupeGuru as desired, without losing the [field](/doc/library/field.md) data that has been assigned to either. (Once deleted, the "Fix Unlinked Entries" tool can be used to clean up the duplicates)
|
||||
|
||||
### Create Collage
|
||||
|
||||
This tool is a preview of an upcoming feature. When selected, TagStudio will generate a collage of all the contents in a Library, which can be found in the Library folder ("/your-folder/.TagStudio/collages/"). Note that this feature is still in early development, and doesn't yet offer any customization options.
|
||||
|
||||
## Macros
|
||||
|
||||
### Auto-fill [WIP]
|
||||
|
||||
Tool is in development and will be documented in future update.
|
||||
|
||||
### Sort fields
|
||||
|
||||
Tool is in development, will allow for user-defined sorting of [fields](/doc/library/field.md).
|
||||
|
||||
### Folders to Tags
|
||||
|
||||
Creates tags from the existing folder structure in the library, which are previewed in a hierarchy view for the user to confirm. A tag will be created for each folder and applied to all entries, with each subfolder being linked to the parent folder as a [parent tag](/doc/library/tag.md#subtags). Tags will initially be named after the folders, but can be fully edited and customized afterwards.
|
||||
563
flake.lock
generated
Normal file
@@ -0,0 +1,563 @@
|
||||
{
|
||||
"nodes": {
|
||||
"cachix": {
|
||||
"inputs": {
|
||||
"devenv": "devenv_2",
|
||||
"flake-compat": [
|
||||
"devenv",
|
||||
"flake-compat"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
],
|
||||
"pre-commit-hooks": [
|
||||
"devenv",
|
||||
"pre-commit-hooks"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712055811,
|
||||
"narHash": "sha256-7FcfMm5A/f02yyzuavJe06zLa9hcMHsagE28ADcmQvk=",
|
||||
"owner": "cachix",
|
||||
"repo": "cachix",
|
||||
"rev": "02e38da89851ec7fec3356a5c04bc8349cae0e30",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "cachix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devenv": {
|
||||
"inputs": {
|
||||
"cachix": "cachix",
|
||||
"flake-compat": "flake-compat_2",
|
||||
"nix": "nix_2",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"pre-commit-hooks": "pre-commit-hooks"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1724763216,
|
||||
"narHash": "sha256-oW2bwCrJpIzibCNK6zfIDaIQw765yMAuMSG2gyZfGv0=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "1e4ef61205b9aa20fe04bf1c468b6a316281c4f1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devenv-root": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"narHash": "sha256-d6xi4mKdjkX2JFicDIv5niSzpyI0m/Hnm8GGAIU04kY=",
|
||||
"type": "file",
|
||||
"url": "file:///dev/null"
|
||||
},
|
||||
"original": {
|
||||
"type": "file",
|
||||
"url": "file:///dev/null"
|
||||
}
|
||||
},
|
||||
"devenv_2": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"devenv",
|
||||
"cachix",
|
||||
"flake-compat"
|
||||
],
|
||||
"nix": "nix",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"poetry2nix": "poetry2nix",
|
||||
"pre-commit-hooks": [
|
||||
"devenv",
|
||||
"cachix",
|
||||
"pre-commit-hooks"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1708704632,
|
||||
"narHash": "sha256-w+dOIW60FKMaHI1q5714CSibk99JfYxm0CzTinYWr+Q=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "2ee4450b0f4b95a1b90f2eb5ffea98b90e48c196",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "python-rewrite",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1673956053,
|
||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat_2": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1725024810,
|
||||
"narHash": "sha256-ODYRm8zHfLTH3soTFWE452ydPYz2iTvr9T8ftDMUQ3E=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "af510d4a62d071ea13925ce41c95e3dec816c01d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1689068808,
|
||||
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_3": {
|
||||
"inputs": {
|
||||
"systems": "systems_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"pre-commit-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"cachix",
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-regression": "nixpkgs-regression"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712911606,
|
||||
"narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=",
|
||||
"owner": "domenkozar",
|
||||
"repo": "nix",
|
||||
"rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "domenkozar",
|
||||
"ref": "devenv-2.21",
|
||||
"repo": "nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-github-actions": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"cachix",
|
||||
"devenv",
|
||||
"poetry2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1688870561,
|
||||
"narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-github-actions",
|
||||
"rev": "165b1650b753316aa7f1787f3005a8d2da0f5301",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-github-actions",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix2container": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_3",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1724996935,
|
||||
"narHash": "sha256-njRK9vvZ1JJsP8oV2OgkBrpJhgQezI03S7gzskCcHos=",
|
||||
"owner": "nlewo",
|
||||
"repo": "nix2container",
|
||||
"rev": "fa6bb0a1159f55d071ba99331355955ae30b3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nlewo",
|
||||
"repo": "nix2container",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix_2": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"devenv",
|
||||
"flake-compat"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-regression": "nixpkgs-regression_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712911606,
|
||||
"narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=",
|
||||
"owner": "domenkozar",
|
||||
"repo": "nix",
|
||||
"rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "domenkozar",
|
||||
"ref": "devenv-2.21",
|
||||
"repo": "nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1692808169,
|
||||
"narHash": "sha256-x9Opq06rIiwdwGeK2Ykj69dNc2IvUH1fY55Wm7atwrE=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9201b5ff357e781bf014d0330d18555695df7ba8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-qt6": {
|
||||
"locked": {
|
||||
"lastModified": 1718428119,
|
||||
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-regression": {
|
||||
"locked": {
|
||||
"lastModified": 1643052045,
|
||||
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-regression_2": {
|
||||
"locked": {
|
||||
"lastModified": 1643052045,
|
||||
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1710695816,
|
||||
"narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "614b4613980a522ba49f0d194531beddbb7220d3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-23.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1713361204,
|
||||
"narHash": "sha256-TA6EDunWTkc5FvDCqU3W2T3SFn0gRZqh6D/hJnM02MM=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"rev": "285676e87ad9f0ca23d8714a6ab61e7e027020c6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "rolling",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1724819573,
|
||||
"narHash": "sha256-GnR7/ibgIH1vhoy8cYdmXE6iyZqKqFxQSVkFgosBh6w=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "71e91c409d1e654808b2621f28a327acfdad8dc2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"poetry2nix": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nix-github-actions": "nix-github-actions",
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"cachix",
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1692876271,
|
||||
"narHash": "sha256-IXfZEkI0Mal5y1jr6IRWMqK8GW2/f28xJenZIPQqkY0=",
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"rev": "d5006be9c2c2417dafb2e2e5034d83fabd207ee3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pre-commit-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"devenv",
|
||||
"flake-compat"
|
||||
],
|
||||
"flake-utils": "flake-utils_2",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-stable": "nixpkgs-stable"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1713775815,
|
||||
"narHash": "sha256-Wu9cdYTnGQQwtT20QQMg7jzkANKQjwBD9iccfGKkfls=",
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"rev": "2ac4dcbf55ed43f3be0bae15e181f08a57af24a4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"devenv-root": "devenv-root",
|
||||
"flake-parts": "flake-parts",
|
||||
"nix2container": "nix2container",
|
||||
"nixpkgs": "nixpkgs_3",
|
||||
"nixpkgs-qt6": "nixpkgs-qt6",
|
||||
"systems": "systems_4"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_3": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_4": {
|
||||
"locked": {
|
||||
"lastModified": 1689347949,
|
||||
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default-linux",
|
||||
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default-linux",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
208
flake.nix
Normal file
@@ -0,0 +1,208 @@
|
||||
{
|
||||
description = "TagStudio";
|
||||
|
||||
inputs = {
|
||||
devenv.url = "github:cachix/devenv";
|
||||
|
||||
devenv-root = {
|
||||
url = "file+file:///dev/null";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
flake-parts = {
|
||||
url = "github:hercules-ci/flake-parts";
|
||||
inputs.nixpkgs-lib.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
nix2container = {
|
||||
url = "github:nlewo/nix2container";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
|
||||
# Pinned to Qt version 6.7.1
|
||||
nixpkgs-qt6.url = "github:NixOS/nixpkgs/e6cea36f83499eb4e9cd184c8a8e823296b50ad5";
|
||||
|
||||
systems.url = "github:nix-systems/default-linux";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
flake-parts,
|
||||
nixpkgs,
|
||||
nixpkgs-qt6,
|
||||
self,
|
||||
systems,
|
||||
...
|
||||
}@inputs:
|
||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
imports = [ inputs.devenv.flakeModule ];
|
||||
|
||||
systems = import systems;
|
||||
|
||||
perSystem =
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
system,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (nixpkgs) lib;
|
||||
|
||||
qt6Pkgs = import nixpkgs-qt6 { inherit system; };
|
||||
in
|
||||
{
|
||||
formatter = pkgs.nixfmt-rfc-style;
|
||||
|
||||
devenv.shells = rec {
|
||||
default = tagstudio;
|
||||
|
||||
tagstudio =
|
||||
let
|
||||
cfg = config.devenv.shells.tagstudio;
|
||||
in
|
||||
{
|
||||
# NOTE: many things were simply transferred over from previous,
|
||||
# there must be additional work in ensuring all relevant dependencies
|
||||
# are in place (and no extraneous). I have already spent much
|
||||
# work making this in the first place and just need to get it out
|
||||
# there, especially after my promises. Would appreciate any help
|
||||
# (possibly PRs!) on taking care of this. Otherwise, just expect
|
||||
# this to get ironed out over time.
|
||||
#
|
||||
# Thank you! -Xarvex
|
||||
|
||||
devenv.root =
|
||||
let
|
||||
devenvRoot = builtins.readFile inputs.devenv-root.outPath;
|
||||
in
|
||||
# If not overriden (/dev/null), --impure is necessary.
|
||||
pkgs.lib.mkIf (devenvRoot != "") devenvRoot;
|
||||
|
||||
name = "TagStudio";
|
||||
|
||||
# Derived from previous flake iteration.
|
||||
packages =
|
||||
(with pkgs; [
|
||||
cmake
|
||||
binutils
|
||||
coreutils
|
||||
dbus
|
||||
fontconfig
|
||||
freetype
|
||||
gdb
|
||||
glib
|
||||
libGL
|
||||
libGLU
|
||||
libgcc
|
||||
libxkbcommon
|
||||
mypy
|
||||
ruff
|
||||
xorg.libxcb
|
||||
zstd
|
||||
])
|
||||
++ (with qt6Pkgs; [
|
||||
qt6.full
|
||||
qt6.qtbase
|
||||
qt6.qtwayland
|
||||
qtcreator
|
||||
]);
|
||||
|
||||
enterShell =
|
||||
let
|
||||
setQtEnv =
|
||||
pkgs.runCommand "set-qt-env"
|
||||
{
|
||||
buildInputs = with qt6Pkgs.qt6; [
|
||||
qtbase
|
||||
];
|
||||
|
||||
nativeBuildInputs =
|
||||
(with pkgs; [
|
||||
makeShellWrapper
|
||||
])
|
||||
++ (with qt6Pkgs.qt6; [
|
||||
wrapQtAppsHook
|
||||
]);
|
||||
}
|
||||
''
|
||||
makeShellWrapper "$(type -p sh)" "$out" "''${qtWrapperArgs[@]}"
|
||||
sed "/^exec/d" -i "$out"
|
||||
'';
|
||||
in
|
||||
''
|
||||
source ${setQtEnv}
|
||||
'';
|
||||
|
||||
scripts.tagstudio.exec = ''
|
||||
python ${cfg.devenv.root}/tagstudio/tag_studio.py
|
||||
'';
|
||||
|
||||
env = {
|
||||
QT_QPA_PLATFORM = "wayland;xcb";
|
||||
|
||||
# Derived from previous flake iteration.
|
||||
# Not desired given LD_LIBRARY_PATH pollution.
|
||||
# See supposed alternative below, further research required.
|
||||
LD_LIBRARY_PATH = lib.makeLibraryPath (
|
||||
(with pkgs; [
|
||||
dbus
|
||||
fontconfig
|
||||
freetype
|
||||
gcc-unwrapped
|
||||
glib
|
||||
libglvnd
|
||||
libkrb5
|
||||
libpulseaudio
|
||||
libva
|
||||
libxkbcommon
|
||||
openssl
|
||||
stdenv.cc.cc.lib
|
||||
wayland
|
||||
xorg.libxcb
|
||||
xorg.libXrandr
|
||||
zlib
|
||||
zstd
|
||||
])
|
||||
++ (with qt6Pkgs.qt6; [
|
||||
qtbase
|
||||
qtwayland
|
||||
full
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
languages.python = {
|
||||
enable = true;
|
||||
venv = {
|
||||
enable = true;
|
||||
quiet = true;
|
||||
requirements =
|
||||
let
|
||||
excludeDeps =
|
||||
req: deps:
|
||||
builtins.concatStringsSep "\n" (
|
||||
builtins.filter (line: !(lib.any (elem: lib.hasPrefix elem line) deps)) (lib.splitString "\n" req)
|
||||
);
|
||||
in
|
||||
''
|
||||
${builtins.readFile ./requirements.txt}
|
||||
${excludeDeps (builtins.readFile ./requirements-dev.txt) [
|
||||
"mypy"
|
||||
"ruff"
|
||||
]}
|
||||
'';
|
||||
};
|
||||
|
||||
# Should be able to replace LD_LIBRARY_PATH?
|
||||
# Was not quite able to get working,
|
||||
# will be consulting cachix community. -Xarvex
|
||||
# libraries = with pkgs; [ ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
9
pyproject.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[tool.ruff]
|
||||
exclude = ["main_window.py", "home_ui.py", "resources.py", "resources_rc.py", "**/vendored/"]
|
||||
|
||||
[tool.mypy]
|
||||
strict_optional = false
|
||||
disable_error_code = ["union-attr", "annotation-unchecked", "import-untyped"]
|
||||
explicit_package_bases = true
|
||||
warn_unused_ignores = true
|
||||
exclude = ['tests', 'src/qt/helpers/vendored']
|
||||
6
requirements-dev.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
ruff==0.4.2
|
||||
pre-commit==3.7.0
|
||||
pytest==8.2.0
|
||||
Pyinstaller==6.6.0
|
||||
mypy==1.10.0
|
||||
syrupy==4.6.1
|
||||
@@ -1,12 +1,18 @@
|
||||
click==8.1.3
|
||||
climage==0.1.3
|
||||
humanfriendly==10.0
|
||||
opencv_python==4.8.0.74
|
||||
opencv_python>=4.8.0.74,<=4.9.0.80
|
||||
Pillow==10.3.0
|
||||
pillow_avif_plugin==1.3.1
|
||||
PySide6==6.5.1.1
|
||||
PySide6_Addons==6.5.1.1
|
||||
PySide6_Essentials==6.5.1.1
|
||||
Requests==2.31.0
|
||||
typing_extensions==3.10.0.0
|
||||
ujson==5.8.0
|
||||
PySide6==6.7.1
|
||||
PySide6_Addons==6.7.1
|
||||
PySide6_Essentials==6.7.1
|
||||
typing_extensions>=3.10.0.0,<=4.11.0
|
||||
ujson>=5.8.0,<=5.9.0
|
||||
numpy==1.26.4
|
||||
rawpy==0.21.0
|
||||
pillow-heif==0.16.0
|
||||
chardet==5.2.0
|
||||
pydub==0.25.1
|
||||
mutagen==1.47.0
|
||||
numpy==1.26.4
|
||||
ffmpeg-python==0.2.0
|
||||
Send2Trash==1.8.3
|
||||
vtf2img==0.1.0
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
@echo off
|
||||
.venv\Scripts\python.exe .\TagStudio\tagstudio.py --ui qt %*
|
||||
.venv\Scripts\python.exe .\TagStudio\tag_studio.py --ui qt %*
|
||||
86
tagstudio.spec
Normal file
@@ -0,0 +1,86 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
# vi: ft=python
|
||||
|
||||
|
||||
from argparse import ArgumentParser
|
||||
import sys
|
||||
from PyInstaller.building.api import COLLECT, EXE, PYZ
|
||||
from PyInstaller.building.build_main import Analysis
|
||||
from PyInstaller.building.osx import BUNDLE
|
||||
|
||||
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('--portable', action='store_true')
|
||||
options = parser.parse_args()
|
||||
|
||||
|
||||
name = 'TagStudio' if sys.platform == 'win32' else 'tagstudio'
|
||||
icon = None
|
||||
if sys.platform == 'win32':
|
||||
icon = 'tagstudio/resources/icon.ico'
|
||||
elif sys.platform == 'darwin':
|
||||
icon = 'tagstudio/resources/icon.icns'
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['tagstudio/tag_studio.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('tagstudio/resources', 'resources'), ('tagstudio/src', 'src')],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
excludes=[],
|
||||
runtime_hooks=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
include = [a.scripts]
|
||||
if options.portable:
|
||||
include += (a.binaries, a.datas)
|
||||
exe = EXE(
|
||||
pyz,
|
||||
*include,
|
||||
[],
|
||||
bootloader_ignore_signals=False,
|
||||
console=False,
|
||||
hide_console='hide-early',
|
||||
disable_windowed_traceback=False,
|
||||
debug=False,
|
||||
name=name,
|
||||
exclude_binaries=not options.portable,
|
||||
icon=icon,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None
|
||||
)
|
||||
|
||||
coll = None if options.portable else COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
name=name,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
)
|
||||
|
||||
app = BUNDLE(
|
||||
exe if coll is None else coll,
|
||||
name='TagStudio.app',
|
||||
icon=icon,
|
||||
bundle_identifier='com.github.tagstudiodev',
|
||||
version='0.0.0',
|
||||
info_plist={
|
||||
'NSAppleScriptEnabled': False,
|
||||
'NSPrincipalClass': 'NSApplication',
|
||||
}
|
||||
)
|
||||
BIN
tagstudio/resources/icon.icns
Normal file
|
Before Width: | Height: | Size: 628 KiB After Width: | Height: | Size: 361 KiB |
|
Before Width: | Height: | Size: 992 KiB After Width: | Height: | Size: 677 KiB |
BIN
tagstudio/resources/qt/images/broken_link_icon.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
tagstudio/resources/qt/images/file_icons/adobe_illustrator.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
tagstudio/resources/qt/images/file_icons/adobe_photoshop.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
tagstudio/resources/qt/images/file_icons/affinity_photo.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
tagstudio/resources/qt/images/file_icons/audio.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
tagstudio/resources/qt/images/file_icons/document.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
tagstudio/resources/qt/images/file_icons/file_generic.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
tagstudio/resources/qt/images/file_icons/font.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
tagstudio/resources/qt/images/file_icons/image.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
tagstudio/resources/qt/images/file_icons/image_vector.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
tagstudio/resources/qt/images/file_icons/material.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
tagstudio/resources/qt/images/file_icons/model.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
tagstudio/resources/qt/images/file_icons/presentation.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
tagstudio/resources/qt/images/file_icons/program.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
tagstudio/resources/qt/images/file_icons/spreadsheet.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
tagstudio/resources/qt/images/file_icons/text.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
tagstudio/resources/qt/images/file_icons/video.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
1
tagstudio/resources/qt/images/pause.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="#ffffff"><path d="M560-200v-560h160v560H560Zm-320 0v-560h160v560H240Z"/></svg>
|
||||
|
After Width: | Height: | Size: 172 B |
1
tagstudio/resources/qt/images/play.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="#ffffff"><path d="M320-200v-560l440 280-440 280Z"/></svg>
|
||||
|
After Width: | Height: | Size: 151 B |
|
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 397 KiB |
BIN
tagstudio/resources/qt/images/tagstudio_logo_text_mono.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 24 KiB |
BIN
tagstudio/resources/qt/images/thumb_loading.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
1
tagstudio/resources/qt/images/volume.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="#ffffff"><path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm440 40v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320Z"/></svg>
|
||||
|
After Width: | Height: | Size: 327 B |
1
tagstudio/resources/qt/images/volume_mute.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="#ffffff"><path d="M792-56 671-177q-25 16-53 27.5T560-131v-82q14-5 27.5-10t25.5-12L480-368v208L280-360H120v-240h128L56-792l56-56 736 736-56 56Zm-8-232-58-58q17-31 25.5-65t8.5-70q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 53-14.5 102T784-288ZM650-422l-90-90v-130q47 22 73.5 66t26.5 96q0 15-2.5 29.5T650-422ZM480-592 376-696l104-104v208Z"/></svg>
|
||||
|
After Width: | Height: | Size: 445 B |
BIN
tagstudio/resources/qt/videos/placeholder.mp4
Normal file
59
tagstudio/src/core/constants.py
Normal file
@@ -0,0 +1,59 @@
|
||||
VERSION: str = "9.4.2" # Major.Minor.Patch
|
||||
VERSION_BRANCH: str = "" # Usually "" or "Pre-Release"
|
||||
|
||||
# The folder & file names where TagStudio keeps its data relative to a library.
|
||||
TS_FOLDER_NAME: str = ".TagStudio"
|
||||
BACKUP_FOLDER_NAME: str = "backups"
|
||||
COLLAGE_FOLDER_NAME: str = "collages"
|
||||
LIBRARY_FILENAME: str = "ts_library.json"
|
||||
|
||||
FONT_SAMPLE_TEXT: str = (
|
||||
"""ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?@$%(){}[]"""
|
||||
)
|
||||
FONT_SAMPLE_SIZES: list[int] = [10, 15, 20]
|
||||
|
||||
BOX_FIELDS = ["tag_box", "text_box"]
|
||||
TEXT_FIELDS = ["text_line", "text_box"]
|
||||
DATE_FIELDS = ["datetime"]
|
||||
|
||||
TAG_COLORS = [
|
||||
"",
|
||||
"black",
|
||||
"dark gray",
|
||||
"gray",
|
||||
"light gray",
|
||||
"white",
|
||||
"light pink",
|
||||
"pink",
|
||||
"red",
|
||||
"red orange",
|
||||
"orange",
|
||||
"yellow orange",
|
||||
"yellow",
|
||||
"lime",
|
||||
"light green",
|
||||
"mint",
|
||||
"green",
|
||||
"teal",
|
||||
"cyan",
|
||||
"light blue",
|
||||
"blue",
|
||||
"blue violet",
|
||||
"violet",
|
||||
"purple",
|
||||
"lavender",
|
||||
"berry",
|
||||
"magenta",
|
||||
"salmon",
|
||||
"auburn",
|
||||
"dark brown",
|
||||
"brown",
|
||||
"light brown",
|
||||
"blonde",
|
||||
"peach",
|
||||
"warm gray",
|
||||
"cool gray",
|
||||
"olive",
|
||||
]
|
||||
TAG_FAVORITE = 1
|
||||
TAG_ARCHIVED = 0
|
||||
41
tagstudio/src/core/enums.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import enum
|
||||
|
||||
|
||||
class SettingItems(str, enum.Enum):
|
||||
"""List of setting item names."""
|
||||
|
||||
START_LOAD_LAST = "start_load_last"
|
||||
LAST_LIBRARY = "last_library"
|
||||
LIBS_LIST = "libs_list"
|
||||
WINDOW_SHOW_LIBS = "window_show_libs"
|
||||
AUTOPLAY = "autoplay_videos"
|
||||
|
||||
|
||||
class Theme(str, enum.Enum):
|
||||
COLOR_BG_DARK = "#65000000"
|
||||
COLOR_BG_LIGHT = "#22000000"
|
||||
COLOR_DARK_LABEL = "#DD000000"
|
||||
COLOR_HOVER = "#65AAAAAA"
|
||||
COLOR_PRESSED = "#65EEEEEE"
|
||||
COLOR_DISABLED = "#65F39CAA"
|
||||
COLOR_DISABLED_BG = "#65440D12"
|
||||
|
||||
|
||||
class SearchMode(int, enum.Enum):
|
||||
"""Operational modes for item searching."""
|
||||
|
||||
AND = 0
|
||||
OR = 1
|
||||
|
||||
|
||||
class FieldID(int, enum.Enum):
|
||||
TITLE = 0
|
||||
AUTHOR = 1
|
||||
ARTIST = 2
|
||||
DESCRIPTION = 4
|
||||
NOTES = 5
|
||||
TAGS = 6
|
||||
CONTENT_TAGS = 7
|
||||
META_TAGS = 8
|
||||
DATE_PUBLISHED = 14
|
||||
SOURCE = 21
|
||||
@@ -2,6 +2,7 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
class FieldTemplate:
|
||||
"""A TagStudio Library Field Template object."""
|
||||
|
||||
@@ -11,17 +12,17 @@ class FieldTemplate:
|
||||
self.type = type
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'\nID: {self.id}\nName: {self.name}\nType: {self.type}\n'
|
||||
return f"\nID: {self.id}\nName: {self.name}\nType: {self.type}\n"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
def to_compressed_obj(self) -> dict:
|
||||
"""An alternative to __dict__ that only includes fields containing non-default data."""
|
||||
obj = {}
|
||||
obj: dict = {}
|
||||
# All Field fields (haha) are mandatory, so no value checks are done.
|
||||
obj['id'] = self.id
|
||||
obj['name'] = self.name
|
||||
obj['type'] = self.type
|
||||
obj["id"] = self.id
|
||||
obj["name"] = self.name
|
||||
obj["type"] = self.type
|
||||
|
||||
return obj
|
||||
|
||||
42
tagstudio/src/core/json_typing.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from typing import TypedDict
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
|
||||
class JsonLibary(TypedDict("", {"ts-version": str})):
|
||||
# "ts-version": str
|
||||
tags: "list[JsonTag]"
|
||||
collations: "list[JsonCollation]"
|
||||
fields: list # TODO
|
||||
macros: "list[JsonMacro]"
|
||||
entries: "list[JsonEntry]"
|
||||
ext_list: list[str]
|
||||
is_exclude_list: bool
|
||||
ignored_extensions: NotRequired[list[str]] # deprecated
|
||||
|
||||
|
||||
class JsonBase(TypedDict):
|
||||
id: int
|
||||
|
||||
|
||||
class JsonTag(JsonBase, total=False):
|
||||
name: str
|
||||
aliases: list[str]
|
||||
color: str
|
||||
shorthand: str
|
||||
subtag_ids: list[int]
|
||||
|
||||
|
||||
class JsonCollation(JsonBase, total=False):
|
||||
title: str
|
||||
e_ids_and_pages: list[list[int]]
|
||||
sort_order: str
|
||||
cover_id: int
|
||||
|
||||
|
||||
class JsonEntry(JsonBase, total=False):
|
||||
filename: str
|
||||
path: str
|
||||
fields: list[dict] # TODO
|
||||
|
||||
|
||||
class JsonMacro(JsonBase, total=False): ... # TODO
|
||||
486
tagstudio/src/core/media_types.py
Normal file
@@ -0,0 +1,486 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
logging.basicConfig(format="%(message)s", level=logging.INFO)
|
||||
|
||||
|
||||
class MediaType(str, Enum):
|
||||
"""Names of media types."""
|
||||
|
||||
ADOBE_PHOTOSHOP: str = "adobe_photoshop"
|
||||
AFFINITY_PHOTO: str = "affinity_photo"
|
||||
ARCHIVE: str = "archive"
|
||||
AUDIO_MIDI: str = "audio_midi"
|
||||
AUDIO: str = "audio"
|
||||
BLENDER: str = "blender"
|
||||
DATABASE: str = "database"
|
||||
DISK_IMAGE: str = "disk_image"
|
||||
DOCUMENT: str = "document"
|
||||
FONT: str = "font"
|
||||
IMAGE_ANIMATED: str = "image_animated"
|
||||
IMAGE_RAW: str = "image_raw"
|
||||
IMAGE_VECTOR: str = "image_vector"
|
||||
IMAGE: str = "image"
|
||||
INSTALLER: str = "installer"
|
||||
MATERIAL: str = "material"
|
||||
MODEL: str = "model"
|
||||
PACKAGE: str = "package"
|
||||
PDF: str = "pdf"
|
||||
PLAINTEXT: str = "plaintext"
|
||||
PRESENTATION: str = "presentation"
|
||||
PROGRAM: str = "program"
|
||||
SHORTCUT: str = "shortcut"
|
||||
SOURCE_ENGINE: str = "source_engine"
|
||||
SPREADSHEET: str = "spreadsheet"
|
||||
TEXT: str = "text"
|
||||
VIDEO: str = "video"
|
||||
|
||||
|
||||
class MediaCategory:
|
||||
"""An object representing a category of media. Includes a MediaType identifier,
|
||||
extensions set, and IANA status flag.
|
||||
|
||||
Args:
|
||||
media_type (MediaType): The MediaType Enum representing this category.
|
||||
|
||||
extensions (set[str]): The set of file extensions associated with this category.
|
||||
Includes leading ".", all lowercase, and does not need to be unique to this category.
|
||||
|
||||
is_iana (bool): Represents whether or not this is an IANA registered category.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
media_type: MediaType,
|
||||
extensions: set[str],
|
||||
is_iana: bool = False,
|
||||
) -> None:
|
||||
self.media_type: MediaType = media_type
|
||||
self.extensions: set[str] = extensions
|
||||
self.is_iana: bool = is_iana
|
||||
|
||||
|
||||
class MediaCategories:
|
||||
"""Contains pre-made MediaCategory objects as well as methods to interact with them."""
|
||||
|
||||
# These sets are used either individually or together to form the final sets
|
||||
# for the MediaCategory(s).
|
||||
# These sets may be combined and are NOT 1:1 with the final categories.
|
||||
_ADOBE_PHOTOSHOP_SET: set[str] = {
|
||||
".pdd",
|
||||
".psb",
|
||||
".psd",
|
||||
}
|
||||
_AFFINITY_PHOTO_SET: set[str] = {".afphoto"}
|
||||
_ARCHIVE_SET: set[str] = {
|
||||
".7z",
|
||||
".gz",
|
||||
".rar",
|
||||
".s7z",
|
||||
".tar",
|
||||
".tgz",
|
||||
".zip",
|
||||
}
|
||||
_AUDIO_MIDI_SET: set[str] = {
|
||||
".mid",
|
||||
".midi",
|
||||
}
|
||||
_AUDIO_SET: set[str] = {
|
||||
".aac",
|
||||
".aif",
|
||||
".aiff",
|
||||
".alac",
|
||||
".flac",
|
||||
".m4a",
|
||||
".m4p",
|
||||
".mp3",
|
||||
".mpeg4",
|
||||
".ogg",
|
||||
".wav",
|
||||
".wma",
|
||||
}
|
||||
_BLENDER_SET: set[str] = {
|
||||
".blen_tc",
|
||||
".blend",
|
||||
".blend1",
|
||||
".blend10",
|
||||
".blend11",
|
||||
".blend12",
|
||||
".blend13",
|
||||
".blend14",
|
||||
".blend15",
|
||||
".blend16",
|
||||
".blend17",
|
||||
".blend18",
|
||||
".blend19",
|
||||
".blend2",
|
||||
".blend20",
|
||||
".blend21",
|
||||
".blend22",
|
||||
".blend23",
|
||||
".blend24",
|
||||
".blend25",
|
||||
".blend26",
|
||||
".blend27",
|
||||
".blend28",
|
||||
".blend29",
|
||||
".blend3",
|
||||
".blend30",
|
||||
".blend31",
|
||||
".blend32",
|
||||
".blend4",
|
||||
".blend5",
|
||||
".blend6",
|
||||
".blend7",
|
||||
".blend8",
|
||||
".blend9",
|
||||
}
|
||||
_DATABASE_SET: set[str] = {
|
||||
".accdb",
|
||||
".mdb",
|
||||
".sqlite",
|
||||
}
|
||||
_DISK_IMAGE_SET: set[str] = {".bios", ".dmg", ".iso"}
|
||||
_DOCUMENT_SET: set[str] = {
|
||||
".doc",
|
||||
".docm",
|
||||
".docx",
|
||||
".dot",
|
||||
".dotm",
|
||||
".dotx",
|
||||
".odt",
|
||||
".pages",
|
||||
".pdf",
|
||||
".rtf",
|
||||
".tex",
|
||||
".wpd",
|
||||
".wps",
|
||||
}
|
||||
_FONT_SET: set[str] = {
|
||||
".fon",
|
||||
".otf",
|
||||
".ttc",
|
||||
".ttf",
|
||||
".woff",
|
||||
".woff2",
|
||||
}
|
||||
_IMAGE_ANIMATED_SET: set[str] = {
|
||||
".apng",
|
||||
".gif",
|
||||
".webp",
|
||||
".jxl",
|
||||
}
|
||||
_IMAGE_RAW_SET: set[str] = {
|
||||
".arw",
|
||||
".cr2",
|
||||
".cr3",
|
||||
".crw",
|
||||
".dng",
|
||||
".nef",
|
||||
".orf",
|
||||
".raf",
|
||||
".raw",
|
||||
".rw2",
|
||||
}
|
||||
_IMAGE_VECTOR_SET: set[str] = {".svg"}
|
||||
_IMAGE_SET: set[str] = {
|
||||
".apng",
|
||||
".avif",
|
||||
".bmp",
|
||||
".exr",
|
||||
".gif",
|
||||
".heic",
|
||||
".heif",
|
||||
".j2k",
|
||||
".jfif",
|
||||
".jp2",
|
||||
".jpeg_large",
|
||||
".jpeg",
|
||||
".jpg_large",
|
||||
".jpg",
|
||||
".jpg2",
|
||||
".jxl",
|
||||
".png",
|
||||
".psb",
|
||||
".psd",
|
||||
".tif",
|
||||
".tiff",
|
||||
".webp",
|
||||
}
|
||||
_INSTALLER_SET: set[str] = {".appx", ".msi", ".msix"}
|
||||
_MATERIAL_SET: set[str] = {".mtl"}
|
||||
_MODEL_SET: set[str] = {".3ds", ".fbx", ".obj", ".stl"}
|
||||
_PACKAGE_SET: set[str] = {
|
||||
".aab",
|
||||
".akp",
|
||||
".apk",
|
||||
".apkm",
|
||||
".apks",
|
||||
".pkg",
|
||||
".xapk",
|
||||
}
|
||||
_PDF_SET: set[str] = {
|
||||
".pdf",
|
||||
}
|
||||
_PLAINTEXT_SET: set[str] = {
|
||||
".bat",
|
||||
".css",
|
||||
".csv",
|
||||
".htm",
|
||||
".html",
|
||||
".ini",
|
||||
".js",
|
||||
".json",
|
||||
".jsonc",
|
||||
".md",
|
||||
".php",
|
||||
".plist",
|
||||
".prefs",
|
||||
".sh",
|
||||
".ts",
|
||||
".txt",
|
||||
".xml",
|
||||
".vmt",
|
||||
".fgd",
|
||||
".nut",
|
||||
".cfg",
|
||||
".conf",
|
||||
".vdf",
|
||||
".vcfg",
|
||||
".gi",
|
||||
".inf",
|
||||
".vqlayout",
|
||||
".qss",
|
||||
".vsc",
|
||||
".kv3",
|
||||
".vsnd_template",
|
||||
}
|
||||
_PRESENTATION_SET: set[str] = {
|
||||
".key",
|
||||
".odp",
|
||||
".ppt",
|
||||
".pptx",
|
||||
}
|
||||
_PROGRAM_SET: set[str] = {".app", ".exe"}
|
||||
_SOURCE_ENGINE_SET: set[str] = {
|
||||
".vtf",
|
||||
}
|
||||
_SHORTCUT_SET: set[str] = {".desktop", ".lnk", ".url"}
|
||||
_SPREADSHEET_SET: set[str] = {
|
||||
".csv",
|
||||
".numbers",
|
||||
".ods",
|
||||
".xls",
|
||||
".xlsx",
|
||||
}
|
||||
_VIDEO_SET: set[str] = {
|
||||
".3gp",
|
||||
".avi",
|
||||
".flv",
|
||||
".gifv",
|
||||
".hevc",
|
||||
".m4p",
|
||||
".m4v",
|
||||
".mkv",
|
||||
".mov",
|
||||
".mp4",
|
||||
".webm",
|
||||
".wmv",
|
||||
}
|
||||
|
||||
ADOBE_PHOTOSHOP_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.ADOBE_PHOTOSHOP,
|
||||
extensions=_ADOBE_PHOTOSHOP_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
AFFINITY_PHOTO_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.AFFINITY_PHOTO,
|
||||
extensions=_AFFINITY_PHOTO_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
ARCHIVE_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.ARCHIVE,
|
||||
extensions=_ARCHIVE_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
AUDIO_MIDI_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.AUDIO_MIDI,
|
||||
extensions=_AUDIO_MIDI_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
AUDIO_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.AUDIO,
|
||||
extensions=_AUDIO_SET | _AUDIO_MIDI_SET,
|
||||
is_iana=True,
|
||||
)
|
||||
BLENDER_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.BLENDER,
|
||||
extensions=_BLENDER_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
DATABASE_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.DATABASE,
|
||||
extensions=_DATABASE_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
DISK_IMAGE_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.DISK_IMAGE,
|
||||
extensions=_DISK_IMAGE_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
DOCUMENT_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.DOCUMENT,
|
||||
extensions=_DOCUMENT_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
FONT_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.FONT,
|
||||
extensions=_FONT_SET,
|
||||
is_iana=True,
|
||||
)
|
||||
IMAGE_ANIMATED_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.IMAGE_ANIMATED,
|
||||
extensions=_IMAGE_ANIMATED_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
IMAGE_RAW_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.IMAGE_RAW,
|
||||
extensions=_IMAGE_RAW_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
IMAGE_VECTOR_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.IMAGE_VECTOR,
|
||||
extensions=_IMAGE_VECTOR_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
IMAGE_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.IMAGE,
|
||||
extensions=_IMAGE_SET | _IMAGE_RAW_SET | _IMAGE_VECTOR_SET,
|
||||
is_iana=True,
|
||||
)
|
||||
INSTALLER_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.INSTALLER,
|
||||
extensions=_INSTALLER_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
MATERIAL_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.MATERIAL,
|
||||
extensions=_MATERIAL_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
MODEL_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.MODEL,
|
||||
extensions=_MODEL_SET,
|
||||
is_iana=True,
|
||||
)
|
||||
PACKAGE_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.PACKAGE,
|
||||
extensions=_PACKAGE_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
PDF_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.PDF,
|
||||
extensions=_PDF_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
PLAINTEXT_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.PLAINTEXT,
|
||||
extensions=_PLAINTEXT_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
PRESENTATION_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.PRESENTATION,
|
||||
extensions=_PRESENTATION_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
PROGRAM_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.PROGRAM,
|
||||
extensions=_PROGRAM_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
SHORTCUT_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.SHORTCUT,
|
||||
extensions=_SHORTCUT_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
SOURCE_ENGINE_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.SOURCE_ENGINE,
|
||||
extensions=_SOURCE_ENGINE_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
SPREADSHEET_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.SPREADSHEET,
|
||||
extensions=_SPREADSHEET_SET,
|
||||
is_iana=False,
|
||||
)
|
||||
TEXT_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.TEXT,
|
||||
extensions=_DOCUMENT_SET | _PLAINTEXT_SET,
|
||||
is_iana=True,
|
||||
)
|
||||
VIDEO_TYPES: MediaCategory = MediaCategory(
|
||||
media_type=MediaType.VIDEO,
|
||||
extensions=_VIDEO_SET,
|
||||
is_iana=True,
|
||||
)
|
||||
|
||||
ALL_CATEGORIES: list[MediaCategory] = [
|
||||
ADOBE_PHOTOSHOP_TYPES,
|
||||
AFFINITY_PHOTO_TYPES,
|
||||
ARCHIVE_TYPES,
|
||||
AUDIO_MIDI_TYPES,
|
||||
AUDIO_TYPES,
|
||||
BLENDER_TYPES,
|
||||
DATABASE_TYPES,
|
||||
DISK_IMAGE_TYPES,
|
||||
DOCUMENT_TYPES,
|
||||
FONT_TYPES,
|
||||
IMAGE_ANIMATED_TYPES,
|
||||
IMAGE_RAW_TYPES,
|
||||
IMAGE_TYPES,
|
||||
IMAGE_VECTOR_TYPES,
|
||||
INSTALLER_TYPES,
|
||||
MATERIAL_TYPES,
|
||||
MODEL_TYPES,
|
||||
PACKAGE_TYPES,
|
||||
PDF_TYPES,
|
||||
PLAINTEXT_TYPES,
|
||||
PRESENTATION_TYPES,
|
||||
PROGRAM_TYPES,
|
||||
SHORTCUT_TYPES,
|
||||
SOURCE_ENGINE_TYPES,
|
||||
SPREADSHEET_TYPES,
|
||||
TEXT_TYPES,
|
||||
VIDEO_TYPES,
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_types(ext: str, mime_fallback: bool = False) -> set[MediaType]:
|
||||
"""Returns a set of MediaTypes given a file extension.
|
||||
|
||||
Args:
|
||||
ext (str): File extension with a leading "." and in all lowercase.
|
||||
mime_fallback (bool): Flag to guess MIME type if no set matches are made.
|
||||
"""
|
||||
types: set[MediaType] = set()
|
||||
mime_guess: bool = False
|
||||
|
||||
for cat in MediaCategories.ALL_CATEGORIES:
|
||||
if ext in cat.extensions:
|
||||
types.add(cat.media_type)
|
||||
elif mime_fallback and cat.is_iana:
|
||||
type: str = mimetypes.guess_type(Path("x" + ext), strict=False)[0]
|
||||
if type and type.startswith(cat.media_type.value):
|
||||
types.add(cat.media_type)
|
||||
mime_guess = True
|
||||
|
||||
# logging.info(
|
||||
# f"({ext}) Media Categories Found: {[x.value for x in types]}{' (MIME)' if mime_guess else ''}"
|
||||
# )
|
||||
return types
|
||||
@@ -5,7 +5,7 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ColorType(Enum):
|
||||
class ColorType(int, Enum):
|
||||
PRIMARY = 0
|
||||
TEXT = 1
|
||||
BORDER = 2
|
||||
@@ -13,240 +13,322 @@ class ColorType(Enum):
|
||||
DARK_ACCENT = 4
|
||||
|
||||
|
||||
_TAG_COLORS = {
|
||||
'': {ColorType.PRIMARY: '#1E1A33',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#2B2547',
|
||||
ColorType.LIGHT_ACCENT: '#CDA7F7',
|
||||
ColorType.DARK_ACCENT: '#1E1A33',
|
||||
},
|
||||
'black': {ColorType.PRIMARY: '#111018',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#18171e',
|
||||
ColorType.LIGHT_ACCENT: '#b7b6be',
|
||||
ColorType.DARK_ACCENT: '#03020a',
|
||||
},
|
||||
'dark gray': {ColorType.PRIMARY: '#24232a',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#2a2930',
|
||||
ColorType.LIGHT_ACCENT: '#bdbcc4',
|
||||
ColorType.DARK_ACCENT: '#07060e',
|
||||
},
|
||||
'gray': {ColorType.PRIMARY: '#53525a',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#5b5a62',
|
||||
ColorType.LIGHT_ACCENT: '#cbcad2',
|
||||
ColorType.DARK_ACCENT: '#191820',
|
||||
},
|
||||
'light gray': {ColorType.PRIMARY: '#aaa9b0',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#b6b4bc',
|
||||
ColorType.LIGHT_ACCENT: '#cbcad2',
|
||||
ColorType.DARK_ACCENT: '#191820',
|
||||
},
|
||||
'white': {ColorType.PRIMARY: '#f2f1f8',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#fefeff',
|
||||
ColorType.LIGHT_ACCENT: '#ffffff',
|
||||
ColorType.DARK_ACCENT: '#302f36',
|
||||
},
|
||||
'light pink': {ColorType.PRIMARY: '#ff99c4',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#ffaad0',
|
||||
ColorType.LIGHT_ACCENT: '#ffcbe7',
|
||||
ColorType.DARK_ACCENT: '#6c2e3b',
|
||||
},
|
||||
'pink': {ColorType.PRIMARY: '#ff99c4',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#ffaad0',
|
||||
ColorType.LIGHT_ACCENT: '#ffcbe7',
|
||||
ColorType.DARK_ACCENT: '#6c2e3b',
|
||||
},
|
||||
'magenta': {ColorType.PRIMARY: '#f6466f',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#f7587f',
|
||||
ColorType.LIGHT_ACCENT: '#fba4bf',
|
||||
ColorType.DARK_ACCENT: '#61152f',
|
||||
},
|
||||
'red': {ColorType.PRIMARY: '#e22c3c',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#b21f2d',
|
||||
# ColorType.BORDER: '#e54252',
|
||||
ColorType.LIGHT_ACCENT: '#f39caa',
|
||||
ColorType.DARK_ACCENT: '#440d12',
|
||||
},
|
||||
'red orange': {ColorType.PRIMARY: '#e83726',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#ea4b3b',
|
||||
ColorType.LIGHT_ACCENT: '#f5a59d',
|
||||
ColorType.DARK_ACCENT: '#61120b',
|
||||
},
|
||||
'salmon': {ColorType.PRIMARY: '#f65848',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#f76c5f',
|
||||
ColorType.LIGHT_ACCENT: '#fcadaa',
|
||||
ColorType.DARK_ACCENT: '#6f1b16',
|
||||
},
|
||||
'orange': {ColorType.PRIMARY: '#ed6022',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#ef7038',
|
||||
ColorType.LIGHT_ACCENT: '#f7b79b',
|
||||
ColorType.DARK_ACCENT: '#551e0a',
|
||||
},
|
||||
'yellow orange': {ColorType.PRIMARY: '#fa9a2c',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#fba94b',
|
||||
ColorType.LIGHT_ACCENT: '#fdd7ab',
|
||||
ColorType.DARK_ACCENT: '#66330d',
|
||||
},
|
||||
'yellow': {ColorType.PRIMARY: '#ffd63d',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
# ColorType.BORDER: '#ffe071',
|
||||
ColorType.BORDER: '#e8af31',
|
||||
ColorType.LIGHT_ACCENT: '#fff3c4',
|
||||
ColorType.DARK_ACCENT: '#754312',
|
||||
},
|
||||
'mint': {ColorType.PRIMARY: '#4aed90',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#79f2b1',
|
||||
ColorType.LIGHT_ACCENT: '#c8fbe9',
|
||||
ColorType.DARK_ACCENT: '#164f3e',
|
||||
},
|
||||
'lime': {ColorType.PRIMARY: '#92e649',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#b2ed72',
|
||||
ColorType.LIGHT_ACCENT: '#e9f9b7',
|
||||
ColorType.DARK_ACCENT: '#405516',
|
||||
},
|
||||
'light green': {ColorType.PRIMARY: '#85ec76',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#a3f198',
|
||||
ColorType.LIGHT_ACCENT: '#e7fbe4',
|
||||
ColorType.DARK_ACCENT: '#2b5524',
|
||||
},
|
||||
'green': {ColorType.PRIMARY: '#28bb48',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#43c568',
|
||||
ColorType.LIGHT_ACCENT: '#93e2c8',
|
||||
ColorType.DARK_ACCENT: '#0d3828',
|
||||
},
|
||||
'teal': {ColorType.PRIMARY: '#1ad9b2',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#4de3c7',
|
||||
ColorType.LIGHT_ACCENT: '#a0f3e8',
|
||||
ColorType.DARK_ACCENT: '#08424b',
|
||||
},
|
||||
'cyan': {ColorType.PRIMARY: '#49e4d5',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#76ebdf',
|
||||
ColorType.LIGHT_ACCENT: '#bff5f0',
|
||||
ColorType.DARK_ACCENT: '#0f4246',
|
||||
},
|
||||
'light blue': {ColorType.PRIMARY: '#55bbf6',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#70c6f7',
|
||||
ColorType.LIGHT_ACCENT: '#bbe4fb',
|
||||
ColorType.DARK_ACCENT: '#122541',
|
||||
},
|
||||
'blue': {ColorType.PRIMARY: '#3b87f0',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#4e95f2',
|
||||
ColorType.LIGHT_ACCENT: '#aedbfa',
|
||||
ColorType.DARK_ACCENT: '#122948',
|
||||
},
|
||||
'blue violet': {ColorType.PRIMARY: '#5948f2',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#6258f3',
|
||||
ColorType.LIGHT_ACCENT: '#9cb8fb',
|
||||
ColorType.DARK_ACCENT: '#1b1649',
|
||||
},
|
||||
'violet': {ColorType.PRIMARY: '#874ff5',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#9360f6',
|
||||
ColorType.LIGHT_ACCENT: '#c9b0fa',
|
||||
ColorType.DARK_ACCENT: '#3a1860',
|
||||
},
|
||||
'purple': {ColorType.PRIMARY: '#bb4ff0',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#c364f2',
|
||||
ColorType.LIGHT_ACCENT: '#dda7f7',
|
||||
ColorType.DARK_ACCENT: '#531862',
|
||||
},
|
||||
'peach': {ColorType.PRIMARY: '#f1c69c',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#f4d4b4',
|
||||
ColorType.LIGHT_ACCENT: '#fbeee1',
|
||||
ColorType.DARK_ACCENT: '#613f2f',
|
||||
},
|
||||
'brown': {ColorType.PRIMARY: '#823216',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#8a3e22',
|
||||
ColorType.LIGHT_ACCENT: '#cd9d83',
|
||||
ColorType.DARK_ACCENT: '#3a1804',
|
||||
},
|
||||
'lavender': {ColorType.PRIMARY: '#ad8eef',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#b99ef2',
|
||||
ColorType.LIGHT_ACCENT: '#d5c7fa',
|
||||
ColorType.DARK_ACCENT: '#492b65',
|
||||
},
|
||||
'blonde': {ColorType.PRIMARY: '#efc664',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#f3d387',
|
||||
ColorType.LIGHT_ACCENT: '#faebc6',
|
||||
ColorType.DARK_ACCENT: '#6d461e',
|
||||
},
|
||||
'auburn': {ColorType.PRIMARY: '#a13220',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#aa402f',
|
||||
ColorType.LIGHT_ACCENT: '#d98a7f',
|
||||
ColorType.DARK_ACCENT: '#3d100a',
|
||||
},
|
||||
'light brown': {ColorType.PRIMARY: '#be5b2d',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#c4693d',
|
||||
ColorType.LIGHT_ACCENT: '#e5b38c',
|
||||
ColorType.DARK_ACCENT: '#4c290e',
|
||||
},
|
||||
'dark brown': {ColorType.PRIMARY: '#4c2315',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#542a1c',
|
||||
ColorType.LIGHT_ACCENT: '#b78171',
|
||||
ColorType.DARK_ACCENT: '#211006',
|
||||
},
|
||||
'cool gray': {ColorType.PRIMARY: '#515768',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#5b6174',
|
||||
ColorType.LIGHT_ACCENT: '#9ea1c3',
|
||||
ColorType.DARK_ACCENT: '#181a37',
|
||||
},
|
||||
'warm gray': {ColorType.PRIMARY: '#625550',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#6c5e57',
|
||||
ColorType.LIGHT_ACCENT: '#c0a392',
|
||||
ColorType.DARK_ACCENT: '#371d18',
|
||||
},
|
||||
'olive': {ColorType.PRIMARY: '#4c652e',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#586f36',
|
||||
ColorType.LIGHT_ACCENT: '#b4c17a',
|
||||
ColorType.DARK_ACCENT: '#23300e',
|
||||
},
|
||||
'berry': {ColorType.PRIMARY: '#9f2aa7',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#aa43b4',
|
||||
ColorType.LIGHT_ACCENT: '#cc8fdc',
|
||||
ColorType.DARK_ACCENT: '#41114a',
|
||||
},
|
||||
_TAG_COLORS: dict = {
|
||||
"": {
|
||||
ColorType.PRIMARY: "#1e1e1e",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#333333",
|
||||
ColorType.LIGHT_ACCENT: "#FFFFFF",
|
||||
ColorType.DARK_ACCENT: "#222222",
|
||||
},
|
||||
"black": {
|
||||
ColorType.PRIMARY: "#111018",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#18171e",
|
||||
ColorType.LIGHT_ACCENT: "#b7b6be",
|
||||
ColorType.DARK_ACCENT: "#03020a",
|
||||
},
|
||||
"dark gray": {
|
||||
ColorType.PRIMARY: "#24232a",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#2a2930",
|
||||
ColorType.LIGHT_ACCENT: "#bdbcc4",
|
||||
ColorType.DARK_ACCENT: "#07060e",
|
||||
},
|
||||
"gray": {
|
||||
ColorType.PRIMARY: "#53525a",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#5b5a62",
|
||||
ColorType.LIGHT_ACCENT: "#cbcad2",
|
||||
ColorType.DARK_ACCENT: "#191820",
|
||||
},
|
||||
"light gray": {
|
||||
ColorType.PRIMARY: "#aaa9b0",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#b6b4bc",
|
||||
ColorType.LIGHT_ACCENT: "#cbcad2",
|
||||
ColorType.DARK_ACCENT: "#191820",
|
||||
},
|
||||
"white": {
|
||||
ColorType.PRIMARY: "#f2f1f8",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#fefeff",
|
||||
ColorType.LIGHT_ACCENT: "#ffffff",
|
||||
ColorType.DARK_ACCENT: "#302f36",
|
||||
},
|
||||
"light pink": {
|
||||
ColorType.PRIMARY: "#ff99c4",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#ffaad0",
|
||||
ColorType.LIGHT_ACCENT: "#ffcbe7",
|
||||
ColorType.DARK_ACCENT: "#6c2e3b",
|
||||
},
|
||||
"pink": {
|
||||
ColorType.PRIMARY: "#ff99c4",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#ffaad0",
|
||||
ColorType.LIGHT_ACCENT: "#ffcbe7",
|
||||
ColorType.DARK_ACCENT: "#6c2e3b",
|
||||
},
|
||||
"magenta": {
|
||||
ColorType.PRIMARY: "#f6466f",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#f7587f",
|
||||
ColorType.LIGHT_ACCENT: "#fba4bf",
|
||||
ColorType.DARK_ACCENT: "#61152f",
|
||||
},
|
||||
"red": {
|
||||
ColorType.PRIMARY: "#e22c3c",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#b21f2d",
|
||||
# ColorType.BORDER: '#e54252',
|
||||
ColorType.LIGHT_ACCENT: "#f39caa",
|
||||
ColorType.DARK_ACCENT: "#440d12",
|
||||
},
|
||||
"red orange": {
|
||||
ColorType.PRIMARY: "#e83726",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#ea4b3b",
|
||||
ColorType.LIGHT_ACCENT: "#f5a59d",
|
||||
ColorType.DARK_ACCENT: "#61120b",
|
||||
},
|
||||
"salmon": {
|
||||
ColorType.PRIMARY: "#f65848",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#f76c5f",
|
||||
ColorType.LIGHT_ACCENT: "#fcadaa",
|
||||
ColorType.DARK_ACCENT: "#6f1b16",
|
||||
},
|
||||
"orange": {
|
||||
ColorType.PRIMARY: "#ed6022",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#ef7038",
|
||||
ColorType.LIGHT_ACCENT: "#f7b79b",
|
||||
ColorType.DARK_ACCENT: "#551e0a",
|
||||
},
|
||||
"yellow orange": {
|
||||
ColorType.PRIMARY: "#fa9a2c",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#fba94b",
|
||||
ColorType.LIGHT_ACCENT: "#fdd7ab",
|
||||
ColorType.DARK_ACCENT: "#66330d",
|
||||
},
|
||||
"yellow": {
|
||||
ColorType.PRIMARY: "#ffd63d",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
# ColorType.BORDER: '#ffe071',
|
||||
ColorType.BORDER: "#e8af31",
|
||||
ColorType.LIGHT_ACCENT: "#fff3c4",
|
||||
ColorType.DARK_ACCENT: "#754312",
|
||||
},
|
||||
"mint": {
|
||||
ColorType.PRIMARY: "#4aed90",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#79f2b1",
|
||||
ColorType.LIGHT_ACCENT: "#c8fbe9",
|
||||
ColorType.DARK_ACCENT: "#164f3e",
|
||||
},
|
||||
"lime": {
|
||||
ColorType.PRIMARY: "#92e649",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#b2ed72",
|
||||
ColorType.LIGHT_ACCENT: "#e9f9b7",
|
||||
ColorType.DARK_ACCENT: "#405516",
|
||||
},
|
||||
"light green": {
|
||||
ColorType.PRIMARY: "#85ec76",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#a3f198",
|
||||
ColorType.LIGHT_ACCENT: "#e7fbe4",
|
||||
ColorType.DARK_ACCENT: "#2b5524",
|
||||
},
|
||||
"green": {
|
||||
ColorType.PRIMARY: "#28bb48",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#43c568",
|
||||
ColorType.LIGHT_ACCENT: "#93e2c8",
|
||||
ColorType.DARK_ACCENT: "#0d3828",
|
||||
},
|
||||
"teal": {
|
||||
ColorType.PRIMARY: "#1ad9b2",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#4de3c7",
|
||||
ColorType.LIGHT_ACCENT: "#a0f3e8",
|
||||
ColorType.DARK_ACCENT: "#08424b",
|
||||
},
|
||||
"cyan": {
|
||||
ColorType.PRIMARY: "#49e4d5",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#76ebdf",
|
||||
ColorType.LIGHT_ACCENT: "#bff5f0",
|
||||
ColorType.DARK_ACCENT: "#0f4246",
|
||||
},
|
||||
"light blue": {
|
||||
ColorType.PRIMARY: "#55bbf6",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#70c6f7",
|
||||
ColorType.LIGHT_ACCENT: "#bbe4fb",
|
||||
ColorType.DARK_ACCENT: "#122541",
|
||||
},
|
||||
"blue": {
|
||||
ColorType.PRIMARY: "#3b87f0",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#4e95f2",
|
||||
ColorType.LIGHT_ACCENT: "#aedbfa",
|
||||
ColorType.DARK_ACCENT: "#122948",
|
||||
},
|
||||
"blue violet": {
|
||||
ColorType.PRIMARY: "#5948f2",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#6258f3",
|
||||
ColorType.LIGHT_ACCENT: "#9cb8fb",
|
||||
ColorType.DARK_ACCENT: "#1b1649",
|
||||
},
|
||||
"violet": {
|
||||
ColorType.PRIMARY: "#874ff5",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#9360f6",
|
||||
ColorType.LIGHT_ACCENT: "#c9b0fa",
|
||||
ColorType.DARK_ACCENT: "#3a1860",
|
||||
},
|
||||
"purple": {
|
||||
ColorType.PRIMARY: "#bb4ff0",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#c364f2",
|
||||
ColorType.LIGHT_ACCENT: "#dda7f7",
|
||||
ColorType.DARK_ACCENT: "#531862",
|
||||
},
|
||||
"peach": {
|
||||
ColorType.PRIMARY: "#f1c69c",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#f4d4b4",
|
||||
ColorType.LIGHT_ACCENT: "#fbeee1",
|
||||
ColorType.DARK_ACCENT: "#613f2f",
|
||||
},
|
||||
"brown": {
|
||||
ColorType.PRIMARY: "#823216",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#8a3e22",
|
||||
ColorType.LIGHT_ACCENT: "#cd9d83",
|
||||
ColorType.DARK_ACCENT: "#3a1804",
|
||||
},
|
||||
"lavender": {
|
||||
ColorType.PRIMARY: "#ad8eef",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#b99ef2",
|
||||
ColorType.LIGHT_ACCENT: "#d5c7fa",
|
||||
ColorType.DARK_ACCENT: "#492b65",
|
||||
},
|
||||
"blonde": {
|
||||
ColorType.PRIMARY: "#efc664",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#f3d387",
|
||||
ColorType.LIGHT_ACCENT: "#faebc6",
|
||||
ColorType.DARK_ACCENT: "#6d461e",
|
||||
},
|
||||
"auburn": {
|
||||
ColorType.PRIMARY: "#a13220",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#aa402f",
|
||||
ColorType.LIGHT_ACCENT: "#d98a7f",
|
||||
ColorType.DARK_ACCENT: "#3d100a",
|
||||
},
|
||||
"light brown": {
|
||||
ColorType.PRIMARY: "#be5b2d",
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: "#c4693d",
|
||||
ColorType.LIGHT_ACCENT: "#e5b38c",
|
||||
ColorType.DARK_ACCENT: "#4c290e",
|
||||
},
|
||||
"dark brown": {
|
||||
ColorType.PRIMARY: "#4c2315",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#542a1c",
|
||||
ColorType.LIGHT_ACCENT: "#b78171",
|
||||
ColorType.DARK_ACCENT: "#211006",
|
||||
},
|
||||
"cool gray": {
|
||||
ColorType.PRIMARY: "#515768",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#5b6174",
|
||||
ColorType.LIGHT_ACCENT: "#9ea1c3",
|
||||
ColorType.DARK_ACCENT: "#181a37",
|
||||
},
|
||||
"warm gray": {
|
||||
ColorType.PRIMARY: "#625550",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#6c5e57",
|
||||
ColorType.LIGHT_ACCENT: "#c0a392",
|
||||
ColorType.DARK_ACCENT: "#371d18",
|
||||
},
|
||||
"olive": {
|
||||
ColorType.PRIMARY: "#4c652e",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#586f36",
|
||||
ColorType.LIGHT_ACCENT: "#b4c17a",
|
||||
ColorType.DARK_ACCENT: "#23300e",
|
||||
},
|
||||
"berry": {
|
||||
ColorType.PRIMARY: "#9f2aa7",
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: "#aa43b4",
|
||||
ColorType.LIGHT_ACCENT: "#cc8fdc",
|
||||
ColorType.DARK_ACCENT: "#41114a",
|
||||
},
|
||||
}
|
||||
|
||||
_UI_COLORS: dict = {
|
||||
"": {
|
||||
ColorType.PRIMARY: "#333333",
|
||||
ColorType.BORDER: "#555555",
|
||||
ColorType.LIGHT_ACCENT: "#FFFFFF",
|
||||
ColorType.DARK_ACCENT: "#1e1e1e",
|
||||
},
|
||||
"red": {
|
||||
ColorType.PRIMARY: "#e22c3c",
|
||||
ColorType.BORDER: "#e54252",
|
||||
ColorType.LIGHT_ACCENT: "#f39caa",
|
||||
ColorType.DARK_ACCENT: "#440d12",
|
||||
},
|
||||
"green": {
|
||||
ColorType.PRIMARY: "#28bb48",
|
||||
ColorType.BORDER: "#43c568",
|
||||
ColorType.LIGHT_ACCENT: "#DDFFCC",
|
||||
ColorType.DARK_ACCENT: "#0d3828",
|
||||
},
|
||||
"purple": {
|
||||
ColorType.PRIMARY: "#C76FF3",
|
||||
ColorType.BORDER: "#c364f2",
|
||||
ColorType.LIGHT_ACCENT: "#EFD4FB",
|
||||
ColorType.DARK_ACCENT: "#3E1555",
|
||||
},
|
||||
"theme_dark": {
|
||||
ColorType.PRIMARY: "#333333",
|
||||
ColorType.BORDER: "#555555",
|
||||
ColorType.LIGHT_ACCENT: "#FFFFFF",
|
||||
ColorType.DARK_ACCENT: "#1e1e1e",
|
||||
},
|
||||
"theme_light": {
|
||||
ColorType.PRIMARY: "#FFFFFF",
|
||||
ColorType.BORDER: "#333333",
|
||||
ColorType.LIGHT_ACCENT: "#999999",
|
||||
ColorType.DARK_ACCENT: "#888888",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_tag_color(type: ColorType, color: str):
|
||||
def get_tag_color(color_type, color):
|
||||
color = color.lower()
|
||||
try:
|
||||
if type == ColorType.TEXT:
|
||||
return get_tag_color(_TAG_COLORS[color][type], color)
|
||||
if color_type == ColorType.TEXT:
|
||||
return get_tag_color(_TAG_COLORS[color][color_type], color)
|
||||
else:
|
||||
return _TAG_COLORS[color][type]
|
||||
return _TAG_COLORS[color][color_type]
|
||||
except KeyError:
|
||||
return '#FF00FF'
|
||||
return "#FF00FF"
|
||||
|
||||
|
||||
def get_ui_color(color_type: ColorType, color: str):
|
||||
"""Returns a hex value given a color name and ColorType."""
|
||||
color = color.lower()
|
||||
return _UI_COLORS.get(color).get(color_type)
|
||||
|
||||
@@ -4,230 +4,208 @@
|
||||
|
||||
"""The core classes and methods of TagStudio."""
|
||||
|
||||
import os
|
||||
from types import FunctionType
|
||||
# from typing import Dict, Optional, TypedDict, List
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import traceback
|
||||
import requests
|
||||
# from bs4 import BeautifulSoup as bs
|
||||
from src.core.library import *
|
||||
from src.core.field_template import FieldTemplate
|
||||
from enum import Enum
|
||||
|
||||
VERSION: str = '9.1.0' # Major.Minor.Patch
|
||||
VERSION_BRANCH: str = 'Alpha' # 'Alpha', 'Beta', or '' for Full Release
|
||||
|
||||
# The folder & file names where TagStudio keeps its data relative to a library.
|
||||
TS_FOLDER_NAME: str = '.TagStudio'
|
||||
BACKUP_FOLDER_NAME: str = 'backups'
|
||||
COLLAGE_FOLDER_NAME: str = 'collages'
|
||||
LIBRARY_FILENAME: str = 'ts_library.json'
|
||||
|
||||
IMAGE_TYPES: list[str] = ['png', 'jpg', 'jpeg', 'jpg_large', 'jpeg_large',
|
||||
'jfif', 'gif', 'tif', 'tiff', 'heic', 'heif', 'webp',
|
||||
'bmp', 'svg', 'avif', 'apng', 'jp2', 'j2k', 'jpg2']
|
||||
VIDEO_TYPES: list[str] = ['mp4', 'webm', 'mov', 'hevc', 'mkv', 'avi', 'wmv',
|
||||
'flv', 'gifv', 'm4p', 'm4v', '3gp']
|
||||
AUDIO_TYPES: list[str] = ['mp3', 'mp4', 'mpeg4', 'm4a', 'aac', 'wav', 'flac',
|
||||
'alac', 'wma', 'ogg', 'aiff']
|
||||
TEXT_TYPES: list[str] = ['txt', 'rtf', 'md',
|
||||
'doc', 'docx', 'pdf', 'tex', 'odt', 'pages']
|
||||
SPREADSHEET_TYPES: list[str] = ['csv', 'xls', 'xlsx', 'numbers', 'ods']
|
||||
PRESENTATION_TYPES: list[str] = ['ppt', 'pptx', 'key', 'odp']
|
||||
ARCHIVE_TYPES: list[str] = ['zip', 'rar', 'tar', 'tar.gz', 'tgz', '7z']
|
||||
PROGRAM_TYPES: list[str] = ['exe', 'app']
|
||||
SHORTCUT_TYPES: list[str] = ['lnk', 'desktop']
|
||||
|
||||
ALL_FILE_TYPES: list[str] = IMAGE_TYPES + VIDEO_TYPES
|
||||
|
||||
BOX_FIELDS = ['tag_box', 'text_box']
|
||||
TEXT_FIELDS = ['text_line', 'text_box']
|
||||
DATE_FIELDS = ['datetime']
|
||||
|
||||
TAG_COLORS = ['', 'black', 'dark gray', 'gray', 'light gray', 'white', 'light pink',
|
||||
'pink', 'red', 'red orange', 'orange', 'yellow orange', 'yellow',
|
||||
'lime', 'light green', 'mint', 'green','teal', 'cyan', 'light blue',
|
||||
'blue', 'blue violet', 'violet', 'purple', 'lavender', 'berry',
|
||||
'magenta', 'salmon', 'auburn', 'dark brown', 'brown', 'light brown',
|
||||
'blonde', 'peach', 'warm gray', 'cool gray', 'olive']
|
||||
from src.core.library import Entry, Library
|
||||
from src.core.constants import TS_FOLDER_NAME, TEXT_FIELDS
|
||||
|
||||
|
||||
class TagStudioCore:
|
||||
"""
|
||||
Instantiate this to establish a TagStudio session.
|
||||
Holds all TagStudio session data and provides methods to manage it.
|
||||
"""
|
||||
"""
|
||||
Instantiate this to establish a TagStudio session.
|
||||
Holds all TagStudio session data and provides methods to manage it.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.lib: Library = Library()
|
||||
def __init__(self):
|
||||
self.lib: Library = Library()
|
||||
|
||||
def get_gdl_sidecar(self, filepath: str | Path, source: str = "") -> dict:
|
||||
"""
|
||||
Attempts to open and dump a Gallery-DL Sidecar sidecar file for
|
||||
the filepath.\n Returns a formatted object with notable values or an
|
||||
empty object if none is found.
|
||||
"""
|
||||
json_dump = {}
|
||||
info = {}
|
||||
_filepath: Path = Path(filepath)
|
||||
_filepath = _filepath.parent / (_filepath.name + ".json")
|
||||
|
||||
def get_gdl_sidecar(self, filepath: str, source: str = '') -> dict:
|
||||
"""
|
||||
Attempts to open and dump a Gallery-DL Sidecar sidecar file for
|
||||
the filepath.\n Returns a formatted object with notable values or an
|
||||
empty object if none is found.
|
||||
"""
|
||||
json_dump = {}
|
||||
info = {}
|
||||
# NOTE: This fixes an unknown (recent?) bug in Gallery-DL where Instagram sidecar
|
||||
# files may be downloaded with indices starting at 1 rather than 0, unlike the posts.
|
||||
# This may only occur with sidecar files that are downloaded separate from posts.
|
||||
if source == "instagram":
|
||||
if not _filepath.is_file():
|
||||
newstem = _filepath.stem[:-16] + "1" + _filepath.stem[-15:]
|
||||
_filepath = _filepath.parent / (newstem + ".json")
|
||||
|
||||
# NOTE: This fixes an unknown (recent?) bug in Gallery-DL where Instagram sidecar
|
||||
# files may be downloaded with indices starting at 1 rather than 0, unlike the posts.
|
||||
# This may only occur with sidecar files that are downloaded separate from posts.
|
||||
if source == 'instagram':
|
||||
if not os.path.isfile(os.path.normpath(filepath + ".json")):
|
||||
filepath = filepath[:-16] + '1' + filepath[-15:]
|
||||
try:
|
||||
with open(_filepath, "r", encoding="utf8") as f:
|
||||
json_dump = json.load(f)
|
||||
|
||||
try:
|
||||
with open(os.path.normpath(filepath + ".json"), "r", encoding="utf8") as f:
|
||||
json_dump = json.load(f)
|
||||
if json_dump:
|
||||
if source == "twitter":
|
||||
info["content"] = json_dump["content"].strip()
|
||||
info["date_published"] = json_dump["date"]
|
||||
elif source == "instagram":
|
||||
info["description"] = json_dump["description"].strip()
|
||||
info["date_published"] = json_dump["date"]
|
||||
elif source == "artstation":
|
||||
info["title"] = json_dump["title"].strip()
|
||||
info["artist"] = json_dump["user"]["full_name"].strip()
|
||||
info["description"] = json_dump["description"].strip()
|
||||
info["tags"] = json_dump["tags"]
|
||||
# info["tags"] = [x for x in json_dump["mediums"]["name"]]
|
||||
info["date_published"] = json_dump["date"]
|
||||
elif source == "newgrounds":
|
||||
# info["title"] = json_dump["title"]
|
||||
# info["artist"] = json_dump["artist"]
|
||||
# info["description"] = json_dump["description"]
|
||||
info["tags"] = json_dump["tags"]
|
||||
info["date_published"] = json_dump["date"]
|
||||
info["artist"] = json_dump["user"].strip()
|
||||
info["description"] = json_dump["description"].strip()
|
||||
info["source"] = json_dump["post_url"].strip()
|
||||
# else:
|
||||
# print(
|
||||
# f'[INFO]: TagStudio does not currently support sidecar files for "{source}"')
|
||||
|
||||
if json_dump:
|
||||
if source == "twitter":
|
||||
info["content"] = json_dump["content"].strip()
|
||||
info["date_published"] = json_dump["date"]
|
||||
elif source == "instagram":
|
||||
info["description"] = json_dump["description"].strip()
|
||||
info["date_published"] = json_dump["date"]
|
||||
elif source == "artstation":
|
||||
info["title"] = json_dump["title"].strip()
|
||||
info["artist"] = json_dump["user"]["full_name"].strip()
|
||||
info["description"] = json_dump["description"].strip()
|
||||
info["tags"] = json_dump["tags"]
|
||||
# info["tags"] = [x for x in json_dump["mediums"]["name"]]
|
||||
info["date_published"] = json_dump["date"]
|
||||
elif source == "newgrounds":
|
||||
# info["title"] = json_dump["title"]
|
||||
# info["artist"] = json_dump["artist"]
|
||||
# info["description"] = json_dump["description"]
|
||||
info["tags"] = json_dump["tags"]
|
||||
info["date_published"] = json_dump["date"]
|
||||
info["artist"] = json_dump["user"].strip()
|
||||
info["description"] = json_dump["description"].strip()
|
||||
info["source"] = json_dump["post_url"].strip()
|
||||
# else:
|
||||
# print(
|
||||
# f'[INFO]: TagStudio does not currently support sidecar files for "{source}"')
|
||||
# except FileNotFoundError:
|
||||
except:
|
||||
# print(
|
||||
# f'[INFO]: No sidecar file found at "{os.path.normpath(file_path + ".json")}"')
|
||||
pass
|
||||
|
||||
# except FileNotFoundError:
|
||||
except:
|
||||
# print(
|
||||
# f'[INFO]: No sidecar file found at "{os.path.normpath(file_path + ".json")}"')
|
||||
pass
|
||||
return info
|
||||
|
||||
return info
|
||||
# def scrape(self, entry_id):
|
||||
# entry = self.lib.get_entry(entry_id)
|
||||
# if entry.fields:
|
||||
# urls: list[str] = []
|
||||
# if self.lib.get_field_index_in_entry(entry, 21):
|
||||
# urls.extend([self.lib.get_field_attr(entry.fields[x], 'content')
|
||||
# for x in self.lib.get_field_index_in_entry(entry, 21)])
|
||||
# if self.lib.get_field_index_in_entry(entry, 3):
|
||||
# urls.extend([self.lib.get_field_attr(entry.fields[x], 'content')
|
||||
# for x in self.lib.get_field_index_in_entry(entry, 3)])
|
||||
# # try:
|
||||
# if urls:
|
||||
# for url in urls:
|
||||
# url = "https://" + url if 'https://' not in url else url
|
||||
# html_doc = requests.get(url).text
|
||||
# soup = bs(html_doc, "html.parser")
|
||||
# print(soup)
|
||||
# input()
|
||||
|
||||
# def scrape(self, entry_id):
|
||||
# entry = self.lib.get_entry(entry_id)
|
||||
# if entry.fields:
|
||||
# urls: list[str] = []
|
||||
# if self.lib.get_field_index_in_entry(entry, 21):
|
||||
# urls.extend([self.lib.get_field_attr(entry.fields[x], 'content')
|
||||
# for x in self.lib.get_field_index_in_entry(entry, 21)])
|
||||
# if self.lib.get_field_index_in_entry(entry, 3):
|
||||
# urls.extend([self.lib.get_field_attr(entry.fields[x], 'content')
|
||||
# for x in self.lib.get_field_index_in_entry(entry, 3)])
|
||||
# # try:
|
||||
# if urls:
|
||||
# for url in urls:
|
||||
# url = "https://" + url if 'https://' not in url else url
|
||||
# html_doc = requests.get(url).text
|
||||
# soup = bs(html_doc, "html.parser")
|
||||
# print(soup)
|
||||
# input()
|
||||
# # except:
|
||||
# # # print("Could not resolve URL.")
|
||||
# # pass
|
||||
|
||||
# # except:
|
||||
# # # print("Could not resolve URL.")
|
||||
# # pass
|
||||
def match_conditions(self, entry_id: int) -> None:
|
||||
"""Matches defined conditions against a file to add Entry data."""
|
||||
|
||||
def match_conditions(self, entry_id: int) -> str:
|
||||
"""Matches defined conditions against a file to add Entry data."""
|
||||
cond_file = self.lib.library_dir / TS_FOLDER_NAME / "conditions.json"
|
||||
# TODO: Make this stored somewhere better instead of temporarily in this JSON file.
|
||||
entry: Entry = self.lib.get_entry(entry_id)
|
||||
try:
|
||||
if cond_file.is_file():
|
||||
with open(cond_file, "r", encoding="utf8") as f:
|
||||
json_dump = json.load(f)
|
||||
for c in json_dump["conditions"]:
|
||||
match: bool = False
|
||||
for path_c in c["path_conditions"]:
|
||||
if str(Path(path_c).resolve()) in str(entry.path):
|
||||
match = True
|
||||
break
|
||||
if match:
|
||||
if fields := c.get("fields"):
|
||||
for field in fields:
|
||||
field_id = self.lib.get_field_attr(field, "id")
|
||||
content = field[field_id]
|
||||
|
||||
cond_file = os.path.normpath(f'{self.lib.library_dir}/{TS_FOLDER_NAME}/conditions.json')
|
||||
# TODO: Make this stored somewhere better instead of temporarily in this JSON file.
|
||||
json_dump = {}
|
||||
entry: Entry = self.lib.get_entry(entry_id)
|
||||
try:
|
||||
if os.path.isfile(cond_file):
|
||||
with open(cond_file, "r", encoding="utf8") as f:
|
||||
json_dump = json.load(f)
|
||||
for c in json_dump['conditions']:
|
||||
match: bool = False
|
||||
for path_c in c['path_conditions']:
|
||||
if os.path.normpath(path_c) in entry.path:
|
||||
match = True
|
||||
break
|
||||
if match:
|
||||
if 'fields' in c.keys() and c['fields']:
|
||||
for field in c['fields']:
|
||||
if (
|
||||
self.lib.get_field_obj(int(field_id))["type"]
|
||||
== "tag_box"
|
||||
):
|
||||
existing_fields: list[int] = (
|
||||
self.lib.get_field_index_in_entry(
|
||||
entry, field_id
|
||||
)
|
||||
)
|
||||
if existing_fields:
|
||||
self.lib.update_entry_field(
|
||||
entry_id,
|
||||
existing_fields[0],
|
||||
content,
|
||||
"append",
|
||||
)
|
||||
else:
|
||||
self.lib.add_field_to_entry(
|
||||
entry_id, field_id
|
||||
)
|
||||
self.lib.update_entry_field(
|
||||
entry_id, -1, content, "append"
|
||||
)
|
||||
|
||||
field_id = self.lib.get_field_attr(
|
||||
field, 'id')
|
||||
content = field[field_id]
|
||||
if (
|
||||
self.lib.get_field_obj(int(field_id))["type"]
|
||||
in TEXT_FIELDS
|
||||
):
|
||||
if not self.lib.does_field_content_exist(
|
||||
entry_id, field_id, content
|
||||
):
|
||||
self.lib.add_field_to_entry(
|
||||
entry_id, field_id
|
||||
)
|
||||
self.lib.update_entry_field(
|
||||
entry_id, -1, content, "replace"
|
||||
)
|
||||
except:
|
||||
print("Error in match_conditions...")
|
||||
# input()
|
||||
pass
|
||||
|
||||
if self.lib.get_field_obj(int(field_id))['type'] == 'tag_box':
|
||||
existing_fields: list[int] = self.lib.get_field_index_in_entry(
|
||||
entry, field_id)
|
||||
if existing_fields:
|
||||
self.lib.update_entry_field(
|
||||
entry_id, existing_fields[0], content, 'append')
|
||||
else:
|
||||
self.lib.add_field_to_entry(
|
||||
entry_id, field_id)
|
||||
self.lib.update_entry_field(
|
||||
entry_id, -1, content, 'append')
|
||||
def build_url(self, entry_id: int, source: str):
|
||||
"""Tries to rebuild a source URL given a specific filename structure."""
|
||||
|
||||
if self.lib.get_field_obj(int(field_id))['type'] in TEXT_FIELDS:
|
||||
if not self.lib.does_field_content_exist(entry_id, field_id, content):
|
||||
self.lib.add_field_to_entry(
|
||||
entry_id, field_id)
|
||||
self.lib.update_entry_field(
|
||||
entry_id, -1, content, 'replace')
|
||||
except:
|
||||
print('Error in match_conditions...')
|
||||
# input()
|
||||
pass
|
||||
source = source.lower().replace("-", " ").replace("_", " ")
|
||||
if "twitter" in source:
|
||||
return self._build_twitter_url(entry_id)
|
||||
elif "instagram" in source:
|
||||
return self._build_instagram_url(entry_id)
|
||||
|
||||
def build_url(self, entry_id: int, source: str) -> str:
|
||||
"""Tries to rebuild a source URL given a specific filename structure."""
|
||||
def _build_twitter_url(self, entry_id: int):
|
||||
"""
|
||||
Builds an Twitter URL given a specific filename structure.
|
||||
Method expects filename to be formatted as 'USERNAME_TWEET-ID_INDEX_YEAR-MM-DD'
|
||||
"""
|
||||
try:
|
||||
entry = self.lib.get_entry(entry_id)
|
||||
stubs = str(entry.filename).rsplit("_", 3)
|
||||
# print(stubs)
|
||||
# source, author = os.path.split(entry.path)
|
||||
url = f"www.twitter.com/{stubs[0]}/status/{stubs[-3]}/photo/{stubs[-2]}"
|
||||
return url
|
||||
except:
|
||||
return ""
|
||||
|
||||
source = source.lower().replace('-', ' ').replace('_', ' ')
|
||||
if 'twitter' in source:
|
||||
return self._build_twitter_url(entry_id)
|
||||
elif 'instagram' in source:
|
||||
return self._build_instagram_url(entry_id)
|
||||
|
||||
def _build_twitter_url(self, entry_id: int):
|
||||
"""
|
||||
Builds an Twitter URL given a specific filename structure.
|
||||
Method expects filename to be formatted as 'USERNAME_TWEET-ID_INDEX_YEAR-MM-DD'
|
||||
"""
|
||||
try:
|
||||
entry = self.lib.get_entry(entry_id)
|
||||
stubs = entry.filename.rsplit('_', 3)
|
||||
# print(stubs)
|
||||
# source, author = os.path.split(entry.path)
|
||||
url = f"www.twitter.com/{stubs[0]}/status/{stubs[-3]}/photo/{stubs[-2]}"
|
||||
return url
|
||||
except:
|
||||
return ''
|
||||
|
||||
def _build_instagram_url(self, entry_id: int):
|
||||
"""
|
||||
Builds an Instagram URL given a specific filename structure.
|
||||
Method expects filename to be formatted as 'USERNAME_POST-ID_INDEX_YEAR-MM-DD'
|
||||
"""
|
||||
try:
|
||||
entry = self.lib.get_entry(entry_id)
|
||||
stubs = entry.filename.rsplit('_', 2)
|
||||
# stubs[0] = stubs[0].replace(f"{author}_", '', 1)
|
||||
# print(stubs)
|
||||
# NOTE: Both Instagram usernames AND their ID can have underscores in them,
|
||||
# so unless you have the exact username (which can change) on hand to remove,
|
||||
# your other best bet is to hope that the ID is only 11 characters long, which
|
||||
# seems to more or less be the case... for now...
|
||||
url = f"www.instagram.com/p/{stubs[-3][-11:]}"
|
||||
return url
|
||||
except:
|
||||
return ''
|
||||
def _build_instagram_url(self, entry_id: int):
|
||||
"""
|
||||
Builds an Instagram URL given a specific filename structure.
|
||||
Method expects filename to be formatted as 'USERNAME_POST-ID_INDEX_YEAR-MM-DD'
|
||||
"""
|
||||
try:
|
||||
entry = self.lib.get_entry(entry_id)
|
||||
stubs = str(entry.filename).rsplit("_", 2)
|
||||
# stubs[0] = stubs[0].replace(f"{author}_", '', 1)
|
||||
# print(stubs)
|
||||
# NOTE: Both Instagram usernames AND their ID can have underscores in them,
|
||||
# so unless you have the exact username (which can change) on hand to remove,
|
||||
# your other best bet is to hope that the ID is only 11 characters long, which
|
||||
# seems to more or less be the case... for now...
|
||||
url = f"www.instagram.com/p/{stubs[-3][-11:]}"
|
||||
return url
|
||||
except:
|
||||
return ""
|
||||
|
||||
27
tagstudio/src/core/utils/encoding.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
from chardet.universaldetector import UniversalDetector
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def detect_char_encoding(filepath: Path) -> str | None:
|
||||
"""
|
||||
Attempts to detect the character encoding of a text file.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the text file to analyze.
|
||||
|
||||
Returns:
|
||||
str | None: The detected character encoding, if any.
|
||||
"""
|
||||
|
||||
detector = UniversalDetector()
|
||||
with open(filepath, "rb") as text_file:
|
||||
for line in text_file.readlines():
|
||||
detector.feed(line)
|
||||
if detector.done:
|
||||
break
|
||||
detector.close()
|
||||
return detector.result["encoding"]
|
||||
@@ -2,12 +2,10 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def clean_folder_name(folder_name: str) -> str:
|
||||
cleaned_name = folder_name
|
||||
invalid_chars = "<>:\"/\\|?*."
|
||||
invalid_chars = '<>:"/\\|?*.'
|
||||
for char in invalid_chars:
|
||||
cleaned_name = cleaned_name.replace(char, '_')
|
||||
cleaned_name = cleaned_name.replace(char, "_")
|
||||
return cleaned_name
|
||||
|
||||
@@ -2,10 +2,25 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
def strip_punctuation(string: str) -> str:
|
||||
"""Returns a given string stripped of all punctuation characters."""
|
||||
return string.replace('(', '').replace(')', '').replace('[', '') \
|
||||
.replace(']', '').replace('{', '').replace('}', '').replace("'", '') \
|
||||
.replace('`', '').replace('’', '').replace('‘', '').replace('"', '') \
|
||||
.replace('“', '').replace('”', '').replace('_', '').replace('-', '') \
|
||||
.replace(' ', '').replace(' ', '')
|
||||
return (
|
||||
string.replace("(", "")
|
||||
.replace(")", "")
|
||||
.replace("[", "")
|
||||
.replace("]", "")
|
||||
.replace("{", "")
|
||||
.replace("}", "")
|
||||
.replace("'", "")
|
||||
.replace("`", "")
|
||||
.replace("’", "")
|
||||
.replace("‘", "")
|
||||
.replace('"', "")
|
||||
.replace("“", "")
|
||||
.replace("”", "")
|
||||
.replace("_", "")
|
||||
.replace("-", "")
|
||||
.replace(" ", "")
|
||||
.replace(" ", "")
|
||||
)
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
def strip_web_protocol(string: str) -> str:
|
||||
"""Strips a leading web protocol (ex. \"https://\") as well as \"www.\" from a string."""
|
||||
new_str = string
|
||||
new_str = new_str.removeprefix('https://')
|
||||
new_str = new_str.removeprefix('http://')
|
||||
new_str = new_str.removeprefix('www.')
|
||||
new_str = new_str.removeprefix('www2.')
|
||||
return new_str
|
||||
prefixes = ["https://", "http://", "www.", "www2."]
|
||||
for prefix in prefixes:
|
||||
string = string.removeprefix(prefix)
|
||||
return string
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
|
||||
"""PySide6 port of the widgets/layouts/flowlayout example from Qt v6.x"""
|
||||
|
||||
import sys
|
||||
from PySide6.QtCore import Qt, QMargins, QPoint, QRect, QSize
|
||||
from PySide6.QtWidgets import QApplication, QLayout, QPushButton, QSizePolicy, QWidget
|
||||
from PySide6.QtWidgets import QLayout, QSizePolicy, QWidget
|
||||
|
||||
|
||||
# class Window(QWidget):
|
||||
@@ -22,143 +21,153 @@ from PySide6.QtWidgets import QApplication, QLayout, QPushButton, QSizePolicy, Q
|
||||
|
||||
# self.setWindowTitle("Flow Layout")
|
||||
|
||||
|
||||
class FlowWidget(QWidget):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.ignore_size: bool = False
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.ignore_size: bool = False
|
||||
|
||||
|
||||
class FlowLayout(QLayout):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
if parent is not None:
|
||||
self.setContentsMargins(QMargins(0, 0, 0, 0))
|
||||
if parent is not None:
|
||||
self.setContentsMargins(QMargins(0, 0, 0, 0))
|
||||
|
||||
self._item_list = []
|
||||
self.grid_efficiency = False
|
||||
self._item_list = []
|
||||
self.grid_efficiency = False
|
||||
|
||||
def __del__(self):
|
||||
item = self.takeAt(0)
|
||||
while item:
|
||||
item = self.takeAt(0)
|
||||
def __del__(self):
|
||||
item = self.takeAt(0)
|
||||
while item:
|
||||
item = self.takeAt(0)
|
||||
|
||||
def addItem(self, item):
|
||||
self._item_list.append(item)
|
||||
def addItem(self, item):
|
||||
self._item_list.append(item)
|
||||
|
||||
def count(self):
|
||||
return len(self._item_list)
|
||||
def count(self):
|
||||
return len(self._item_list)
|
||||
|
||||
def itemAt(self, index):
|
||||
if 0 <= index < len(self._item_list):
|
||||
return self._item_list[index]
|
||||
def itemAt(self, index):
|
||||
if 0 <= index < len(self._item_list):
|
||||
return self._item_list[index]
|
||||
|
||||
return None
|
||||
return None
|
||||
|
||||
def takeAt(self, index):
|
||||
if 0 <= index < len(self._item_list):
|
||||
return self._item_list.pop(index)
|
||||
def takeAt(self, index):
|
||||
if 0 <= index < len(self._item_list):
|
||||
return self._item_list.pop(index)
|
||||
|
||||
return None
|
||||
return None
|
||||
|
||||
def expandingDirections(self):
|
||||
return Qt.Orientation(0)
|
||||
def expandingDirections(self):
|
||||
return Qt.Orientation(0)
|
||||
|
||||
def hasHeightForWidth(self):
|
||||
return True
|
||||
def hasHeightForWidth(self):
|
||||
return True
|
||||
|
||||
def heightForWidth(self, width):
|
||||
height = self._do_layout(QRect(0, 0, width, 0), True)
|
||||
return height
|
||||
def heightForWidth(self, width):
|
||||
height = self._do_layout(QRect(0, 0, width, 0), True)
|
||||
return height
|
||||
|
||||
def setGeometry(self, rect):
|
||||
super(FlowLayout, self).setGeometry(rect)
|
||||
self._do_layout(rect, False)
|
||||
|
||||
def setGridEfficiency(self, bool):
|
||||
"""
|
||||
Enables or Disables efficiencies when all objects are equally sized.
|
||||
"""
|
||||
self.grid_efficiency = bool
|
||||
def setGeometry(self, rect):
|
||||
super(FlowLayout, self).setGeometry(rect)
|
||||
self._do_layout(rect, False)
|
||||
|
||||
def sizeHint(self):
|
||||
return self.minimumSize()
|
||||
def setGridEfficiency(self, bool):
|
||||
"""
|
||||
Enables or Disables efficiencies when all objects are equally sized.
|
||||
"""
|
||||
self.grid_efficiency = bool
|
||||
|
||||
def minimumSize(self):
|
||||
if self.grid_efficiency:
|
||||
if self._item_list:
|
||||
return self._item_list[0].minimumSize()
|
||||
else:
|
||||
return QSize()
|
||||
else:
|
||||
size = QSize()
|
||||
def sizeHint(self):
|
||||
return self.minimumSize()
|
||||
|
||||
for item in self._item_list:
|
||||
size = size.expandedTo(item.minimumSize())
|
||||
def minimumSize(self):
|
||||
if self.grid_efficiency:
|
||||
if self._item_list:
|
||||
return self._item_list[0].minimumSize()
|
||||
else:
|
||||
return QSize()
|
||||
else:
|
||||
size = QSize()
|
||||
|
||||
size += QSize(2 * self.contentsMargins().top(), 2 * self.contentsMargins().top())
|
||||
return size
|
||||
|
||||
|
||||
for item in self._item_list:
|
||||
size = size.expandedTo(item.minimumSize())
|
||||
|
||||
def _do_layout(self, rect, test_only):
|
||||
x = rect.x()
|
||||
y = rect.y()
|
||||
line_height = 0
|
||||
spacing = self.spacing()
|
||||
item = None
|
||||
style = None
|
||||
layout_spacing_x = None
|
||||
layout_spacing_y = None
|
||||
size += QSize(
|
||||
2 * self.contentsMargins().top(), 2 * self.contentsMargins().top()
|
||||
)
|
||||
return size
|
||||
|
||||
if self.grid_efficiency:
|
||||
if self._item_list:
|
||||
item = self._item_list[0]
|
||||
style = item.widget().style()
|
||||
layout_spacing_x = style.layoutSpacing(
|
||||
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
|
||||
)
|
||||
layout_spacing_y = style.layoutSpacing(
|
||||
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
|
||||
)
|
||||
for i, item in enumerate(self._item_list):
|
||||
# print(issubclass(type(item.widget()), FlowWidget))
|
||||
# print(item.widget().ignore_size)
|
||||
skip_count = 0
|
||||
if (issubclass(type(item.widget()), FlowWidget) and item.widget().ignore_size):
|
||||
skip_count += 1
|
||||
def _do_layout(self, rect, test_only):
|
||||
x = rect.x()
|
||||
y = rect.y()
|
||||
line_height = 0
|
||||
spacing = self.spacing()
|
||||
item = None
|
||||
style = None
|
||||
layout_spacing_x = None
|
||||
layout_spacing_y = None
|
||||
|
||||
if (issubclass(type(item.widget()), FlowWidget) and not item.widget().ignore_size) or (not issubclass(type(item.widget()), FlowWidget)):
|
||||
# print(f'Item {i}')
|
||||
if not self.grid_efficiency:
|
||||
style = item.widget().style()
|
||||
layout_spacing_x = style.layoutSpacing(
|
||||
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
|
||||
)
|
||||
layout_spacing_y = style.layoutSpacing(
|
||||
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
|
||||
)
|
||||
space_x = spacing + layout_spacing_x
|
||||
space_y = spacing + layout_spacing_y
|
||||
next_x = x + item.sizeHint().width() + space_x
|
||||
if next_x - space_x > rect.right() and line_height > 0:
|
||||
x = rect.x()
|
||||
y = y + line_height + space_y
|
||||
next_x = x + item.sizeHint().width() + space_x
|
||||
line_height = 0
|
||||
if self.grid_efficiency:
|
||||
if self._item_list:
|
||||
item = self._item_list[0]
|
||||
style = item.widget().style()
|
||||
layout_spacing_x = style.layoutSpacing(
|
||||
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
|
||||
)
|
||||
layout_spacing_y = style.layoutSpacing(
|
||||
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
|
||||
)
|
||||
for i, item in enumerate(self._item_list):
|
||||
# print(issubclass(type(item.widget()), FlowWidget))
|
||||
# print(item.widget().ignore_size)
|
||||
skip_count = 0
|
||||
if (
|
||||
issubclass(type(item.widget()), FlowWidget)
|
||||
and item.widget().ignore_size
|
||||
):
|
||||
skip_count += 1
|
||||
|
||||
if not test_only:
|
||||
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
|
||||
if (
|
||||
issubclass(type(item.widget()), FlowWidget)
|
||||
and not item.widget().ignore_size
|
||||
) or (not issubclass(type(item.widget()), FlowWidget)):
|
||||
# print(f'Item {i}')
|
||||
if not self.grid_efficiency:
|
||||
style = item.widget().style()
|
||||
layout_spacing_x = style.layoutSpacing(
|
||||
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
|
||||
)
|
||||
layout_spacing_y = style.layoutSpacing(
|
||||
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
|
||||
)
|
||||
space_x = spacing + layout_spacing_x
|
||||
space_y = spacing + layout_spacing_y
|
||||
next_x = x + item.sizeHint().width() + space_x
|
||||
if next_x - space_x > rect.right() and line_height > 0:
|
||||
x = rect.x()
|
||||
y = y + line_height + space_y
|
||||
next_x = x + item.sizeHint().width() + space_x
|
||||
line_height = 0
|
||||
|
||||
x = next_x
|
||||
line_height = max(line_height, item.sizeHint().height())
|
||||
if not test_only:
|
||||
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
|
||||
|
||||
# print(y + line_height - rect.y() * ((len(self._item_list) - skip_count) / len(self._item_list)))
|
||||
# print(y + line_height - rect.y()) * ((len(self._item_list) - skip_count) / len(self._item_list))
|
||||
return y + line_height - rect.y() * ((len(self._item_list)) / len(self._item_list))
|
||||
x = next_x
|
||||
line_height = max(line_height, item.sizeHint().height())
|
||||
|
||||
# print(y + line_height - rect.y() * ((len(self._item_list) - skip_count) / len(self._item_list)))
|
||||
# print(y + line_height - rect.y()) * ((len(self._item_list) - skip_count) / len(self._item_list))
|
||||
return (
|
||||
y + line_height - rect.y() * ((len(self._item_list)) / len(self._item_list))
|
||||
)
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# app = QApplication(sys.argv)
|
||||
# main_win = Window()
|
||||
# main_win.show()
|
||||
# sys.exit(app.exec())
|
||||
# sys.exit(app.exec())
|
||||
|
||||
109
tagstudio/src/qt/helpers/blender_thumbnailer.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
|
||||
## This file is a modified script that gets the thumbnail data stored in a blend file
|
||||
|
||||
|
||||
import struct
|
||||
from PIL import (
|
||||
Image,
|
||||
ImageOps,
|
||||
)
|
||||
import gzip
|
||||
import os
|
||||
|
||||
|
||||
def blend_extract_thumb(path):
|
||||
REND = b"REND"
|
||||
TEST = b"TEST"
|
||||
|
||||
blendfile = open(path, "rb")
|
||||
|
||||
head = blendfile.read(12)
|
||||
|
||||
if head[0:2] == b"\x1f\x8b": # gzip magic
|
||||
blendfile.close()
|
||||
blendfile = gzip.GzipFile("", "rb", 0, open(path, "rb"))
|
||||
head = blendfile.read(12)
|
||||
|
||||
if not head.startswith(b"BLENDER"):
|
||||
blendfile.close()
|
||||
return None, 0, 0
|
||||
|
||||
is_64_bit = head[7] == b"-"[0]
|
||||
|
||||
# true for PPC, false for X86
|
||||
is_big_endian = head[8] == b"V"[0]
|
||||
|
||||
# blender pre 2.5 had no thumbs
|
||||
if head[9:11] <= b"24":
|
||||
return None, 0, 0
|
||||
|
||||
sizeof_bhead = 24 if is_64_bit else 20
|
||||
int_endian = ">i" if is_big_endian else "<i"
|
||||
int_endian_pair = int_endian + "i"
|
||||
|
||||
while True:
|
||||
bhead = blendfile.read(sizeof_bhead)
|
||||
|
||||
if len(bhead) < sizeof_bhead:
|
||||
return None, 0, 0
|
||||
|
||||
code = bhead[:4]
|
||||
length = struct.unpack(int_endian, bhead[4:8])[0] # 4 == sizeof(int)
|
||||
|
||||
if code == REND:
|
||||
blendfile.seek(length, os.SEEK_CUR)
|
||||
else:
|
||||
break
|
||||
|
||||
if code != TEST:
|
||||
return None, 0, 0
|
||||
|
||||
try:
|
||||
x, y = struct.unpack(int_endian_pair, blendfile.read(8)) # 8 == sizeof(int) * 2
|
||||
except struct.error:
|
||||
return None, 0, 0
|
||||
|
||||
length -= 8 # sizeof(int) * 2
|
||||
|
||||
if length != x * y * 4:
|
||||
return None, 0, 0
|
||||
|
||||
image_buffer = blendfile.read(length)
|
||||
|
||||
if len(image_buffer) != length:
|
||||
return None, 0, 0
|
||||
|
||||
return image_buffer, x, y
|
||||
|
||||
|
||||
def blend_thumb(file_in):
|
||||
buf, width, height = blend_extract_thumb(file_in)
|
||||
image = Image.frombuffer(
|
||||
"RGBA",
|
||||
(width, height),
|
||||
buf,
|
||||
)
|
||||
image = ImageOps.flip(image)
|
||||
return image
|
||||
64
tagstudio/src/qt/helpers/color_overlay.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from PIL import Image
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QGuiApplication
|
||||
from src.qt.helpers.gradient import linear_gradient
|
||||
|
||||
# TODO: Consolidate the built-in QT theme values with the values
|
||||
# here, in enums.py, and in palette.py.
|
||||
_THEME_DARK_FG: str = "#FFFFFF77"
|
||||
_THEME_LIGHT_FG: str = "#000000DD"
|
||||
_THEME_DARK_BG: str = "#000000DD"
|
||||
_THEME_LIGHT_BG: str = "#FFFFFF55"
|
||||
|
||||
|
||||
def theme_fg_overlay(image: Image.Image, use_alpha: bool = True) -> Image.Image:
|
||||
"""
|
||||
Overlay the foreground theme color onto an image.
|
||||
|
||||
Args:
|
||||
image (Image): The PIL Image object to apply an overlay to.
|
||||
"""
|
||||
dark_fg: str = _THEME_DARK_FG[:-2] if not use_alpha else _THEME_DARK_FG
|
||||
light_fg: str = _THEME_LIGHT_FG[:-2] if not use_alpha else _THEME_LIGHT_FG
|
||||
|
||||
overlay_color = (
|
||||
dark_fg
|
||||
if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
|
||||
else light_fg
|
||||
)
|
||||
|
||||
im = Image.new(mode="RGBA", size=image.size, color=overlay_color)
|
||||
return _apply_overlay(image, im)
|
||||
|
||||
|
||||
def gradient_overlay(image: Image.Image, gradient=list[str]) -> Image.Image:
|
||||
"""
|
||||
Overlay a color gradient onto an image.
|
||||
|
||||
Args:
|
||||
image (Image): The PIL Image object to apply an overlay to.
|
||||
gradient (list[str): A list of string hex color codes for use as
|
||||
the colors of the gradient.
|
||||
"""
|
||||
|
||||
im: Image.Image = _apply_overlay(image, linear_gradient(image.size, gradient))
|
||||
return im
|
||||
|
||||
|
||||
def _apply_overlay(image: Image.Image, overlay: Image.Image) -> Image.Image:
|
||||
"""
|
||||
Internal method to apply an overlay on top of an image, using
|
||||
the image's alpha channel as a mask.
|
||||
|
||||
Args:
|
||||
image (Image): The PIL Image object to apply an overlay to.
|
||||
overlay (Image): The PIL Image object to act as the overlay contents.
|
||||
"""
|
||||
im: Image.Image = Image.new(mode="RGBA", size=image.size, color="#00000000")
|
||||
im.paste(overlay, (0, 0), mask=image)
|
||||
return im
|
||||
19
tagstudio/src/qt/helpers/custom_runnable.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from PySide6.QtCore import Signal, QRunnable, QObject
|
||||
|
||||
|
||||
class CustomRunnable(QRunnable, QObject):
|
||||
done = Signal()
|
||||
|
||||
def __init__(self, function) -> None:
|
||||
QRunnable.__init__(self)
|
||||
QObject.__init__(self)
|
||||
self.function = function
|
||||
|
||||
def run(self):
|
||||
self.function()
|
||||
self.done.emit()
|
||||
30
tagstudio/src/qt/helpers/file_deleter.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from send2trash import send2trash
|
||||
|
||||
logging.basicConfig(format="%(message)s", level=logging.INFO)
|
||||
|
||||
|
||||
def delete_file(path: str | Path) -> bool:
|
||||
"""Sends a file to the system trash.
|
||||
|
||||
Args:
|
||||
path (str | Path): The path of the file to delete.
|
||||
"""
|
||||
_path = Path(path)
|
||||
try:
|
||||
logging.info(f"[delete_file] Sending to Trash: {_path}")
|
||||
send2trash(_path)
|
||||
return True
|
||||
except PermissionError as e:
|
||||
logging.error(f"[delete_file][ERROR] PermissionError: {e}")
|
||||
except FileNotFoundError:
|
||||
logging.error(f"[delete_file][ERROR] File Not Found: {_path}")
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
return False
|
||||
152
tagstudio/src/qt/helpers/file_opener.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import shutil
|
||||
import sys
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtWidgets import QLabel
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
ERROR = f"[ERROR]"
|
||||
WARNING = f"[WARNING]"
|
||||
INFO = f"[INFO]"
|
||||
|
||||
logging.basicConfig(format="%(message)s", level=logging.INFO)
|
||||
|
||||
|
||||
def open_file(path: str | Path, file_manager: bool = False):
|
||||
"""Open a file in the default application or file explorer.
|
||||
|
||||
Args:
|
||||
path (str): The path to the file to open.
|
||||
file_manager (bool, optional): Whether to open the file in the file manager (e.g. Finder on macOS).
|
||||
Defaults to False.
|
||||
"""
|
||||
_path = str(path)
|
||||
logging.info(f"Opening file: {_path}")
|
||||
if not os.path.exists(_path):
|
||||
logging.error(f"File not found: {_path}")
|
||||
return
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
normpath = os.path.normpath(_path)
|
||||
if file_manager:
|
||||
command_name = "explorer"
|
||||
command_args = '/select,"' + normpath + '"'
|
||||
# For some reason, if the args are passed in a list, this will error when the path has spaces, even while surrounded in double quotes
|
||||
subprocess.Popen(
|
||||
command_name + command_args,
|
||||
shell=True,
|
||||
close_fds=True,
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
| subprocess.CREATE_BREAKAWAY_FROM_JOB,
|
||||
)
|
||||
else:
|
||||
command_name = "start"
|
||||
# first parameter is for title, NOT filepath
|
||||
command_args = ["", normpath]
|
||||
subprocess.Popen(
|
||||
[command_name] + command_args,
|
||||
shell=True,
|
||||
close_fds=True,
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
| subprocess.CREATE_BREAKAWAY_FROM_JOB,
|
||||
)
|
||||
else:
|
||||
if sys.platform == "darwin":
|
||||
command_name = "open"
|
||||
command_args = [_path]
|
||||
if file_manager:
|
||||
# will reveal in Finder
|
||||
command_args.append("-R")
|
||||
else:
|
||||
if file_manager:
|
||||
command_name = "dbus-send"
|
||||
# might not be guaranteed to launch default?
|
||||
command_args = [
|
||||
"--session",
|
||||
"--dest=org.freedesktop.FileManager1",
|
||||
"--type=method_call",
|
||||
"/org/freedesktop/FileManager1",
|
||||
"org.freedesktop.FileManager1.ShowItems",
|
||||
f"array:string:file://{_path}",
|
||||
"string:",
|
||||
]
|
||||
else:
|
||||
command_name = "xdg-open"
|
||||
command_args = [_path]
|
||||
command = shutil.which(command_name)
|
||||
if command is not None:
|
||||
subprocess.Popen([command] + command_args, close_fds=True)
|
||||
else:
|
||||
logging.info(f"Could not find {command_name} on system PATH")
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
class FileOpenerHelper:
|
||||
def __init__(self, filepath: str | Path):
|
||||
"""Initialize the FileOpenerHelper.
|
||||
|
||||
Args:
|
||||
filepath (str): The path to the file to open.
|
||||
"""
|
||||
self.filepath = str(filepath)
|
||||
|
||||
def set_filepath(self, filepath: str | Path):
|
||||
"""Set the filepath to open.
|
||||
|
||||
Args:
|
||||
filepath (str): The path to the file to open.
|
||||
"""
|
||||
self.filepath = str(filepath)
|
||||
|
||||
def open_file(self):
|
||||
"""Open the file in the default application."""
|
||||
open_file(self.filepath)
|
||||
|
||||
def open_explorer(self):
|
||||
"""Open the file in the default file explorer."""
|
||||
open_file(self.filepath, file_manager=True)
|
||||
|
||||
|
||||
class FileOpenerLabel(QLabel):
|
||||
def __init__(self, text, parent=None):
|
||||
"""Initialize the FileOpenerLabel.
|
||||
|
||||
Args:
|
||||
text (str): The text to display.
|
||||
parent (QWidget, optional): The parent widget. Defaults to None.
|
||||
"""
|
||||
super().__init__(text, parent)
|
||||
|
||||
def setFilePath(self, filepath):
|
||||
"""Set the filepath to open.
|
||||
|
||||
Args:
|
||||
filepath (str): The path to the file to open.
|
||||
"""
|
||||
self.filepath = filepath
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""Handle mouse press events.
|
||||
|
||||
On a left click, open the file in the default file explorer. On a right click, show a context menu.
|
||||
|
||||
Args:
|
||||
event (QMouseEvent): The mouse press event.
|
||||
"""
|
||||
super().mousePressEvent(event)
|
||||
|
||||
if event.button() == Qt.LeftButton:
|
||||
opener = FileOpenerHelper(self.filepath)
|
||||
opener.open_explorer()
|
||||
elif event.button() == Qt.RightButton:
|
||||
# Show context menu
|
||||
pass
|
||||
31
tagstudio/src/qt/helpers/file_tester.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import ffmpeg
|
||||
from pathlib import Path
|
||||
|
||||
from src.qt.helpers.vendored.ffmpeg import _probe
|
||||
|
||||
|
||||
def is_readable_video(filepath: Path | str):
|
||||
"""Test if a video is in a readable format. Examples of unreadable videos
|
||||
include files with undetermined codecs and DRM-protected content.
|
||||
|
||||
Args:
|
||||
filepath (Path | str):
|
||||
"""
|
||||
try:
|
||||
probe = _probe(Path(filepath))
|
||||
for stream in probe["streams"]:
|
||||
# DRM check
|
||||
if stream.get("codec_tag_string") in [
|
||||
"drma",
|
||||
"drms",
|
||||
"drmi",
|
||||
]:
|
||||
return False
|
||||
except ffmpeg.Error:
|
||||
return False
|
||||
return True
|
||||
21
tagstudio/src/qt/helpers/function_iterator.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from PySide6.QtCore import Signal, QObject
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class FunctionIterator(QObject):
|
||||
"""Iterates over a yielding function and emits progress as the 'value' signal.\n\nThread-Safe Guarantee™"""
|
||||
|
||||
value = Signal(object)
|
||||
|
||||
def __init__(self, function: Callable):
|
||||
super().__init__()
|
||||
self.iterable = function
|
||||
|
||||
def run(self):
|
||||
for i in self.iterable():
|
||||
self.value.emit(i)
|
||||