Skip to content

Commit

Permalink
feat(ui): expanded thumbnail and preview features (TagStudioDev#390)
Browse files Browse the repository at this point in the history
* Fix text and RAW image handling

- Fix RAW images not being loaded correctly in the preview panel
- Fix trying to read size data from null images
- Refactor `os.stat` to `<Path object>.stat()`
- Remove unnecessary upper/lower conversions
- Improve encoding compatibility beyond UTF-8 when reading text files
- Code cleanup

* Use chardet for character encoding detection

* Add support for waveform + album cover thumbnails

* Rename "cover" variables for MyPy

* Rename "audio_tags" variables for MyPy + typing

* Add # type: ignore to fromstring method

* Add GIF preview support

* Add rough check for invalid video codecs

* Add ".plist" to PLAINTEXT_TYPES

* Add readable video tester

* Add ".psd" to IMAGE_TYPES; Handle ID3NoHeaderError

* Improve and style waveform previews

* Add final return statement to _album_artwork()

* Add final return statement to _audio_waveform()

* Tweak waveform color and size

* Fix ItemThumb label text color in light mode

* Fix most theme UI legibility issues

* Match additional UI to color scheme

* ruff format

* feat(ui): add UI color palette dict

* feat(ui) center and color small font previews

* fix(ui): large font previews follow app theme

* fix(ui): blender previews follow app theme

* feat(ui): add resizable thumbnail options

* fix: mkv files with "[0][0][0][0]" codec load properly

* fix: missing audio files properly handled

* feat(ui): use system accent color for thumb selections

* fix(ui): hide gif preview in multi-selections

* feat(ui): add dynamic file thumb icons

* fix(ui): hide previous thumbnail before resizing

* (fix): catch ffmpeg errors in file tester

* Squashed commit of the following:

commit 9a3c19d
Author: Travis Abendshien <[email protected]>
Date:   Wed Jul 24 22:57:32 2024 -0700

    fix: add missing comma + sort extensions

commit 53b2db9
Author: Travis Abendshien <[email protected]>
Date:   Wed Jul 24 14:46:16 2024 -0700

    refactor: move type constants to new media classes

* feat(ui): add media types and icon resources

* feat(ui): add more default media types and icons

Add additional default icons for:
- Blender
- Presentation
- Program
- Spreadsheet
Add/expand additional media types:
- PDF
- Packages

* fix: remove leading dot in preview panel ext

* refactor: remove edge from `four_corner_gradient()`

* fix: handle missing files in `resource_manager`

* fix(ui): thumb edges fading on refresh

* feat(ui): add default icons for audio+vector thumbs

* feat(ui): apply edge to default icon thumbs

* chore: remove unused code

* refactor(ui): move loading icon to `ResourceManager`

* fix(ui) color for default icons follow theme

* fix: remove `theme_color` redef

* refactor: make some consts and args clearer

* refactor: organize arguments, update docstrings

The ability to pass a border radius scaling argument is also included.

* chore: format docstrings with ruff

* refactor: replace magic numbers with named values

* refactor: remove unused code, comments, & imports

* refactor: rename args to not shadow builtins

* refactor: remove unused vars from `thumb_renderer`

* fix: handle ValueError in `render()`

Handle ValueErrors in `render()`. This case was encountered when attempting to render an `XPM` file during testing.

* docs: add FFmpeg requirement to README
  • Loading branch information
CyanVoxel authored and CarterPillow committed Sep 7, 2024
1 parent 2c739ca commit eaedc5b
Show file tree
Hide file tree
Showing 46 changed files with 1,486 additions and 481 deletions.
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tagstudio/resources/qt/images/file_icons/font.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tagstudio/resources/qt/images/file_icons/text.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed tagstudio/resources/qt/images/thumb_border_512.png
Binary file not shown.
Binary file removed tagstudio/resources/qt/images/thumb_broken_512.png
Binary file not shown.
Binary file not shown.
Binary file added tagstudio/resources/qt/images/thumb_loading.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed tagstudio/resources/qt/images/thumb_loading_512.png
Binary file not shown.
Binary file not shown.
Binary file removed tagstudio/resources/qt/images/thumb_mask_128.png
Binary file not shown.
Binary file removed tagstudio/resources/qt/images/thumb_mask_512.png
Binary file not shown.
Binary file removed tagstudio/resources/qt/images/thumb_mask_hl_512.png
Diff not rendered.
1 change: 0 additions & 1 deletion tagstudio/src/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,5 @@
"cool gray",
"olive",
]

TAG_FAVORITE = 1
TAG_ARCHIVED = 0
4 changes: 3 additions & 1 deletion tagstudio/src/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ class SettingItems(str, enum.Enum):


