Skip to content

Commit

Permalink
Improve the manul selection popup (#2)
Browse files Browse the repository at this point in the history
Use a `Combo` instead of a radio select for the destination folder, allow folder creation.
Also handle `WIN_CLOSED` events, add color theme

---

* dropdown combobox
* correctly handle preview
* Refactor preview image code
* Allow directory creation
* Better instrucitons
* color theme
* WIN_CLOSED handling
* rename thumbnail method, larger dimensions
* manual select image
  • Loading branch information
michelcrypt4d4mus authored May 27, 2023
1 parent 713b253 commit d7f1875
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 63 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# NEXT RELEASE
* Combobox instead of radio buttons for manual fallback directory select
* Replace fewer special characters in filenames
* New default crypto sort rules

Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ sort_screenshots -h
# they just show you what will happen if you run again with the --execute flag)
sort_screenshots

# Execute default cryptocurrency sort rules against ~/Pictures/Screenshots
sort_screenshots --execute
# Execute default cryptocurrency sort rules against ~/Pictures/Screenshots with debug logging
sort_screenshots --execute --debug

# Sort a different directory of screenshots
sort_screenshots --screenshots-dir /Users/hrollins/Pictures/get_in_the_van/tourphotos --execute
Expand All @@ -58,6 +58,9 @@ sort_screenshots --rules-csv /Users/hrollins/my_war.csv --execute

# Sort pdfs
sort_screenshots -f '.*pdf$' -e

# Sort all but put up the manual folder / filename selector window if file doesn't match any sort rules
sort_screenshots -a -mf -e
```

# Setup
Expand Down Expand Up @@ -95,7 +98,7 @@ While every effort has been made to use Python's cross platform `Pathlib` module
### Help Screen
![](doc/sort_screenshots_help.png)

(In my personal usuage I tend to run the tool with the `--all` and `--only-if-match` options.)
(In my personal usuage I tend to run the tool with the `--all` and `--manual-fallback` options.)

### Custom Sorting Rules
The default is to sort cryptocurrency related content but you can define your own CSV of rules with two columns `folder` and `regex`. The value in `folder` specifies the subdirectory to sort into and `regex` is the pattern to match against. See [the default crypto related configuration](clown_sort/sorting_rules/crypto.csv) for an example. An explanation of regular expressions is beyond the scope of this README but many resources are available to help. If you're not good at regexes just remember that any alphanumeric string is a regex that will match that string. [pythex](http://pythex.org/) is a great website for testing your regexes.
Expand All @@ -107,7 +110,9 @@ The default is to sort cryptocurrency related content but you can define your ow
## Manually Sorting (Experimental)
**This is an experimental feature.** It's only been tested on macOS.

If you run with the `--manual-sort` command line the behavior is quite different. Rather than automatically sort files for you, instead for every file you will be greeted with a popup asking you for a desired filename and a radio button select of possible subdirectories off your `Sorted/` directory.
If you run with the `--manual-sort` command line the behavior is quite different. Rather than automatically sort files for you for every image file you will be greeted with a popup asking you for a desired filename and a radio button select of possible subdirectories off your `Sorted/` directory.

A related command line option is `--manual-fallback` which will popup a window only when the file is an image and has not matched any of the configured sorting rules.

To use this feature you must install the optional `PySimpleGUI` package which can be accomplished like this:
```sh
Expand Down
16 changes: 7 additions & 9 deletions clown_sort/files/image_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from clown_sort.util.logging import log
from clown_sort.util.rich_helper import console, warning_text

THUMBNAIL_DIMENSIONS = (400, 400)
THUMBNAIL_DIMENSIONS = (512, 512)
IMAGE_DESCRIPTION = 'ImageDescription'
FILENAME_LENGTH_TO_CONSIDER_SORTED = 80

Expand All @@ -31,7 +31,7 @@


class ImageFile(SortableFile):
def copy_file_to_sorted_dir(self, destination_path: Path, match: Optional[re.Match] = None) -> Path:
def copy_file_to_sorted_dir(self, destination_path: Path, match: Optional[re.Match] = None) -> None:
"""
Copies to a new file and injects the ImageDescription exif tag.
If :destination_subdir is given new file will be in :destination_subdir off
Expand All @@ -42,7 +42,7 @@ def copy_file_to_sorted_dir(self, destination_path: Path, match: Optional[re.Mat
self._log_copy_file(destination_path, match)

if Config.dry_run:
return destination_path
return

try:
self.pillow_image_obj().save(destination_path, exif=exif_data)
Expand All @@ -52,8 +52,6 @@ def copy_file_to_sorted_dir(self, destination_path: Path, match: Optional[re.Mat
console.print(f"ERROR while processing '{self.file_path}'", style='bright_red')
raise e

return destination_path

def new_basename(self) -> str:
"""Return a descriptive string usable in a filename."""
if self._new_basename is not None:
Expand All @@ -70,13 +68,13 @@ def new_basename(self) -> str:
self._new_basename = self._new_basename.replace('""', '"')
return self._new_basename

def image_bytes(self) -> bytes:
def thumbnail_bytes(self) -> bytes:
"""Return bytes for a thumbnail."""
image = self.pillow_image_obj()
image.thumbnail(THUMBNAIL_DIMENSIONS)
_image_bytes = io.BytesIO()
image.save(_image_bytes, format="PNG")
return _image_bytes.getvalue()
_thumbnail_bytes = io.BytesIO()
image.save(_thumbnail_bytes, format="PNG")
return _thumbnail_bytes.getvalue()

def extracted_text(self) -> Optional[str]:
"""Use Tesseract to OCR the text in the image, which is returned as a string."""
Expand Down
12 changes: 12 additions & 0 deletions clown_sort/files/sortable_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
Base class for sortable files of any type.
"""
import re
import platform
import shutil
from collections import namedtuple
from os import path, remove
from pathlib import Path
from subprocess import run
from typing import List, Optional, Union

from exiftool import ExifToolHelper
Expand Down Expand Up @@ -170,6 +172,16 @@ def sort_destination_path(self, subdir: Optional[Union[Path, str]] = None) -> Pa

return destination_path.joinpath(self.new_basename())

def preview(self) -> None:
"""Attempt to open a separate application to view the image."""
log.info(f"Opening '{self.file_path}'")

if platform.system() == 'Windows':
log.debug("Windows platform detected; attempting to run the file itself...")
run([self.file_path])
else:
run(['open', self.file_path])

def _log_copy_file(self, destination_path: Path, match: Optional[re.Match] = None) -> None:
"""Log info about a file copy."""
if Config.debug:
Expand Down
98 changes: 48 additions & 50 deletions clown_sort/sort_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,17 @@
Open a GUI window to allow manual name / select.
TODO: rename to something more appropriate
"""
import shutil
import sys
from os import path, remove
from subprocess import run

from rich.panel import Panel
from rich.text import Text

from clown_sort.config import Config
from clown_sort.filename_extractor import FilenameExtractor
from clown_sort.util.dict_helper import get_dict_key_by_value
from clown_sort.util.logging import log
from clown_sort.util.rich_helper import bullet_text, console, indented_bullet
from clown_sort.util.string_helper import is_empty

RADIO_COLS = 11
SELECT_SIZE = 45
DELETE = 'Delete'
OK = 'Move'
OPEN = 'Preview Image'
Expand All @@ -25,72 +21,74 @@


def process_file_with_popup(image: 'ImageFile') -> None:
import PySimpleGUI as sg
file_msg = Text("Processing: '").append(str(image.file_path), style='cyan reverse').append("'...")
console.print(Panel(file_msg, expand=False))
extracted_text = ' '.join((image.extracted_text() or '').splitlines())
log.info(f"OCR Text: {extracted_text} ({len(extracted_text)} chars)")
# Do the import here so as to allow usage without installing PySimpleGUI
import PySimpleGUI as psg
psg.theme('SystemDefault1')
suggested_filename = FilenameExtractor(image).filename()
input = sg.Input(suggested_filename, size=(len(suggested_filename), 1))
sort_dirs = [path.basename(dir) for dir in Config.get_sort_dirs()]
max_dirname_length = max([len(dir) for dir in sort_dirs])

layout = [
[sg.Image(data=image.image_bytes(), key="-IMAGE-")],
#[sg.Text(image.extracted_text())],
[sg.Text("Enter file name:")],
[input]
] + list(_subdir_radio_select_columns(sg)) + \
[[sg.Button(OK, bind_return_key=True), sg.Button(DELETE), sg.Button(OPEN), sg.Button(SKIP), sg.Button(EXIT)]]
[psg.Column([[psg.Image(data=image.thumbnail_bytes(), key="-IMAGE-")]], justification='center')],
[psg.HSep()],
[psg.Text("Choose Filename:")],
[psg.Input(suggested_filename, size=(len(suggested_filename), 1))],# font=("Courier New", 12))],
[
psg.Text(f"Choose Directory:"),
psg.Combo(sort_dirs, size=(max_dirname_length, SELECT_SIZE)),
psg.Text(f"(Enter custom text to create new directory. If no directory is chosen file will be copied to '{Config.sorted_screenshots_dir}'.)")
],
[
psg.Button(OK, bind_return_key=True),
psg.Button(DELETE),
psg.Button(OPEN),
psg.Button(SKIP),
psg.Button(EXIT)
]
]

window = sg.Window(image.basename, layout)
window = psg.Window(image.basename, layout)

# Event Loop
while True:
event, values = window.Read()

if event == OPEN:
image.preview()
continue

window.close()

if event == DELETE:
log.warning(f"Deleting '{image.file_path}'")
remove(image.file_path)
window.close()
return
elif event == OPEN:
log.info(f"Opening '{image.file_path}'")
run(['open', image.file_path])
elif event == SKIP:
window.close()
elif event == SKIP or event == psg.WIN_CLOSED:
return
elif event == EXIT:
window.close()
sys.exit()
elif event == OK:
break

window.close()
log.debug(f"All values: {values}")
chosen_filename = values[0]
new_subdir = values[1]
destination_dir = Config.sorted_screenshots_dir.joinpath(new_subdir)

if chosen_filename is None or len(chosen_filename) == 0:
if is_empty(chosen_filename):
raise ValueError("Filename can't be blank!")

try:
new_dir = get_dict_key_by_value(values, True)
except ValueError:
new_dir = Config.sorted_screenshots_dir
if not destination_dir.exists():
result = psg.popup_yes_no(f"Subdir '{new_subdir}' doesn't exist. Create?", title="Unknown Subdirectory")

new_filename = path.join(new_dir, chosen_filename)
log.info(f"Chosen Filename: '{chosen_filename}'\nDirectory: '{new_dir}'\nNew file: '{new_filename}'\nEvent: {event}\n")
log.debug(f"All values: {values}")
console.print(bullet_text(f"Moving '{image.file_path}' to '{new_filename}'..."))

if Config.dry_run:
console.print(indented_bullet("Dry run so not moving..."), style='dim')
else:
shutil.move(image.file_path, new_filename)


def _subdir_radio_select_columns(sg):
dirs = Config.get_sort_dirs()
if result == 'Yes' and not Config.dry_run:
log.info(f"Creating directory '{new_subdir}'...")
destination_dir.mkdir()
else:
console.print(bullet_text(f"Directory not found. Skipping '{image.file_path}'..."))
return

for i in range(0, len(dirs), RADIO_COLS):
yield [
sg.Radio(path.basename(dir), "SORTDIR_RADIO", default=False, key=dir)
for dir in dirs[i: i + RADIO_COLS]
]
new_filename = destination_dir.joinpath(chosen_filename)
log.info(f"Chosen Filename: '{chosen_filename}'\nSubdir: '{new_subdir}'\nNew file: '{new_filename}'\nEvent: {event}\n")
console.print(bullet_text(f"Moving '{image.file_path}' to '{new_filename}'..."))
image.copy_file_to_sorted_dir(new_filename)
5 changes: 5 additions & 0 deletions clown_sort/util/string_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@ def strip_bad_chars(text: str) -> str:


def strip_mac_screenshot(text: str) -> str:
"""Strip default macOS screenshot format from filename."""
return re.sub(SCREENSHOT_REGEX, '', text).strip()


def is_empty(text: str) -> bool:
return text is None or len(text) == 0
Binary file modified doc/manual_select_box.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit d7f1875

Please sign in to comment.