Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: port thumbnail (#390) and related features to v9.5 #522

Merged
merged 18 commits into from
Oct 7, 2024

Conversation

CyanVoxel
Copy link
Member

This PR ports the following thumbnail and related PRs from the Alpha-v9.4 branch to main (v9.5+):

Many of these features and changes were closely intertwined with the focused PR (#390) to where it didn't make sense to spend the extra time and effort to try and comb them apart just for a cleaner git history. If I missed any other PRs or attributions, please let me know.

CyanVoxel and others added 5 commits September 22, 2024 13:39
Ports the following thumbnail and related PRs from the `Alpha-v9.4` branch to `main` (v9.5+):
- (#273) Blender thumbnail support
- (#307) Add font thumbnail preview support
- (#331) refactor: move type constants to new media classes
- (#390) feat(ui): expanded thumbnail and preview features
- (#370) ui: "open in explorer" action follows os name
- (#373) feat(ui): preview support for source engine files
- (#274) Refactor video_player.py (Fix #270)
- (#430) feat(ui): show file creation/modified dates + restyle path label
- (#471) fix(ui): use default audio icon if ffmpeg is absent
- (#472) fix(ui): use birthtime for creation time on mac & win

Co-Authored-By: Ethnogeny <111099761+050011-code@users.noreply.github.com>
Co-Authored-By: Theasacraft <91694323+Thesacraft@users.noreply.github.com>
Co-Authored-By: SupKittyMeow <77246128+supkittymeow@users.noreply.github.com>
Co-Authored-By: EJ Stinson <93455158+favroitegamers@users.noreply.github.com>
Co-Authored-By: Sean Krueger <71362472+seakrueger@users.noreply.github.com>
@CyanVoxel CyanVoxel added Type: Enhancement New feature or request Type: UI/UX User interface and/or user experience Priority: Medium An issue that shouldn't be be saved for last Status: Review Needed A review of this is needed labels Sep 22, 2024
@CyanVoxel CyanVoxel added this to the SQL Parity milestone Sep 22, 2024
Copy link
Collaborator

@yedpodtrzitko yedpodtrzitko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend doing smaller PRs instead, which would be easier to review.

requirements.txt Outdated
mutagen==1.47.0
numpy==1.26.4
ffmpeg-python==0.2.0
Send2Trash==1.8.3
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this dependency looks unused

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Send2Trash was accidentally added due to its inclusion in future a v9.4 PR, fixed 👍



class MediaCategories:
"""Contains pre-made MediaCategory objects as well as methods to interact with them."""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Contains pre-made MediaCategory objects as well as methods to interact with them."""
"""Contain pre-made MediaCategory objects as well as methods to interact with them."""

Comment on lines 59 to 67
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is just a data wrapper, I'd suggest to use dataclass instead. It will save you from writing the constructor and assigning the values in it manually.

@dataclass(frozen=True)
class MediaCategory:
    media_type: MediaType
    extensions: set[str]
    is_iana: bool # maybe assign default value, so you dont have to initialize it explicitly half of the times?

example of usage eg. here:

@dataclass
class BranchData:
dirs: dict[str, "BranchData"] = field(default_factory=dict)
files: list[str] = field(default_factory=list)
tag: Tag | None = None


@staticmethod
def get_types(ext: str, mime_fallback: bool = False) -> set[MediaType]:
"""Returns a set of MediaTypes given a file extension.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Returns a set of MediaTypes given a file extension.
"""Return a set of MediaTypes given a file extension.

Comment on lines 490 to 491
type: str = mimetypes.guess_type(Path("x" + ext), strict=False)[0]
if type and type.startswith(cat.media_type.value):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type is a name of builtin, so it would be better to use different variable name.

@@ -800,6 +817,34 @@ def run_macro(self, name: MacroID, grid_idx: int):
content=strip_web_protocol(field.value),
)

def thumb_size_callback(self, index: int):
"""Performs actions needed when the thumbnail size selection is changed.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Performs actions needed when the thumbnail size selection is changed.
"""Perform actions needed when the thumbnail size selection is changed.

@@ -74,7 +76,8 @@ def render(
color=self.get_file_color(filepath.suffix.lower()),
)

if filepath.suffix.lower() in IMAGE_TYPES:
ext: str = filepath.suffix.lower()
if MediaType.IMAGE in MediaCategories.get_types(ext):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is quite inefficient - get_types() accumulates all the categories and then you check if it's present in the results.

It would be better to do the check in a way that it returns True as soon as it finds a match, so it doesnt need to go through all the remaining categories if it knows it will be a match after the (for example) first comparison.

Same applies for the other similar checks below.

Comment on lines 463 to 464
if self.afm.is_connected:
self.afm.done.disconnect()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

afm is not really good variable name, please use some more descriptive name

Comment on lines 727 to 728
f"[PreviewPanel][ERROR] Couldn't Render thumbnail for {filepath} "
f"(because of {e})"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest not to cram everything into the f-string. structlog allows to use extra variables (see previous version), which is way more readable:
Screenshot 2024-09-23 at 17 50 31

Comment on lines 909 to 911
is_loading=False,
is_grid_thumb=False,
update_on_ratio_change=False,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are the types removed here?

@CyanVoxel CyanVoxel removed the Status: Review Needed A review of this is needed label Sep 26, 2024
@CyanVoxel
Copy link
Member Author

Implemented suggested feedback, thanks for the review 👍

Copy link
Collaborator

@yedpodtrzitko yedpodtrzitko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

almost there :)

@@ -23,9 +24,26 @@ def __init__(self) -> None:
if not ResourceManager._initialized:
with open(Path(__file__).parent / "resources.json", encoding="utf-8") as f:
ResourceManager._map = ujson.load(f)
logger.info("resources registered", count=len(ResourceManager._map.items()))
logger.info(
f"[ResourceManager] {len(ResourceManager._map.items())} resources registered"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any rationale to shove the variables into f-string, or is it a bad merge? The extra parameters are better place where to keep them.

ResourceManager._initialized = True

@staticmethod
def get_path(id: str) -> Path | None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks unused 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gets used by another feature in 9.4 (file deleting) - I could remove it here but I would be adding it back in the next PR here anyways

except FileNotFoundError:
logger.error(
"[ResourceManager][ERROR]: Could not find resource: "
f"{Path(__file__).parents[2] / "resources" / res.get("path")}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Path(__file__).parents[2] / "resources" / res.get("path") is repeated three times in total, that could be worth extracting it into a separate variable.

Comment on lines 130 to 139
self.trash_term: str = "Trash"
if platform.system() == "Windows":
self.trash_term = "Recycle Bin"
self.delete_action = QAction(f"Send file to {self.trash_term}", self)

self.open_explorer_action = QAction("Open in explorer", self) # Default text (Linux, etc.)
if platform.system() == "Darwin":
self.open_explorer_action = QAction("Reveal in Finder", self)
elif platform.system() == "Windows":
self.open_explorer_action = QAction("Open in Explorer", self)
Copy link
Collaborator

@yedpodtrzitko yedpodtrzitko Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • self.trash_term doesnt look used anywhere else than on the line below it, so a local variable trash_term would be sufficient, no reason to keep it in scope of the class.

  • doing the same thing for Open action is totally different appoach - instead of changing only the label value, it creates a whole QAction, and in case the platform doesnt match, it creates the QAction once again, which feels wasteful.

having both these labels decided in the same if-elif-else could be better.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the trash_term/delete_action as those were part of a separate PR that got mixed in here. Otherwise I've split out the explorer action string so it (and future strings) can be reused

Comment on lines 639 to 645
if (
(MediaType.IMAGE in MediaCategories.get_types(ext))
and (MediaType.IMAGE_RAW not in MediaCategories.get_types(ext))
and (MediaType.IMAGE_VECTOR not in MediaCategories.get_types(ext))
):
image = Image.open(str(filepath))
elif filepath.suffix.lower() in RAW_IMAGE_TYPES:
elif MediaType.IMAGE_RAW in MediaCategories.get_types(ext):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to use is_ext_in_category() here as well?

Comment on lines 458 to 461
logger.error(
f"[ThumbRenderer][ERROR]: Couldn't read album artwork for {filepath.name} "
f"({type(e).__name__})"
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logger.error(
f"[ThumbRenderer][ERROR]: Couldn't read album artwork for {filepath.name} "
f"({type(e).__name__})"
)
logger.error("Couldn't read album artwork", path=filepath, error=e)

Comment on lines 546 to 549
logger.error(
f"[ThumbRenderer][WAVEFORM][ERROR]: Couldn't render waveform for {filepath.name} "
f"({type(e).__name__})"
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logger.error(
f"[ThumbRenderer][WAVEFORM][ERROR]: Couldn't render waveform for {filepath.name} "
f"({type(e).__name__})"
)
logger.error("Couldn't render waveform", path=filename, error=e)

Comment on lines 129 to 134
system = platform.system()
open_explorer_action = QAction("Open in explorer", self) # Default, usually Linux
if system == "Darwin":
open_explorer_action = QAction("Reveal in Finder", self)
elif system == "Windows":
open_explorer_action = QAction("Open in Explorer", self)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ this code already exists in preview_panel.py, cant it be reused here somehow?

@CyanVoxel
Copy link
Member Author

@yedpodtrzitko Implemented the new feedback 👍 Let me know how you feel about the PlatformStrings file and if there's anything else I may have missed

Comment on lines +13 to +16
if platform.system() == "Windows":
open_file_str = "Open in Explorer"
elif platform.system() == "Darwin":
open_file_str = "Reveal in Finder"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having the logic directly in the class isnt the usual way how to do it, but I think we can live with it here 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future reference what would be the expected way to do something like this? I was mulling over different implementations and just went with this since it minimizes the performance impact of the checks while keeping the imports somewhat clean

Copy link
Collaborator

@yedpodtrzitko yedpodtrzitko Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just briefly - I would probably go slightly different way - rather than doing everything in one class, I'd create a class with common phrases, and then inherit from that class like WindowStrings where platform-specific phrases would be overriden.

class SharedStrings:
   open_file_str: str = 'generic value'
   generic_string: tr = 'shared among all classes'

class WindowsStrings(SharedStrings):
   open_file_str: str = 'windows specific value'  

TRANSLATIONS = # `platform.system()` logic to decide which class to use

@CyanVoxel CyanVoxel added the Status: Mergeable The code is ready to be merged label Oct 3, 2024
@CyanVoxel CyanVoxel merged commit 7dd0f3d into main Oct 7, 2024
10 checks passed
@CyanVoxel CyanVoxel deleted the thumb-port-v9.5 branch October 7, 2024 21:14
@CyanVoxel CyanVoxel removed Priority: Medium An issue that shouldn't be be saved for last Status: Mergeable The code is ready to be merged labels Oct 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Enhancement New feature or request Type: UI/UX User interface and/or user experience
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[BUG]: Video Playback Icons Missing in Executables
2 participants