class Theme(str, enum.Enum):
COLOR_BG = "#65000000"
COLOR_BG_DARK = "#65000000"
COLOR_BG_LIGHT = "#22000000"
COLOR_DARK_LABEL = "#DD000000"
COLOR_HOVER = "#65AAAAAA"
COLOR_PRESSED = "#65EEEEEE"
COLOR_DISABLED = "#65F39CAA"
Expand Down
60 changes: 49 additions & 11 deletions tagstudio/src/core/media_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
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"
Expand All @@ -27,7 +30,7 @@ class MediaType(str, Enum):
MATERIAL: str = "material"
MODEL: str = "model"
PACKAGE: str = "package"
PHOTOSHOP: str = "photoshop"
PDF: str = "pdf"
PLAINTEXT: str = "plaintext"
PRESENTATION: str = "presentation"
PROGRAM: str = "program"
Expand Down Expand Up @@ -67,6 +70,12 @@ class MediaCategories:
# 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",
Expand All @@ -76,6 +85,10 @@ class MediaCategories:
".tgz",
".zip",
}
_AUDIO_MIDI_SET: set[str] = {
".mid",
".midi",
}
_AUDIO_SET: set[str] = {
".aac",
".aif",
Expand Down Expand Up @@ -182,6 +195,7 @@ class MediaCategories:
".jpg_large",
".jpg",
".jpg2",
".jxl",
".png",
".psb",
".psd",
Expand All @@ -192,11 +206,17 @@ class MediaCategories:
_INSTALLER_SET: set[str] = {".appx", ".msi", ".msix"}
_MATERIAL_SET: set[str] = {".mtl"}
_MODEL_SET: set[str] = {".3ds", ".fbx", ".obj", ".stl"}
_PACKAGE_SET: set[str] = {".pkg"}
_PHOTOSHOP_SET: set[str] = {
".pdd",
".psb",
".psd",
_PACKAGE_SET: set[str] = {
".aab",
".akp",
".apk",
".apkm",
".apks",
".pkg",
".xapk",
}
_PDF_SET: set[str] = {
".pdf",
}
_PLAINTEXT_SET: set[str] = {
".bat",
Expand Down Expand Up @@ -247,14 +267,29 @@ class MediaCategories:
".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,
extensions=_AUDIO_SET | _AUDIO_MIDI_SET,
is_iana=True,
)
BLENDER_TYPES: MediaCategory = MediaCategory(
Expand Down Expand Up @@ -317,9 +352,9 @@ class MediaCategories:
extensions=_PACKAGE_SET,
is_iana=False,
)
PHOTOSHOP_TYPES: MediaCategory = MediaCategory(
media_type=MediaType.PHOTOSHOP,
extensions=_PHOTOSHOP_SET,
PDF_TYPES: MediaCategory = MediaCategory(
media_type=MediaType.PDF,
extensions=_PDF_SET,
is_iana=False,
)
PLAINTEXT_TYPES: MediaCategory = MediaCategory(
Expand Down Expand Up @@ -359,7 +394,10 @@ class MediaCategories:
)

ALL_CATEGORIES: list[MediaCategory] = [
ADOBE_PHOTOSHOP_TYPES,
AFFINITY_PHOTO_TYPES,
ARCHIVE_TYPES,
AUDIO_MIDI_TYPES,
AUDIO_TYPES,
BLENDER_TYPES,
DATABASE_TYPES,
Expand All @@ -373,7 +411,7 @@ class MediaCategories:
MATERIAL_TYPES,
MODEL_TYPES,
PACKAGE_TYPES,
PHOTOSHOP_TYPES,
PDF_TYPES,
PLAINTEXT_TYPES,
PRESENTATION_TYPES,
PROGRAM_TYPES,
Expand Down
55 changes: 50 additions & 5 deletions tagstudio/src/core/palette.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class ColorType(int, Enum):
DARK_ACCENT = 4


_TAG_COLORS = {
_TAG_COLORS: dict = {
"": {
ColorType.PRIMARY: "#1e1e1e",
ColorType.TEXT: ColorType.LIGHT_ACCENT,
Expand Down Expand Up @@ -277,13 +277,58 @@ class ColorType(int, Enum):
},
}

_UI_COLORS: dict = {
"": {
ColorType.PRIMARY: "#333333",
ColorType.BORDER: "#555555",
ColorType.LIGHT_ACCENT: "#FFFFFF",
ColorType.DARK_ACCENT: "#1e1e1e",
},
"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",
},
"red": {
ColorType.PRIMARY: "#e22c3c",
ColorType.BORDER: "#e54252",
ColorType.LIGHT_ACCENT: "#f39caa",
ColorType.DARK_ACCENT: "#440d12",
},
"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, color):
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"


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)
13 changes: 9 additions & 4 deletions tagstudio/src/qt/helpers/color_overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,28 @@

# TODO: Consolidate the built-in QT theme values with the values
# here, in enums.py, and in palette.py.
_THEME_DARK_FG: str = "#FFFFFF55"
_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) -> Image.Image:
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 = (
_THEME_DARK_FG
dark_fg
if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
else _THEME_LIGHT_FG
else light_fg
)

im = Image.new(mode="RGBA", size=image.size, color=overlay_color)
return _apply_overlay(image, im)

Expand Down
29 changes: 29 additions & 0 deletions tagstudio/src/qt/helpers/file_tester.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 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


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 = ffmpeg.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
39 changes: 14 additions & 25 deletions tagstudio/src/qt/helpers/gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,14 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio

from PIL import Image, ImageEnhance, ImageChops
from PIL import Image


def four_corner_gradient_background(
image: Image.Image, adj_size, mask, hl
def four_corner_gradient(
image: Image.Image, size: tuple[int, int], mask: Image.Image
) -> Image.Image:
if image.size != (adj_size, adj_size):
# Old 1 color method.
# bg_col = image.copy().resize((1, 1)).getpixel((0,0))
# bg = Image.new(mode='RGB',size=(adj_size,adj_size),color=bg_col)
# bg.thumbnail((1, 1))
# bg = bg.resize((adj_size,adj_size), resample=Image.Resampling.NEAREST)

# Small gradient background. Looks decent, and is only a one-liner.
# bg = image.copy().resize((2, 2), resample=Image.Resampling.BILINEAR).resize((adj_size,adj_size),resample=Image.Resampling.BILINEAR)

if image.size != size:
# Four-Corner Gradient Background.
# Not exactly a one-liner, but it's (subjectively) really cool.
tl = image.getpixel((0, 0))
tr = image.getpixel(((image.size[0] - 1), 0))
bl = image.getpixel((0, (image.size[1] - 1)))
Expand All @@ -29,26 +19,25 @@ def four_corner_gradient_background(
bg.paste(tr, (1, 0, 2, 2))
bg.paste(bl, (0, 1, 2, 2))
bg.paste(br, (1, 1, 2, 2))
bg = bg.resize((adj_size, adj_size), resample=Image.Resampling.BICUBIC)

bg = bg.resize(size, resample=Image.Resampling.BICUBIC)
bg.paste(
image,
box=(
(adj_size - image.size[0]) // 2,
(adj_size - image.size[1]) // 2,
(size[0] - image.size[0]) // 2,
(size[1] - image.size[1]) // 2,
),
)

bg.putalpha(mask)
final = bg
final = Image.new("RGBA", bg.size, (0, 0, 0, 0))
final.paste(bg, mask=mask.getchannel(0))

else:
image.putalpha(mask)
final = image
final = Image.new("RGBA", size, (0, 0, 0, 0))
final.paste(image, mask=mask.getchannel(0))

if final.mode != "RGBA":
final = final.convert("RGBA")

hl_soft = hl.copy()
hl_soft.putalpha(ImageEnhance.Brightness(hl.getchannel(3)).enhance(0.5))
final.paste(ImageChops.soft_light(final, hl_soft), mask=hl_soft.getchannel(3))
return final


Expand Down
31 changes: 31 additions & 0 deletions tagstudio/src/qt/helpers/rounded_pixmap_style.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Based on the implementation by eyllanesc:
# https://stackoverflow.com/questions/54230005/qmovie-with-border-radius
# Licensed under the Creative Commons CC BY-SA 4.0 License:
# https://creativecommons.org/licenses/by-sa/4.0/
# Modified for TagStudio: https://github.com/CyanVoxel/TagStudio

from PySide6.QtGui import QBrush, QColor, QPainter, QPixmap
from PySide6.QtWidgets import (
QProxyStyle,
)


class RoundedPixmapStyle(QProxyStyle):
def __init__(self, radius=8):
super().__init__()
self._radius = radius

def drawItemPixmap(self, painter, rectangle, alignment, pixmap):
painter.save()
pix = QPixmap(pixmap.size())
pix.fill(QColor("transparent"))
p = QPainter(pix)
p.setBrush(QBrush(pixmap))
p.setPen(QColor("transparent"))
p.setRenderHint(QPainter.RenderHint.Antialiasing)
p.drawRoundedRect(pixmap.rect(), self._radius, self._radius)
p.end()
super(RoundedPixmapStyle, self).drawItemPixmap(
painter, rectangle, alignment, pix
)
painter.restore()
Loading

0 comments on commit eaedc5b

Please sign in to comment.