Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bc80a043f | ||
|
|
65d88b9987 | ||
|
|
37ff35fcf6 | ||
|
|
9b13e338bb | ||
|
|
a47b0adb6e | ||
|
|
9f39bf6fdc | ||
|
|
e375166bfe | ||
|
|
7054ffd227 | ||
|
|
6d283d1f2d | ||
|
|
a0baf015db |
62
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Bug Report
|
||||
description: File a bug or issue report.
|
||||
title: '[Bug]: '
|
||||
labels: ['Type: Bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
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.
|
||||
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature.
|
||||
title: '[Feature Request]: '
|
||||
labels: ['Type: Enhancement']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
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?
|
||||
172
CONTRIBUTING.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# 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`)_
|
||||
|
||||
### 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** (TagStudio.sh)
|
||||
> [!WARNING]
|
||||
> Support for NixOS is still a work in progress.
|
||||
- Use the provided `flake.nix` file to create and enter a working environment by running `nix develop`. Then, run the `TagStudio.sh` script.
|
||||
|
||||
- **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 _(Work in Progress)_
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Tests are not currently run as part of any automated workflow.
|
||||
|
||||
To run all the tests use `python -m pytest tests/` from the `tagstudio` folder.
|
||||
|
||||
## 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_
|
||||
94
README.md
@@ -10,15 +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)
|
||||
@@ -50,13 +53,17 @@ TagStudio is a photo & file organization application with an underlying system t
|
||||
- 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
|
||||
|
||||
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. 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.
|
||||
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.
|
||||
|
||||
> [!NOTE]
|
||||
> [!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
|
||||
@@ -112,9 +119,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.
|
||||
@@ -143,7 +147,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
|
||||
|
||||
@@ -159,65 +163,20 @@ 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.
|
||||
|
||||
## Creating a Development Environment
|
||||
## Launching/Building From Source
|
||||
|
||||
If you're interested in contributing to TagStudio or just wish to poke around the live codebase, here are instructions for setting up the Python project.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.12
|
||||
|
||||
### Creating a Python Virtual Environment
|
||||
|
||||
> [!NOTE]
|
||||
> Depending on your system, Python may be called `python`, `py`, `python3`, or `py3`. These instructions use the alias `python3`. You can check to see which alias you system uses and if it's for the correct Python version by typing `python3 --version` (or whichever alias) into your terminal.
|
||||
|
||||
_Skip these steps if launching from the .sh script on Linux/macOS._
|
||||
|
||||
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:
|
||||
|
||||
- required to run the app: `pip install -r requirements.txt`
|
||||
- required to develop: `pip install -r requirements-dev.txt`
|
||||
|
||||
To run all the tests use `python -m pytest tests/` from the `tagstudio` folder.
|
||||
|
||||
_Learn more about setting up a virtual environment [here](https://docs.python.org/3/tutorial/venv.html)._
|
||||
|
||||
### Launching Development Environment
|
||||
|
||||
- **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** (TagStudio.sh)
|
||||
- Use the provided `flake.nix` file to create and enter a working environment by running `nix develop`. Then, run the `TagStudio.sh` script.
|
||||
|
||||
- **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`.
|
||||
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.2.1) the project is in a useable state, however it lacks proper testing and quality of life features.
|
||||
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?
|
||||
|
||||
> [!NOTE]
|
||||
> **_See [Planned Features](/doc/planned_features.md) documentation._**
|
||||
> [!IMPORTANT]
|
||||
> See the [Planned Features](/doc/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.
|
||||
|
||||
@@ -283,18 +242,3 @@ 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.
|
||||
|
||||
Code formatting is automatically checked via [Ruff](https://docs.astral.sh/ruff/).
|
||||
|
||||
To format the code manually, install ruff via `pip install -r requirements-dev.txt` and then run `ruff format`
|
||||
|
||||
To format the code automatically before each commit, there's a configured action available for `pre-commit` hook. Install it by running `pre-commit install`. The hook will be executed each time on running `git commit`.
|
||||
|
||||
More structured documentation on contribution requirements is on its way, but for now:
|
||||
|
||||
- Use `pathlib` in favor of `os.path`
|
||||
- Try to make new UI additions match the existing style of the application
|
||||
|
||||
25
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1712473363,
|
||||
"narHash": "sha256-TIScFAVdI2yuybMxxNjC4YZ/j++c64wwuKbpnZnGiyU=",
|
||||
"lastModified": 1717602782,
|
||||
"narHash": "sha256-pL9jeus5QpX5R+9rsp3hhZ+uplVHscNJh8n8VpqscM0=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e89cf1c932006531f454de7d652163a9a5c86668",
|
||||
"rev": "e8057b67ebf307f01bdcc8fba94d94f75039d1f6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -16,9 +16,26 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"qt6Nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1711460435,
|
||||
"narHash": "sha256-Qb/J9NFk2Qemg7vTl8EDCto6p3Uf/GGORkGhTQJLj9U=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f862bd46d3020bcfe7195b3dad638329271b0524",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f862bd46d3020bcfe7195b3dad638329271b0524",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"qt6Nixpkgs": "qt6Nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
37
flake.nix
@@ -1,9 +1,18 @@
|
||||
{
|
||||
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
|
||||
outputs = { self, nixpkgs, }:
|
||||
qt6Nixpkgs = {
|
||||
# Commit bumping to qt6.6.3
|
||||
url = "github:NixOS/nixpkgs/f862bd46d3020bcfe7195b3dad638329271b0524";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, qt6Nixpkgs }:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||
|
||||
qt6Pkgs = qt6Nixpkgs.legacyPackages.x86_64-linux;
|
||||
in {
|
||||
devShells.x86_64-linux.default = pkgs.mkShell {
|
||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [
|
||||
@@ -17,19 +26,19 @@
|
||||
pkgs.xorg.libxcb
|
||||
pkgs.freetype
|
||||
pkgs.dbus
|
||||
pkgs.qt6.qtwayland
|
||||
pkgs.qt6.full
|
||||
pkgs.qt6.qtbase
|
||||
pkgs.zstd
|
||||
# For PySide6 Multimedia
|
||||
pkgs.libpulseaudio
|
||||
pkgs.libkrb5
|
||||
|
||||
qt6Pkgs.qt6.qtwayland
|
||||
qt6Pkgs.qt6.full
|
||||
qt6Pkgs.qt6.qtbase
|
||||
];
|
||||
buildInputs = with pkgs; [
|
||||
cmake
|
||||
gdb
|
||||
zstd
|
||||
qt6.qtbase
|
||||
qt6.full
|
||||
qt6.qtwayland
|
||||
qtcreator
|
||||
python312Packages.pip
|
||||
python312Full
|
||||
python312Packages.virtualenv # run virtualenv .
|
||||
@@ -49,11 +58,17 @@
|
||||
fontconfig
|
||||
xorg.libxcb
|
||||
|
||||
|
||||
# this is for the shellhook portion
|
||||
qt6.wrapQtAppsHook
|
||||
makeWrapper
|
||||
bashInteractive
|
||||
] ++ [
|
||||
qt6Pkgs.qt6.qtbase
|
||||
qt6Pkgs.qt6.full
|
||||
qt6Pkgs.qt6.qtwayland
|
||||
qt6Pkgs.qtcreator
|
||||
|
||||
# this is for the shellhook portion
|
||||
qt6Pkgs.qt6.wrapQtAppsHook
|
||||
];
|
||||
# set the environment variables that Qt apps expect
|
||||
shellHook = ''
|
||||
|
||||
@@ -26,7 +26,7 @@ a = Analysis(
|
||||
['tagstudio/tag_studio.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('tagstudio/resources', 'resources')],
|
||||
datas=[('tagstudio/resources', 'resources'), ('tagstudio/src', 'src')],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
|
||||
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 361 KiB |
|
Before Width: | Height: | Size: 172 B After Width: | Height: | Size: 172 B |
|
Before Width: | Height: | Size: 151 B After Width: | Height: | Size: 151 B |
|
Before Width: | Height: | Size: 327 B After Width: | Height: | Size: 327 B |
|
Before Width: | Height: | Size: 445 B After Width: | Height: | Size: 445 B |
@@ -1,5 +1,5 @@
|
||||
VERSION: str = "9.3.0" # Major.Minor.Patch
|
||||
VERSION_BRANCH: str = "" # Usually "" or "Pre-Release"
|
||||
VERSION: str = "9.3.1" # Major.Minor.Patch
|
||||
VERSION_BRANCH: str = "Pre-Release" # Usually "" or "Pre-Release"
|
||||
|
||||
# The folder & file names where TagStudio keeps its data relative to a library.
|
||||
TS_FOLDER_NAME: str = ".TagStudio"
|
||||
|
||||
67
tagstudio/src/qt/resource_manager.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# 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 typing import Any
|
||||
|
||||
import ujson
|
||||
|
||||
logging.basicConfig(format="%(message)s", level=logging.INFO)
|
||||
|
||||
|
||||
class ResourceManager:
|
||||
"""A resource manager for retrieving resources."""
|
||||
|
||||
_map: dict = {}
|
||||
_cache: dict[str, Any] = {}
|
||||
_initialized: bool = False
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Load JSON resource map
|
||||
if not ResourceManager._initialized:
|
||||
with open(
|
||||
Path(__file__).parent / "resources.json", mode="r", encoding="utf-8"
|
||||
) as f:
|
||||
ResourceManager._map = ujson.load(f)
|
||||
logging.info(
|
||||
f"[ResourceManager] {len(ResourceManager._map.items())} resources registered"
|
||||
)
|
||||
ResourceManager._initialized = True
|
||||
|
||||
def get(self, id: str) -> Any:
|
||||
"""Get a resource from the ResourceManager.
|
||||
This can include resources inside and outside of QResources, and will return
|
||||
theme-respecting variations of resources if available.
|
||||
|
||||
Args:
|
||||
id (str): The name of the resource.
|
||||
|
||||
Returns:
|
||||
Any: The resource if found, else None.
|
||||
"""
|
||||
cached_res = ResourceManager._cache.get(id)
|
||||
if cached_res:
|
||||
return cached_res
|
||||
else:
|
||||
res: dict = ResourceManager._map.get(id)
|
||||
if res.get("mode") in ["r", "rb"]:
|
||||
with open(
|
||||
(Path(__file__).parents[2] / "resources" / res.get("path")),
|
||||
res.get("mode"),
|
||||
) as f:
|
||||
data = f.read()
|
||||
if res.get("mode") == "rb":
|
||||
data = bytes(data)
|
||||
ResourceManager._cache[id] = data
|
||||
return data
|
||||
elif res.get("mode") in ["qt"]:
|
||||
# TODO: Qt resource loading logic
|
||||
pass
|
||||
|
||||
def __getattr__(self, __name: str) -> Any:
|
||||
attr = self.get(__name)
|
||||
if attr:
|
||||
return attr
|
||||
raise AttributeError(f"Attribute {id} not found")
|
||||
18
tagstudio/src/qt/resources.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"play_icon": {
|
||||
"path": "qt/images/play.svg",
|
||||
"mode": "rb"
|
||||
},
|
||||
"pause_icon": {
|
||||
"path": "qt/images/pause.svg",
|
||||
"mode": "rb"
|
||||
},
|
||||
"volume_icon": {
|
||||
"path": "qt/images/volume.svg",
|
||||
"mode": "rb"
|
||||
},
|
||||
"volume_mute_icon": {
|
||||
"path": "qt/images/volume_mute.svg",
|
||||
"mode": "rb"
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,7 @@ from src.qt.flowlayout import FlowLayout
|
||||
from src.qt.main_window import Ui_MainWindow
|
||||
from src.qt.helpers.function_iterator import FunctionIterator
|
||||
from src.qt.helpers.custom_runnable import CustomRunnable
|
||||
from src.qt.resource_manager import ResourceManager
|
||||
from src.qt.widgets.collage_icon import CollageIconRenderer
|
||||
from src.qt.widgets.panel import PanelModal
|
||||
from src.qt.widgets.thumb_renderer import ThumbRenderer
|
||||
@@ -164,6 +165,7 @@ class QtDriver(QObject):
|
||||
super().__init__()
|
||||
self.core: TagStudioCore = core
|
||||
self.lib = self.core.lib
|
||||
self.rm: ResourceManager = ResourceManager()
|
||||
self.args = args
|
||||
self.frame_dict: dict = {}
|
||||
self.nav_frames: list[NavigationState] = []
|
||||
|
||||
@@ -311,6 +311,7 @@ class ItemThumb(FlowWidget):
|
||||
|
||||
def set_mode(self, mode: Optional[ItemType]) -> None:
|
||||
if mode is None:
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
|
||||
self.unsetCursor()
|
||||
self.thumb_button.setHidden(True)
|
||||
# self.check_badges.setHidden(True)
|
||||
@@ -318,6 +319,7 @@ class ItemThumb(FlowWidget):
|
||||
# self.item_type_badge.setHidden(True)
|
||||
pass
|
||||
elif mode == ItemType.ENTRY and self.mode != ItemType.ENTRY:
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.thumb_button.setHidden(False)
|
||||
self.cb_container.setHidden(False)
|
||||
@@ -327,6 +329,7 @@ class ItemThumb(FlowWidget):
|
||||
self.count_badge.setHidden(True)
|
||||
self.ext_badge.setHidden(True)
|
||||
elif mode == ItemType.COLLATION and self.mode != ItemType.COLLATION:
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.thumb_button.setHidden(False)
|
||||
self.cb_container.setHidden(True)
|
||||
@@ -335,6 +338,7 @@ class ItemThumb(FlowWidget):
|
||||
self.count_badge.setHidden(False)
|
||||
self.item_type_badge.setHidden(False)
|
||||
elif mode == ItemType.TAG_GROUP and self.mode != ItemType.TAG_GROUP:
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.thumb_button.setHidden(False)
|
||||
# self.cb_container.setHidden(True)
|
||||
|
||||
@@ -532,6 +532,8 @@ class PreviewPanel(QWidget):
|
||||
pass
|
||||
elif filepath.suffix.lower() in VIDEO_TYPES:
|
||||
video = cv2.VideoCapture(str(filepath))
|
||||
if video.get(cv2.CAP_PROP_FRAME_COUNT) <= 0:
|
||||
raise cv2.error("File is invalid or has 0 frames")
|
||||
video.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||
success, frame = video.read()
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
|
||||
@@ -162,10 +162,10 @@ class ThumbRenderer(QObject):
|
||||
# Videos =======================================================
|
||||
elif _filepath.suffix.lower() in VIDEO_TYPES:
|
||||
video = cv2.VideoCapture(str(_filepath))
|
||||
video.set(
|
||||
cv2.CAP_PROP_POS_FRAMES,
|
||||
(video.get(cv2.CAP_PROP_FRAME_COUNT) // 2),
|
||||
)
|
||||
frame_count = video.get(cv2.CAP_PROP_FRAME_COUNT)
|
||||
if frame_count <= 0:
|
||||
raise cv2.error("File is invalid or has 0 frames")
|
||||
video.set(cv2.CAP_PROP_POS_FRAMES, frame_count // 2)
|
||||
success, frame = video.read()
|
||||
if not success:
|
||||
# Depending on the video format, compression, and frame
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import logging
|
||||
import os
|
||||
import typing
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
# os.environ["QT_MEDIA_BACKEND"] = "ffmpeg"
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
import typing
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt,
|
||||
@@ -18,7 +20,6 @@ from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput, QMediaDevices
|
||||
from PySide6.QtMultimediaWidgets import QGraphicsVideoItem
|
||||
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene
|
||||
from PySide6.QtGui import (
|
||||
QInputMethodEvent,
|
||||
QPen,
|
||||
QColor,
|
||||
QBrush,
|
||||
@@ -29,10 +30,7 @@ from PySide6.QtGui import (
|
||||
QBitmap,
|
||||
)
|
||||
from PySide6.QtSvgWidgets import QSvgWidget
|
||||
from PIL import Image
|
||||
from src.qt.helpers.file_opener import FileOpenerHelper
|
||||
|
||||
from src.core.constants import VIDEO_TYPES, AUDIO_TYPES
|
||||
from PIL import Image, ImageDraw
|
||||
from src.core.enums import SettingItems
|
||||
|
||||
@@ -41,26 +39,26 @@ if typing.TYPE_CHECKING:
|
||||
|
||||
|
||||
class VideoPlayer(QGraphicsView):
|
||||
"""A simple video player for the TagStudio application."""
|
||||
"""A basic video player."""
|
||||
|
||||
resolution = QSize(1280, 720)
|
||||
hover_fix_timer = QTimer()
|
||||
video_preview = None
|
||||
play_pause = None
|
||||
mute_button = None
|
||||
content_visible = False
|
||||
filepath = None
|
||||
|
||||
def __init__(self, driver: "QtDriver") -> None:
|
||||
# Set up the base class.
|
||||
super().__init__()
|
||||
self.driver = driver
|
||||
self.resolution = QSize(1280, 720)
|
||||
self.animation = QVariantAnimation(self)
|
||||
self.animation.valueChanged.connect(
|
||||
lambda value: self.setTintTransparency(value)
|
||||
)
|
||||
self.hover_fix_timer = QTimer()
|
||||
self.hover_fix_timer.timeout.connect(lambda: self.checkIfStillHovered())
|
||||
self.hover_fix_timer.setSingleShot(True)
|
||||
self.content_visible = False
|
||||
self.filepath = None
|
||||
|
||||
# Set up the video player.
|
||||
self.installEventFilter(self)
|
||||
self.setScene(QGraphicsScene(self))
|
||||
@@ -82,6 +80,7 @@ class VideoPlayer(QGraphicsView):
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.scene().addItem(self.video_preview)
|
||||
self.video_preview.setAcceptedMouseButtons(Qt.MouseButton.LeftButton)
|
||||
|
||||
# Set up the video tint.
|
||||
self.video_tint = self.scene().addRect(
|
||||
0,
|
||||
@@ -91,44 +90,31 @@ class VideoPlayer(QGraphicsView):
|
||||
QPen(QColor(0, 0, 0, 0)),
|
||||
QBrush(QColor(0, 0, 0, 0)),
|
||||
)
|
||||
# self.video_tint.setParentItem(self.video_preview)
|
||||
# self.album_art = QGraphicsPixmapItem(self.video_preview)
|
||||
# self.scene().addItem(self.album_art)
|
||||
# self.album_art.setPixmap(
|
||||
# QPixmap("./tagstudio/resources/qt/images/thumb_file_default_512.png")
|
||||
# )
|
||||
# self.album_art.setOpacity(0.0)
|
||||
|
||||
# Set up the buttons.
|
||||
self.play_pause = QSvgWidget("./tagstudio/resources/pause.svg")
|
||||
self.play_pause = QSvgWidget()
|
||||
self.play_pause.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
|
||||
self.play_pause.setMouseTracking(True)
|
||||
self.play_pause.installEventFilter(self)
|
||||
self.scene().addWidget(self.play_pause)
|
||||
self.play_pause.resize(100, 100)
|
||||
self.play_pause.resize(72, 72)
|
||||
self.play_pause.move(
|
||||
int(self.width() / 2 - self.play_pause.size().width() / 2),
|
||||
int(self.height() / 2 - self.play_pause.size().height() / 2),
|
||||
)
|
||||
self.play_pause.hide()
|
||||
|
||||
self.mute_button = QSvgWidget("./tagstudio/resources/volume_muted.svg")
|
||||
self.mute_button = QSvgWidget()
|
||||
self.mute_button.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
|
||||
self.mute_button.setMouseTracking(True)
|
||||
self.mute_button.installEventFilter(self)
|
||||
self.scene().addWidget(self.mute_button)
|
||||
self.mute_button.resize(40, 40)
|
||||
self.mute_button.resize(32, 32)
|
||||
self.mute_button.move(
|
||||
int(self.width() - self.mute_button.size().width() / 2),
|
||||
int(self.height() - self.mute_button.size().height() / 2),
|
||||
)
|
||||
self.mute_button.hide()
|
||||
# self.fullscreen_button = QSvgWidget('./tagstudio/resources/pause.svg', self)
|
||||
# self.fullscreen_button.setMouseTracking(True)
|
||||
# self.fullscreen_button.installEventFilter(self)
|
||||
# self.scene().addWidget(self.fullscreen_button)
|
||||
# self.fullscreen_button.resize(40, 40)
|
||||
# self.fullscreen_button.move(self.fullscreen_button.size().width()/2, self.height() - self.fullscreen_button.size().height()/2)
|
||||
# self.fullscreen_button.hide()
|
||||
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.opener = FileOpenerHelper(filepath=self.filepath)
|
||||
@@ -157,22 +143,17 @@ class VideoPlayer(QGraphicsView):
|
||||
self.driver.settings.sync()
|
||||
|
||||
def checkMediaStatus(self, media_status: QMediaPlayer.MediaStatus) -> None:
|
||||
# logging.info(media_status)
|
||||
if media_status == QMediaPlayer.MediaStatus.EndOfMedia:
|
||||
# Switches current video to with video at filepath. Reason for this is because Pyside6 is dumb and can't handle setting a new source and freezes.
|
||||
# Switches current video to with video at filepath.
|
||||
# Reason for this is because Pyside6 can't handle setting a new source and freezes.
|
||||
# Even if I stop the player before switching, it breaks.
|
||||
# On the plus side, this adds infinite looping for the video preview.
|
||||
self.player.stop()
|
||||
self.player.setSource(QUrl().fromLocalFile(self.filepath))
|
||||
# logging.info(f'Set source to {self.filepath}.')
|
||||
# self.video_preview.setSize(self.resolution)
|
||||
self.player.setPosition(0)
|
||||
# logging.info(f'Set muted to true.')
|
||||
if self.autoplay.isChecked():
|
||||
# logging.info(self.driver.settings.value("autoplay_videos", True, bool))
|
||||
self.player.play()
|
||||
else:
|
||||
# logging.info("Paused")
|
||||
self.player.pause()
|
||||
self.opener.set_filepath(self.filepath)
|
||||
self.keepControlsInPlace()
|
||||
@@ -180,14 +161,14 @@ class VideoPlayer(QGraphicsView):
|
||||
|
||||
def updateControls(self) -> None:
|
||||
if self.player.audioOutput().isMuted():
|
||||
self.mute_button.load("./tagstudio/resources/volume_muted.svg")
|
||||
self.mute_button.load(self.driver.rm.volume_mute_icon)
|
||||
else:
|
||||
self.mute_button.load("./tagstudio/resources/volume_unmuted.svg")
|
||||
self.mute_button.load(self.driver.rm.volume_icon)
|
||||
|
||||
if self.player.isPlaying():
|
||||
self.play_pause.load("./tagstudio/resources/pause.svg")
|
||||
self.play_pause.load(self.driver.rm.pause_icon)
|
||||
else:
|
||||
self.play_pause.load("./tagstudio/resources/play.svg")
|
||||
self.play_pause.load(self.driver.rm.play_icon)
|
||||
|
||||
def wheelEvent(self, event: QWheelEvent) -> None:
|
||||
return
|
||||
@@ -229,8 +210,10 @@ class VideoPlayer(QGraphicsView):
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
def checkIfStillHovered(self) -> None:
|
||||
# Yet again, Pyside6 is dumb. I don't know why, but the HoverLeave event is not triggered sometimes and does not hide the controls.
|
||||
# So, this is a workaround. This is called by a QTimer every 10ms to check if the mouse is still in the video preview.
|
||||
# I don't know why, but the HoverLeave event is not triggered sometimes
|
||||
# and does not hide the controls.
|
||||
# So, this is a workaround. This is called by a QTimer every 10ms to check if the mouse
|
||||
# is still in the video preview.
|
||||
if not self.video_preview.isUnderMouse():
|
||||
self.releaseMouse()
|
||||
else:
|
||||
@@ -240,55 +223,51 @@ class VideoPlayer(QGraphicsView):
|
||||
self.video_tint.setBrush(QBrush(QColor(0, 0, 0, value)))
|
||||
|
||||
def underMouse(self) -> bool:
|
||||
# logging.info("under mouse")
|
||||
self.animation.setStartValue(self.video_tint.brush().color().alpha())
|
||||
self.animation.setEndValue(100)
|
||||
self.animation.setDuration(500)
|
||||
self.animation.setDuration(250)
|
||||
self.animation.start()
|
||||
self.play_pause.show()
|
||||
self.mute_button.show()
|
||||
# self.fullscreen_button.show()
|
||||
self.keepControlsInPlace()
|
||||
self.updateControls()
|
||||
# rcontent = self.contentsRect()
|
||||
# self.setSceneRect(0, 0, rcontent.width(), rcontent.height())
|
||||
|
||||
return super().underMouse()
|
||||
|
||||
def releaseMouse(self) -> None:
|
||||
# logging.info("release mouse")
|
||||
self.animation.setStartValue(self.video_tint.brush().color().alpha())
|
||||
self.animation.setEndValue(0)
|
||||
self.animation.setDuration(500)
|
||||
self.animation.start()
|
||||
self.play_pause.hide()
|
||||
self.mute_button.hide()
|
||||
# self.fullscreen_button.hide()
|
||||
|
||||
return super().releaseMouse()
|
||||
|
||||
def resetControlsToDefault(self) -> None:
|
||||
# Resets the video controls to their default state.
|
||||
self.play_pause.load("./tagstudio/resources/pause.svg")
|
||||
self.mute_button.load("./tagstudio/resources/volume_muted.svg")
|
||||
self.play_pause.load(self.driver.rm.pause_icon)
|
||||
self.mute_button.load(self.driver.rm.volume_mute_icon)
|
||||
|
||||
def pauseToggle(self) -> None:
|
||||
if self.player.isPlaying():
|
||||
self.player.pause()
|
||||
self.play_pause.load("./tagstudio/resources/play.svg")
|
||||
self.play_pause.load(self.driver.rm.play_icon)
|
||||
else:
|
||||
self.player.play()
|
||||
self.play_pause.load("./tagstudio/resources/pause.svg")
|
||||
self.play_pause.load(self.driver.rm.pause_icon)
|
||||
|
||||
def muteToggle(self) -> None:
|
||||
if self.player.audioOutput().isMuted():
|
||||
self.player.audioOutput().setMuted(False)
|
||||
self.mute_button.load("./tagstudio/resources/volume_unmuted.svg")
|
||||
self.mute_button.load(self.driver.rm.volume_icon)
|
||||
else:
|
||||
self.player.audioOutput().setMuted(True)
|
||||
self.mute_button.load("./tagstudio/resources/volume_muted.svg")
|
||||
self.mute_button.load(self.driver.rm.volume_mute_icon)
|
||||
|
||||
def play(self, filepath: str, resolution: QSize) -> None:
|
||||
# Sets the filepath and sends the current player position to the very end, so that the new video can be played.
|
||||
# self.player.audioOutput().setMuted(True)
|
||||
# Sets the filepath and sends the current player position to the very end,
|
||||
# so that the new video can be played.
|
||||
logging.info(f"Playing {filepath}")
|
||||
self.resolution = resolution
|
||||
self.filepath = filepath
|
||||
@@ -297,7 +276,6 @@ class VideoPlayer(QGraphicsView):
|
||||
self.player.play()
|
||||
else:
|
||||
self.checkMediaStatus(QMediaPlayer.MediaStatus.EndOfMedia)
|
||||
# logging.info(f"Successfully stopped.")
|
||||
|
||||
def stop(self) -> None:
|
||||
self.filepath = None
|
||||
@@ -310,10 +288,10 @@ class VideoPlayer(QGraphicsView):
|
||||
0, 0, self.video_preview.size().width(), self.video_preview.size().height()
|
||||
)
|
||||
|
||||
rcontent = self.contentsRect()
|
||||
contents = self.contentsRect()
|
||||
self.centerOn(self.video_preview)
|
||||
self.roundCorners()
|
||||
self.setSceneRect(0, 0, rcontent.width(), rcontent.height())
|
||||
self.setSceneRect(0, 0, contents.width(), contents.height())
|
||||
self.keepControlsInPlace()
|
||||
|
||||
def roundCorners(self) -> None:
|
||||
@@ -346,7 +324,6 @@ class VideoPlayer(QGraphicsView):
|
||||
int(self.width() - self.mute_button.size().width() - 10),
|
||||
int(self.height() - self.mute_button.size().height() - 10),
|
||||
)
|
||||
# self.fullscreen_button.move(-self.fullscreen_button.size().width()-10, self.height() - self.fullscreen_button.size().height()-10)
|
||||
|
||||
def resizeEvent(self, event: QResizeEvent) -> None:
|
||||
# Keeps the video preview in the center of the screen.
|
||||
@@ -358,7 +335,6 @@ class VideoPlayer(QGraphicsView):
|
||||
)
|
||||
)
|
||||
return
|
||||
# return super().resizeEvent(event)\
|
||||
|
||||
|
||||
class VideoPreview(QGraphicsVideoItem):
|
||||
@@ -367,7 +343,8 @@ class VideoPreview(QGraphicsVideoItem):
|
||||
|
||||
def paint(self, painter, option, widget):
|
||||
# painter.brush().setColor(QColor(0, 0, 0, 255))
|
||||
# You can set any shape you want here. RoundedRect is the standard rectangle with rounded corners
|
||||
# You can set any shape you want here.
|
||||
# RoundedRect is the standard rectangle with rounded corners.
|
||||
# With 2nd and 3rd parameter you can tweak the curve until you get what you expect
|
||||
|
||||
super().paint(painter, option, widget)
|
||||
|
||||