pax_global_header00006660000000000000000000000064142617174360014525gustar00rootroot0000000000000052 comment=1f1dfa88dc5f6ffe13f4de514c884ab9c83bd7ce dupeguru-4.3.1/000077500000000000000000000000001426171743600133725ustar00rootroot00000000000000dupeguru-4.3.1/.ctags000066400000000000000000000001021426171743600144650ustar00rootroot00000000000000-R --exclude=build --exclude=env --exclude=.tox --python-kinds=-i dupeguru-4.3.1/.github/000077500000000000000000000000001426171743600147325ustar00rootroot00000000000000dupeguru-4.3.1/.github/FUNDING.yml000066400000000000000000000013371426171743600165530ustar00rootroot00000000000000# These are supported funding model platforms github: arsenetar patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] dupeguru-4.3.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001426171743600171155ustar00rootroot00000000000000dupeguru-4.3.1/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000013761426171743600216160ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. Windows 10 / OSX 10.15 / Ubuntu 20.04 / Arch Linux] - Version [e.g. 4.1.0] **Additional context** Add any other context about the problem here. You may include the debug log although it is normally best to attach it as a file. dupeguru-4.3.1/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011301426171743600226350ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: feature assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. dupeguru-4.3.1/.github/workflows/000077500000000000000000000000001426171743600167675ustar00rootroot00000000000000dupeguru-4.3.1/.github/workflows/codeql-analysis.yml000066400000000000000000000026301426171743600226030ustar00rootroot00000000000000name: "CodeQL" on: push: branches: [master] pull_request: # The branches below must be a subset of the branches above branches: [master] schedule: - cron: "24 20 * * 2" jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: ["cpp", "python"] steps: - name: Checkout repository uses: actions/checkout@v2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main - if: matrix.language == 'cpp' name: Build Cpp run: | sudo apt-get update sudo apt-get install python3-pyqt5 make modules - if: matrix.language == 'python' name: Autobuild uses: github/codeql-action/autobuild@v1 # Analysis - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 dupeguru-4.3.1/.github/workflows/default.yml000066400000000000000000000045271426171743600211460ustar00rootroot00000000000000# Workflow lints, and checks format in parallel then runs tests on all platforms name: Default CI/CD on: push: branches: [master] pull_request: branches: [master] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python 3.10 uses: actions/setup-python@v2 with: python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-extra.txt - name: Lint with flake8 run: | flake8 . format: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python 3.10 uses: actions/setup-python@v2 with: python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-extra.txt - name: Check format with black run: | black . test: needs: [lint, format] runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: [3.7, 3.8, 3.9, "3.10"] exclude: - os: macos-latest python-version: 3.7 - os: macos-latest python-version: 3.8 - os: macos-latest python-version: 3.9 - os: windows-latest python-version: 3.7 - os: windows-latest python-version: 3.8 - os: windows-latest python-version: 3.9 steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-extra.txt - name: Build python modules run: | python build.py --modules - name: Run tests run: | pytest core hscommon - name: Upload Artifacts if: matrix.os == 'ubuntu-latest' uses: actions/upload-artifact@v3 with: name: modules ${{ matrix.python-version }} path: ${{ github.workspace }}/**/*.so dupeguru-4.3.1/.gitignore000066400000000000000000000026031426171743600153630ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo #*.pot # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Environments .env .venv env*/ venv/ ENV/ env.bak/ venv.bak/ # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # macOS .DS_Store # Visual Studio Code .vscode/* !.vscode/settings.json #!.vscode/tasks.json #!.vscode/launch.json !.vscode/extensions.json !.vscode/*.code-snippets # Local History for Visual Studio Code .history/ # Built Visual Studio Code Extensions *.vsix # dupeGuru Specific /qt/*_rc.py /help/*/conf.py /help/*/changelog.rst cocoa/autogen /cocoa/*/Info.plist /cocoa/*/build *.waf* .lock-waf* /tagsdupeguru-4.3.1/.sonarcloud.properties000066400000000000000000000000501426171743600177320ustar00rootroot00000000000000sonar.python.version=3.7, 3.8, 3.9, 3.10dupeguru-4.3.1/.tx/000077500000000000000000000000001426171743600141035ustar00rootroot00000000000000dupeguru-4.3.1/.tx/config000066400000000000000000000007561426171743600153030ustar00rootroot00000000000000[main] host = https://www.transifex.com [o:voltaicideas:p:dupeguru-1:r:columns] file_filter = locale//LC_MESSAGES/columns.po source_file = locale/columns.pot source_lang = en type = PO [o:voltaicideas:p:dupeguru-1:r:core] file_filter = locale//LC_MESSAGES/core.po source_file = locale/core.pot source_lang = en type = PO [o:voltaicideas:p:dupeguru-1:r:ui] file_filter = locale//LC_MESSAGES/ui.po source_file = locale/ui.pot source_lang = en type = PO dupeguru-4.3.1/.vscode/000077500000000000000000000000001426171743600147335ustar00rootroot00000000000000dupeguru-4.3.1/.vscode/extensions.json000066400000000000000000000005421426171743600200260ustar00rootroot00000000000000{ // List of extensions which should be recommended for users of this workspace. "recommendations": [ "redhat.vscode-yaml", "ms-python.vscode-pylance", "ms-python.python" ], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": [] }dupeguru-4.3.1/.vscode/settings.json000066400000000000000000000004601426171743600174660ustar00rootroot00000000000000{ "python.formatting.provider": "black", "cSpell.words": [ "Dupras", "hscommon" ], "python.languageServer": "Pylance", "yaml.schemaStore.enable": true, "yaml.schemas": { "https://json.schemastore.org/github-workflow.json": ".github/workflows/*.yml" } }dupeguru-4.3.1/CONTRIBUTING.md000066400000000000000000000124651426171743600156330ustar00rootroot00000000000000# Contributing to dupeGuru The following is a set of guidelines and information for contributing to dupeGuru. #### Table of Contents [Things to Know Before Starting](#things-to-know-before-starting) [Ways to Contribute](#ways-to-contribute) * [Reporting Bugs](#reporting-bugs) * [Suggesting Enhancements](#suggesting-enhancements) * [Localization](#localization) * [Code Contribution](#code-contribution) * [Pull Requests](#pull-requests) [Style Guides](#style-guides) * [Git Commit Messages](#git-commit-messages) * [Python Style Guide](#python-style-guide) * [Documentation Style Guide](#documentation-style-guide) [Additional Notes](#additional-notes) * [Issue and Pull Request Labels](#issue-and-pull-request-labels) ## Things to Know Before Starting **TODO** ## Ways to contribute ### Reporting Bugs **TODO** ### Suggesting Enhancements **TODO** ### Localization **TODO** ### Code Contribution **TODO** ### Pull Requests Please follow these steps to have your contribution considered by the maintainers: 1. Keep Pull Request specific to one feature or bug. 2. Follow the [style guides](#style-guides) 3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing
What if the status checks are failing?If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.
While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. ## Style Guides ### Git Commit Messages - Use the present tense ("Add feature" not "Added feature") - Use the imperative mood ("Move cursor to..." not "Moves cursor to...") - Limit the first line to 72 characters or less - Reference issues and pull requests liberally after the first line ### Python Style Guide - All files are formatted with [Black](https://github.com/psf/black) - Follow [PEP 8](https://peps.python.org/pep-0008/) as much as practical - Pass [flake8](https://flake8.pycqa.org/en/latest/) linting - Include [PEP 484](https://peps.python.org/pep-0484/) type hints (new code) ### Documentation Style Guide **TODO** ## Additional Notes ### Issue and Pull Request Labels This section lists and describes the various labels used with issues and pull requests. Each of the labels is listed with a search link as well. #### Issue Type and Status | Label name | Search | Description | |------------|--------|-------------| | `enhancement` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement) | Feature requests and enhancements. | | `bug` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Abug) | Bug reports. | | `duplicate` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Aduplicate) | Issue is a duplicate of existing issue. | | `needs-reproduction` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Aneeds-reproduction) | A bug that has not been able to be reproduced. | | `needs-information` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Aneeds-information) | More information needs to be collected about these problems or feature requests (e.g. steps to reproduce). | | `blocked` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Ablocked) | Issue blocked by other issues. | | `beginner` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner) | Less complex issues for users who want to start contributing. | #### Category Labels | Label name | Search | Description | |------------|--------|-------------| | `3rd party` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3A%223rd%20party%22) | Related to a 3rd party dependency. | | `crash` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Acrash) | Related to crashes (complete, or unhandled). | | `documentation` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Adocumentation) | Related to any documentation. | | `linux` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3linux) | Related to running on Linux. | | `mac` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Amac) | Related to running on macOS. | | `performance` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Aperformance) | Related to the performance. | | `ui` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Aui)| Related to the visual design. | | `windows` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Awindows) | Related to running on Windows. | #### Pull Request Labels None at this time, if the volume of Pull Requests increase labels may be added to manage. dupeguru-4.3.1/CREDITS000066400000000000000000000015471426171743600144210ustar00rootroot00000000000000To know who contributed to dupeGuru, you can look at the commit log, but not all contributions result in a commit. This file lists contributors who don't necessarily appear in the commit log. * Jason Cho, Exchange icon * schollidesign (https://findicons.com/pack/1035/human_o2), Zoom-in, Zoom-out, Zoom-best-fit, Zoom-original icons * Jérôme Cantin, Main icon * Gregor Tätzner, German localization * Frank Weber, German localization * Eric Dee, Chinese localization * Aleš Nehyba, Czech localization * Paolo Rossi, Italian localization * Hrant Ohanyan, Armenian localization * Igor Pavlov, Russian localization * Kyrill Detinov, Russian localization * Yuri Petrashko, Ukrainian localization * Nickolas Pohilets, Ukrainian localization * Victor Figueiredo, Brazilian localization * Phan Anh, Vietnamese localization * Gabriel Koutilellis, Greek localization Thanks! dupeguru-4.3.1/LICENSE000066400000000000000000000773311426171743600144120ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS dupeguru-4.3.1/MANIFEST.in000066400000000000000000000001351426171743600151270ustar00rootroot00000000000000recursive-include core *.h recursive-include core *.m include run.py graft locale graft help dupeguru-4.3.1/Makefile000066400000000000000000000072011426171743600150320ustar00rootroot00000000000000PYTHON ?= python3 PYTHON_VERSION_MINOR := $(shell ${PYTHON} -c "import sys; print(sys.version_info.minor)") PYRCC5 ?= pyrcc5 REQ_MINOR_VERSION = 7 PREFIX ?= /usr/local # Window compatability via Msys2 # - venv creates Scripts instead of bin # - compile generates .pyd instead of .so # - venv with --sytem-site-packages has issues on windows as well... ifeq ($(shell ${PYTHON} -c "import platform; print(platform.system())"), Windows) BIN = Scripts SO = *.pyd VENV_OPTIONS = else BIN = bin SO = *.so VENV_OPTIONS = --system-site-packages endif # Set this variable if all dependencies are already met on the system. We will then avoid the # whole vitualenv creation and pip install dance. NO_VENV ?= ifdef NO_VENV VENV_PYTHON = $(PYTHON) else VENV_PYTHON = ./env/$(BIN)/python endif # If you're installing into a path that is not going to be the final path prefix (such as a # sandbox), set DESTDIR to that path. # Our build scripts are not very "make like" yet and perform their task in a bundle. For now, we # use one of each file to act as a representative, a target, of these groups. packages = hscommon core qt localedirs = $(wildcard locale/*/LC_MESSAGES) pofiles = $(wildcard locale/*/LC_MESSAGES/*.po) mofiles = $(patsubst %.po,%.mo,$(pofiles)) vpath %.po $(localedirs) vpath %.mo $(localedirs) all: | env i18n modules qt/dg_rc.py @echo "Build complete! You can run dupeGuru with 'make run'" run: $(VENV_PYTHON) run.py pyc: | env ${VENV_PYTHON} -m compileall ${packages} reqs: ifneq ($(shell test $(PYTHON_VERSION_MINOR) -ge $(REQ_MINOR_VERSION); echo $$?),0) $(error "Python 3.${REQ_MINOR_VERSION}+ required. Aborting.") endif ifndef NO_VENV @${PYTHON} -m venv -h > /dev/null || \ echo "Creation of our virtualenv failed. If you're on Ubuntu, you probably need python3-venv." endif @${PYTHON} -c 'import PyQt5' >/dev/null 2>&1 || \ { echo "PyQt 5.4+ required. Install it and try again. Aborting"; exit 1; } env: | reqs ifndef NO_VENV @echo "Creating our virtualenv" ${PYTHON} -m venv env $(VENV_PYTHON) -m pip install -r requirements.txt # We can't use the "--system-site-packages" flag on creation because otherwise we end up with # the system's pip and that messes up things in some cases (notably in Gentoo). ${PYTHON} -m venv --upgrade ${VENV_OPTIONS} env endif build/help: | env $(VENV_PYTHON) build.py --doc qt/dg_rc.py: qt/dg.qrc $(PYRCC5) qt/dg.qrc > qt/dg_rc.py i18n: $(mofiles) %.mo: %.po msgfmt -o $@ $< modules: | env $(VENV_PYTHON) build.py --modules mergepot: | env $(VENV_PYTHON) build.py --mergepot normpo: | env $(VENV_PYTHON) build.py --normpo install: all pyc mkdir -p ${DESTDIR}${PREFIX}/share/dupeguru cp -rf ${packages} locale ${DESTDIR}${PREFIX}/share/dupeguru cp -f run.py ${DESTDIR}${PREFIX}/share/dupeguru/run.py chmod 755 ${DESTDIR}${PREFIX}/share/dupeguru/run.py mkdir -p ${DESTDIR}${PREFIX}/bin ln -sf ${PREFIX}/share/dupeguru/run.py ${DESTDIR}${PREFIX}/bin/dupeguru mkdir -p ${DESTDIR}${PREFIX}/share/applications cp -f pkg/dupeguru.desktop ${DESTDIR}${PREFIX}/share/applications mkdir -p ${DESTDIR}${PREFIX}/share/pixmaps cp -f images/dgse_logo_128.png ${DESTDIR}${PREFIX}/share/pixmaps/dupeguru.png installdocs: build/help mkdir -p ${DESTDIR}${PREFIX}/share/dupeguru cp -rf build/help ${DESTDIR}${PREFIX}/share/dupeguru uninstall: rm -rf "${DESTDIR}${PREFIX}/share/dupeguru" rm -f "${DESTDIR}${PREFIX}/bin/dupeguru" rm -f "${DESTDIR}${PREFIX}/share/applications/dupeguru.desktop" rm -f "${DESTDIR}${PREFIX}/share/pixmaps/dupeguru.png" clean: -rm -rf build -rm locale/*/LC_MESSAGES/*.mo -rm core/pe/*.$(SO) qt/pe/*.$(SO) .PHONY: clean normpo mergepot modules i18n reqs run pyc install uninstall all dupeguru-4.3.1/README.md000066400000000000000000000077221426171743600146610ustar00rootroot00000000000000# dupeGuru [dupeGuru][dupeguru] is a cross-platform (Linux, OS X, Windows) GUI tool to find duplicate files in a system. It is written mostly in Python 3 and uses [qt](https://www.qt.io/) for the UI. ## Current status Still looking for additional help especially with regards to: * OSX maintenance: reproducing bugs, packaging verification. * Linux maintenance: reproducing bugs, maintaining PPA repository, Debian package, rpm package. * Translations: updating missing strings, transifex project at https://www.transifex.com/voltaicideas/dupeguru-1 * Documentation: keeping it up-to-date. ## Contents of this folder This folder contains the source for dupeGuru. Its documentation is in `help`, but is also [available online][documentation] in its built form. Here's how this source tree is organized: * core: Contains the core logic code for dupeGuru. It's Python code. * qt: UI code for the Qt toolkit. It's written in Python and uses PyQt. * images: Images used by the different UI codebases. * pkg: Skeleton files required to create different packages * help: Help document, written for Sphinx. * locale: .po files for localization. * hscommon: A collection of helpers used across HS applications. ## How to build dupeGuru from source ### Windows & macOS specific additional instructions For windows instructions see the [Windows Instructions](Windows.md). For macos instructions (qt version) see the [macOS Instructions](macos.md). ### Prerequisites * [Python 3.7+][python] * PyQt5 ### System Setup When running in a linux based environment the following system packages or equivalents are needed to build: * python3-pyqt5 * pyqt5-dev-tools (on some systems, see note) * python3-venv (only if using a virtual environment) * python3-dev * build-essential Note: On some linux systems pyrcc5 is not put on the path when installing python3-pyqt5, this will cause some issues with the resource files (and icons). These systems should have a respective pyqt5-dev-tools package, which should also be installed. The presence of pyrcc5 can be checked with `which pyrcc5`. Debian based systems need the extra package, and Arch does not. To create packages the following are also needed: * python3-setuptools * debhelper ### Building with Make dupeGuru comes with a makefile that can be used to build and run: $ make && make run ### Building without Make $ cd $ python3 -m venv --system-site-packages ./env $ source ./env/bin/activate $ pip install -r requirements.txt $ python build.py $ python run.py ### Generating Debian/Ubuntu package To generate packages the extra requirements in requirements-extra.txt must be installed, the steps are as follows: $ cd $ python3 -m venv --system-site-packages ./env $ source ./env/bin/activate $ pip install -r requirements.txt -r requirements-extra.txt $ python build.py --clean $ python package.py This can be made a one-liner (once in the directory) as: $ bash -c "python3 -m venv --system-site-packages env && source env/bin/activate && pip install -r requirements.txt -r requirements-extra.txt && python build.py --clean && python package.py" ## Running tests The complete test suite is run with [Tox 1.7+][tox]. If you have it installed system-wide, you don't even need to set up a virtualenv. Just `cd` into the root project folder and run `tox`. If you don't have Tox system-wide, install it in your virtualenv with `pip install tox` and then run `tox`. You can also run automated tests without Tox. Extra requirements for running tests are in `requirements-extra.txt`. So, you can do `pip install -r requirements-extra.txt` inside your virtualenv and then `py.test core hscommon` [dupeguru]: https://dupeguru.voltaicideas.net/ [cross-toolkit]: http://www.hardcoded.net/articles/cross-toolkit-software [documentation]: http://dupeguru.voltaicideas.net/help/en/ [python]: http://www.python.org/ [pyqt]: http://www.riverbankcomputing.com [tox]: https://tox.readthedocs.org/en/latest/ dupeguru-4.3.1/Windows.md000066400000000000000000000063411426171743600153520ustar00rootroot00000000000000## How to build dupeGuru for Windows ### Prerequisites - [Python 3.7+][python] - [Visual Studio 2019][vs] or [Visual Studio Build Tools 2019][vsBuildTools] with the Windows 10 SDK - [nsis][nsis] (for installer creation) - [msys2][msys2] (for using makefile method) NOTE: When installing Visual Studio or the Visual Studio Build Tools with the Windows 10 SDK on versions of Windows below 10 be sure to make sure that the Universal CRT is installed before installing Visual studio as noted in the [Windows 10 SDK Notes][win10sdk] and found at [KB2999226][KB2999226]. After installing python it is recommended to update setuptools before compiling packages. To update run (example is for python launcher and 3.8): $ py -3.8 -m pip install --upgrade setuptools More details on setting up python for compiling packages on windows can be found on the [python wiki][pythonWindowsCompilers] Take note of the required vc++ versions. ### With build.py (preferred) To build with a different python version 3.7 vs 3.8 or 32 bit vs 64 bit specify that version instead of -3.8 to the `py` command below. If you want to build additional versions while keeping all virtual environments setup use a different location for each virtual environment. $ cd $ py -3.8 -m venv .\env $ .\env\Scripts\activate $ pip install -r requirements.txt $ python build.py $ python run.py ### With makefile It is possible to build dupeGuru with the makefile on windows using a compatable POSIX environment. The following steps have been tested using [msys2][msys2]. Before running make: 1. Install msys2 or other POSIX environment 2. Install PyQt5 globally via pip 3. Use the respective console for msys2 it is `msys2 msys` Then the following execution of the makefile should work. Pass the correct value for PYTHON to the makefile if not on the path as python3. $ cd $ make PYTHON='py -3.8' $ make run ### Generate Windows Installer Packages You need to use the respective x86 or x64 version of python to build the 32 bit and 64 bit versions. The build scripts will automatically detect the python architecture for you. When using build.py make sure the resulting python works before continuing to package.py. NOTE: package.py looks for the 'makensis' executable in the default location for a 64 bit windows system. The extra requirements need to be installed to run packaging: `pip install -r requirements-extra.txt`. Run the following in the respective virtual environment. $ python package.py ### Running tests The complete test suite can be run with tox just like on linux. NOTE: The extra requirements need to be installed to run unit tests: `pip install -r requirements-extra.txt`. [python]: http://www.python.org/ [nsis]: http://nsis.sourceforge.net/Main_Page [vs]: https://www.visualstudio.com/downloads/#visual-studio-community-2019 [vsBuildTools]: https://www.visualstudio.com/downloads/#build-tools-for-visual-studio-2019 [win10sdk]: https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk [KB2999226]: https://support.microsoft.com/en-us/help/2999226/update-for-universal-c-runtime-in-windows [pythonWindowsCompilers]: https://wiki.python.org/moin/WindowsCompilers [msys2]: http://www.msys2.org/ dupeguru-4.3.1/build.py000066400000000000000000000113331426171743600150440ustar00rootroot00000000000000# Copyright 2017 Virgil Dupras # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from pathlib import Path import sys from optparse import OptionParser import shutil from multiprocessing import Pool from setuptools import sandbox from hscommon import sphinxgen from hscommon.build import ( add_to_pythonpath, print_and_do, fix_qt_resource_file, ) from hscommon import loc def parse_args(): usage = "usage: %prog [options]" parser = OptionParser(usage=usage) parser.add_option( "--clean", action="store_true", dest="clean", help="Clean build folder before building", ) parser.add_option("--doc", action="store_true", dest="doc", help="Build only the help file (en)") parser.add_option("--alldoc", action="store_true", dest="all_doc", help="Build only the help file in all languages") parser.add_option("--loc", action="store_true", dest="loc", help="Build only localization") parser.add_option( "--updatepot", action="store_true", dest="updatepot", help="Generate .pot files from source code.", ) parser.add_option( "--mergepot", action="store_true", dest="mergepot", help="Update all .po files based on .pot files.", ) parser.add_option( "--normpo", action="store_true", dest="normpo", help="Normalize all PO files (do this before commit).", ) parser.add_option( "--modules", action="store_true", dest="modules", help="Build the python modules.", ) (options, args) = parser.parse_args() return options def build_one_help(language): print(f"Generating Help in {language}") current_path = Path(".").absolute() changelog_path = current_path.joinpath("help", "changelog") tixurl = "https://github.com/arsenetar/dupeguru/issues/{}" changelogtmpl = current_path.joinpath("help", "changelog.tmpl") conftmpl = current_path.joinpath("help", "conf.tmpl") help_basepath = current_path.joinpath("help", language) help_destpath = current_path.joinpath("build", "help", language) confrepl = {"language": language} sphinxgen.gen( help_basepath, help_destpath, changelog_path, tixurl, confrepl, conftmpl, changelogtmpl, ) def build_help(): languages = ["en", "de", "fr", "hy", "ru", "uk"] # Running with Pools as for some reason sphinx seems to cross contaminate the output otherwise with Pool(len(languages)) as p: p.map(build_one_help, languages) def build_localizations(): loc.compile_all_po("locale") locale_dest = Path("build", "locale") if locale_dest.exists(): shutil.rmtree(locale_dest) shutil.copytree("locale", locale_dest, ignore=shutil.ignore_patterns("*.po", "*.pot")) def build_updatepot(): print("Building .pot files from source files") print("Building core.pot") loc.generate_pot(["core"], Path("locale", "core.pot"), ["tr"]) print("Building columns.pot") loc.generate_pot(["core"], Path("locale", "columns.pot"), ["coltr"]) print("Building ui.pot") loc.generate_pot(["qt"], Path("locale", "ui.pot"), ["tr"], merge=True) def build_mergepot(): print("Updating .po files using .pot files") loc.merge_pots_into_pos("locale") def build_normpo(): loc.normalize_all_pos("locale") def build_pe_modules(): print("Building PE Modules") # Leverage setup.py to build modules sandbox.run_setup("setup.py", ["build_ext", "--inplace"]) def build_normal(): print("Building dupeGuru with UI qt") add_to_pythonpath(".") print("Building dupeGuru") build_pe_modules() print("Building localizations") build_localizations() print("Building Qt stuff") print_and_do("pyrcc5 {} > {}".format(Path("qt", "dg.qrc"), Path("qt", "dg_rc.py"))) fix_qt_resource_file(Path("qt", "dg_rc.py")) build_help() def main(): if sys.version_info < (3, 7): sys.exit("Python < 3.7 is unsupported.") options = parse_args() if options.clean and Path("build").exists(): shutil.rmtree("build") if not Path("build").exists(): Path("build").mkdir() if options.doc: build_one_help("en") elif options.all_doc: build_help() elif options.loc: build_localizations() elif options.updatepot: build_updatepot() elif options.mergepot: build_mergepot() elif options.normpo: build_normpo() elif options.modules: build_pe_modules() else: build_normal() if __name__ == "__main__": main() dupeguru-4.3.1/core/000077500000000000000000000000001426171743600143225ustar00rootroot00000000000000dupeguru-4.3.1/core/__init__.py000066400000000000000000000000571426171743600164350ustar00rootroot00000000000000__version__ = "4.3.1" __appname__ = "dupeGuru" dupeguru-4.3.1/core/app.py000066400000000000000000001057071426171743600154660ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import cProfile import datetime import os import os.path as op import logging import subprocess import re import shutil from pathlib import Path from send2trash import send2trash from hscommon.jobprogress import job from hscommon.notify import Broadcaster from hscommon.conflict import smart_move, smart_copy from hscommon.gui.progress_window import ProgressWindow from hscommon.util import delete_if_empty, first, escape, nonone, allsame from hscommon.trans import tr from hscommon import desktop from core import se, me, pe from core.pe.photo import get_delta_dimensions from core.util import cmp_value, fix_surrogate_encoding from core import directories, results, export, fs, prioritize from core.ignore import IgnoreList from core.exclude import ExcludeDict as ExcludeList from core.scanner import ScanType from core.gui.deletion_options import DeletionOptions from core.gui.details_panel import DetailsPanel from core.gui.directory_tree import DirectoryTree from core.gui.ignore_list_dialog import IgnoreListDialog from core.gui.exclude_list_dialog import ExcludeListDialogCore from core.gui.problem_dialog import ProblemDialog from core.gui.stats_label import StatsLabel HAD_FIRST_LAUNCH_PREFERENCE = "HadFirstLaunch" DEBUG_MODE_PREFERENCE = "DebugMode" MSG_NO_MARKED_DUPES = tr("There are no marked duplicates. Nothing has been done.") MSG_NO_SELECTED_DUPES = tr("There are no selected duplicates. Nothing has been done.") MSG_MANY_FILES_TO_OPEN = tr( "You're about to open many files at once. Depending on what those " "files are opened with, doing so can create quite a mess. Continue?" ) class DestType: DIRECT = 0 RELATIVE = 1 ABSOLUTE = 2 class JobType: SCAN = "job_scan" LOAD = "job_load" MOVE = "job_move" COPY = "job_copy" DELETE = "job_delete" class AppMode: STANDARD = 0 MUSIC = 1 PICTURE = 2 JOBID2TITLE = { JobType.SCAN: tr("Scanning for duplicates"), JobType.LOAD: tr("Loading"), JobType.MOVE: tr("Moving"), JobType.COPY: tr("Copying"), JobType.DELETE: tr("Sending to Trash"), } class DupeGuru(Broadcaster): """Holds everything together. Instantiated once per running application, it holds a reference to every high-level object whose reference needs to be held: :class:`~core.results.Results`, :class:`~core.directories.Directories`, :mod:`core.gui` instances, etc.. It also hosts high level methods and acts as a coordinator for all those elements. This is why some of its methods seem a bit shallow, like for example :meth:`mark_all` and :meth:`remove_duplicates`. These methos are just proxies for a method in :attr:`results`, but they are also followed by a notification call which is very important if we want GUI elements to be correctly notified of a change in the data they're presenting. .. attribute:: directories Instance of :class:`~core.directories.Directories`. It holds the current folder selection. .. attribute:: results Instance of :class:`core.results.Results`. Holds the results of the latest scan. .. attribute:: selected_dupes List of currently selected dupes from our :attr:`results`. Whenever the user changes its selection at the UI level, :attr:`result_table` takes care of updating this attribute, so you can trust that it's always up-to-date. .. attribute:: result_table Instance of :mod:`meta-gui ` table listing the results from :attr:`results` """ # --- View interface # get_default(key_name) # set_default(key_name, value) # show_message(msg) # open_url(url) # open_path(path) # reveal_path(path) # ask_yes_no(prompt) --> bool # create_results_window() # show_results_window() # show_problem_dialog() # select_dest_folder(prompt: str) --> str # select_dest_file(prompt: str, ext: str) --> str NAME = PROMPT_NAME = "dupeGuru" PICTURE_CACHE_TYPE = "sqlite" # set to 'shelve' for a ShelveCache def __init__(self, view, portable=False): if view.get_default(DEBUG_MODE_PREFERENCE): logging.getLogger().setLevel(logging.DEBUG) logging.debug("Debug mode enabled") Broadcaster.__init__(self) self.view = view self.appdata = desktop.special_folder_path(desktop.SpecialFolder.APPDATA, portable=portable) if not op.exists(self.appdata): os.makedirs(self.appdata) self.app_mode = AppMode.STANDARD self.discarded_file_count = 0 self.exclude_list = ExcludeList() hash_cache_file = op.join(self.appdata, "hash_cache.db") fs.filesdb.connect(hash_cache_file) self.directories = directories.Directories(self.exclude_list) self.results = results.Results(self) self.ignore_list = IgnoreList() # In addition to "app-level" options, this dictionary also holds options that will be # sent to the scanner. They don't have default values because those defaults values are # defined in the scanner class. self.options = { "escape_filter_regexp": True, "clean_empty_dirs": False, "ignore_hardlink_matches": False, "copymove_dest_type": DestType.RELATIVE, "picture_cache_type": self.PICTURE_CACHE_TYPE, } self.selected_dupes = [] self.details_panel = DetailsPanel(self) self.directory_tree = DirectoryTree(self) self.problem_dialog = ProblemDialog(self) self.ignore_list_dialog = IgnoreListDialog(self) self.exclude_list_dialog = ExcludeListDialogCore(self) self.stats_label = StatsLabel(self) self.result_table = None self.deletion_options = DeletionOptions() self.progress_window = ProgressWindow(self._job_completed, self._job_error) children = [self.directory_tree, self.stats_label, self.details_panel] for child in children: child.connect() # --- Private def _recreate_result_table(self): if self.result_table is not None: self.result_table.disconnect() if self.app_mode == AppMode.PICTURE: self.result_table = pe.result_table.ResultTable(self) elif self.app_mode == AppMode.MUSIC: self.result_table = me.result_table.ResultTable(self) else: self.result_table = se.result_table.ResultTable(self) self.result_table.connect() self.view.create_results_window() def _get_picture_cache_path(self): cache_type = self.options["picture_cache_type"] cache_name = "cached_pictures.shelve" if cache_type == "shelve" else "cached_pictures.db" return op.join(self.appdata, cache_name) def _get_dupe_sort_key(self, dupe, get_group, key, delta): if self.app_mode in (AppMode.MUSIC, AppMode.PICTURE) and key == "folder_path": dupe_folder_path = getattr(dupe, "display_folder_path", dupe.folder_path) return str(dupe_folder_path).lower() if self.app_mode == AppMode.PICTURE and delta and key == "dimensions": r = cmp_value(dupe, key) ref_value = cmp_value(get_group().ref, key) return get_delta_dimensions(r, ref_value) if key == "marked": return self.results.is_marked(dupe) if key == "percentage": m = get_group().get_match_of(dupe) return m.percentage elif key == "dupe_count": return 0 else: result = cmp_value(dupe, key) if delta: refval = cmp_value(get_group().ref, key) if key in self.result_table.DELTA_COLUMNS: result -= refval else: same = cmp_value(dupe, key) == refval result = (same, result) return result def _get_group_sort_key(self, group, key): if self.app_mode in (AppMode.MUSIC, AppMode.PICTURE) and key == "folder_path": dupe_folder_path = getattr(group.ref, "display_folder_path", group.ref.folder_path) return str(dupe_folder_path).lower() if key == "percentage": return group.percentage if key == "dupe_count": return len(group) if key == "marked": return len([dupe for dupe in group.dupes if self.results.is_marked(dupe)]) return cmp_value(group.ref, key) def _do_delete(self, j, link_deleted, use_hardlinks, direct_deletion): def op(dupe): j.add_progress() return self._do_delete_dupe(dupe, link_deleted, use_hardlinks, direct_deletion) j.start_job(self.results.mark_count) self.results.perform_on_marked(op, True) def _do_delete_dupe(self, dupe, link_deleted, use_hardlinks, direct_deletion): if not dupe.path.exists(): return logging.debug("Sending '%s' to trash", dupe.path) str_path = str(dupe.path) if direct_deletion: if op.isdir(str_path): shutil.rmtree(str_path) else: os.remove(str_path) else: send2trash(str_path) # Raises OSError when there's a problem if link_deleted: group = self.results.get_group_of_duplicate(dupe) ref = group.ref linkfunc = os.link if use_hardlinks else os.symlink linkfunc(str(ref.path), str_path) self.clean_empty_dirs(dupe.path.parent) def _create_file(self, path): # We add fs.Folder to fileclasses in case the file we're loading contains folder paths. return fs.get_file(path, self.fileclasses + [se.fs.Folder]) def _get_file(self, str_path): path = Path(str_path) f = self._create_file(path) if f is None: return None try: f._read_all_info(attrnames=self.METADATA_TO_READ) return f except OSError: return None def _get_export_data(self): columns = [col for col in self.result_table._columns.ordered_columns if col.visible and col.name != "marked"] colnames = [col.display for col in columns] rows = [] for group_id, group in enumerate(self.results.groups): for dupe in group: data = self.get_display_info(dupe, group) row = [fix_surrogate_encoding(data[col.name]) for col in columns] row.insert(0, group_id) rows.append(row) return colnames, rows def _results_changed(self): self.selected_dupes = [d for d in self.selected_dupes if self.results.get_group_of_duplicate(d) is not None] self.notify("results_changed") def _start_job(self, jobid, func, args=()): title = JOBID2TITLE[jobid] try: self.progress_window.run(jobid, title, func, args=args) except job.JobInProgressError: msg = tr( "A previous action is still hanging in there. You can't start a new one yet. Wait " "a few seconds, then try again." ) self.view.show_message(msg) def _job_completed(self, jobid): if jobid == JobType.SCAN: self._results_changed() fs.filesdb.commit() if not self.results.groups: self.view.show_message(tr("No duplicates found.")) else: self.view.show_results_window() if jobid in {JobType.MOVE, JobType.DELETE}: self._results_changed() if jobid == JobType.LOAD: self._recreate_result_table() self._results_changed() self.view.show_results_window() if jobid in {JobType.COPY, JobType.MOVE, JobType.DELETE}: if self.results.problems: self.problem_dialog.refresh() self.view.show_problem_dialog() else: if jobid == JobType.COPY: msg = tr("All marked files were copied successfully.") elif jobid == JobType.MOVE: msg = tr("All marked files were moved successfully.") elif jobid == JobType.DELETE and self.deletion_options.direct: msg = tr("All marked files were deleted successfully.") else: msg = tr("All marked files were successfully sent to Trash.") self.view.show_message(msg) def _job_error(self, jobid, err): if jobid == JobType.LOAD: msg = tr("Could not load file: {}").format(err) self.view.show_message(msg) return False else: raise err @staticmethod def _remove_hardlink_dupes(files): seen_inodes = set() result = [] for file in files: try: inode = file.path.stat().st_ino except OSError: # The file was probably deleted or something continue if inode not in seen_inodes: seen_inodes.add(inode) result.append(file) return result def _select_dupes(self, dupes): if dupes == self.selected_dupes: return self.selected_dupes = dupes self.notify("dupes_selected") # --- Protected def _get_fileclasses(self): if self.app_mode == AppMode.PICTURE: return [pe.photo.PLAT_SPECIFIC_PHOTO_CLASS] elif self.app_mode == AppMode.MUSIC: return [me.fs.MusicFile] else: return [se.fs.File] def _prioritization_categories(self): if self.app_mode == AppMode.PICTURE: return pe.prioritize.all_categories() elif self.app_mode == AppMode.MUSIC: return me.prioritize.all_categories() else: return prioritize.all_categories() # --- Public def add_directory(self, d): """Adds folder ``d`` to :attr:`directories`. Shows an error message dialog if something bad happens. :param str d: path of folder to add """ try: self.directories.add_path(Path(d)) self.notify("directories_changed") except directories.AlreadyThereError: self.view.show_message(tr("'{}' already is in the list.").format(d)) except directories.InvalidPathError: self.view.show_message(tr("'{}' does not exist.").format(d)) def add_selected_to_ignore_list(self): """Adds :attr:`selected_dupes` to :attr:`ignore_list`.""" dupes = self.without_ref(self.selected_dupes) if not dupes: self.view.show_message(MSG_NO_SELECTED_DUPES) return msg = tr("All selected %d matches are going to be ignored in all subsequent scans. Continue?") if not self.view.ask_yes_no(msg % len(dupes)): return for dupe in dupes: g = self.results.get_group_of_duplicate(dupe) for other in g: if other is not dupe: self.ignore_list.ignore(str(other.path), str(dupe.path)) self.remove_duplicates(dupes) self.ignore_list_dialog.refresh() def apply_filter(self, result_filter): """Apply a filter ``filter`` to the results so that it shows only dupe groups that match it. :param str filter: filter to apply """ self.results.apply_filter(None) if self.options["escape_filter_regexp"]: result_filter = escape(result_filter, set("()[]\\.|+?^")) result_filter = escape(result_filter, "*", ".") self.results.apply_filter(result_filter) self._results_changed() def clean_empty_dirs(self, path): if self.options["clean_empty_dirs"]: while delete_if_empty(path, [".DS_Store"]): path = path.parent def clear_picture_cache(self): try: os.remove(self._get_picture_cache_path()) except FileNotFoundError: pass # we don't care def clear_hash_cache(self): fs.filesdb.clear() def copy_or_move(self, dupe, copy: bool, destination: str, dest_type: DestType): source_path = dupe.path location_path = first(p for p in self.directories if p in dupe.path.parents) dest_path = Path(destination) if dest_type in {DestType.RELATIVE, DestType.ABSOLUTE}: # no filename, no windows drive letter source_base = source_path.relative_to(source_path.anchor).parent if dest_type == DestType.RELATIVE: source_base = source_base.relative_to(location_path.relative_to(location_path.anchor)) dest_path = dest_path.joinpath(source_base) if not dest_path.exists(): dest_path.mkdir(parents=True) # Add filename to dest_path. For file move/copy, it's not required, but for folders, yes. dest_path = dest_path.joinpath(source_path.name) logging.debug("Copy/Move operation from '%s' to '%s'", source_path, dest_path) # Raises an EnvironmentError if there's a problem if copy: smart_copy(source_path, dest_path) else: smart_move(source_path, dest_path) self.clean_empty_dirs(source_path.parent) def copy_or_move_marked(self, copy): """Start an async move (or copy) job on marked duplicates. :param bool copy: If True, duplicates will be copied instead of moved """ def do(j): def op(dupe): j.add_progress() self.copy_or_move(dupe, copy, destination, desttype) j.start_job(self.results.mark_count) self.results.perform_on_marked(op, not copy) if not self.results.mark_count: self.view.show_message(MSG_NO_MARKED_DUPES) return destination = self.view.select_dest_folder( tr("Select a directory to copy marked files to") if copy else tr("Select a directory to move marked files to") ) if destination: desttype = self.options["copymove_dest_type"] jobid = JobType.COPY if copy else JobType.MOVE self._start_job(jobid, do) def delete_marked(self): """Start an async job to send marked duplicates to the trash.""" if not self.results.mark_count: self.view.show_message(MSG_NO_MARKED_DUPES) return if not self.deletion_options.show(self.results.mark_count): return args = [ self.deletion_options.link_deleted, self.deletion_options.use_hardlinks, self.deletion_options.direct, ] logging.debug("Starting deletion job with args %r", args) self._start_job(JobType.DELETE, self._do_delete, args=args) def export_to_xhtml(self): """Export current results to XHTML. The configuration of the :attr:`result_table` (columns order and visibility) is used to determine how the data is presented in the export. In other words, the exported table in the resulting XHTML will look just like the results table. """ colnames, rows = self._get_export_data() export_path = export.export_to_xhtml(colnames, rows) desktop.open_path(export_path) def export_to_csv(self): """Export current results to CSV. The columns and their order in the resulting CSV file is determined in the same way as in :meth:`export_to_xhtml`. """ dest_file = self.view.select_dest_file(tr("Select a destination for your exported CSV"), "csv") if dest_file: colnames, rows = self._get_export_data() try: export.export_to_csv(dest_file, colnames, rows) except OSError as e: self.view.show_message(tr("Couldn't write to file: {}").format(str(e))) def get_display_info(self, dupe, group, delta=False): def empty_data(): return {c.name: "---" for c in self.result_table.COLUMNS[1:]} if (dupe is None) or (group is None): return empty_data() try: return dupe.get_display_info(group, delta) except Exception as e: logging.warning("Exception (type: %s) on GetDisplayInfo for %s: %s", type(e), str(dupe.path), str(e)) return empty_data() def invoke_custom_command(self): """Calls command in ``CustomCommand`` pref with ``%d`` and ``%r`` placeholders replaced. Using the current selection, ``%d`` is replaced with the currently selected dupe and ``%r`` is replaced with that dupe's ref file. If there's no selection, the command is not invoked. If the dupe is a ref, ``%d`` and ``%r`` will be the same. """ cmd = self.view.get_default("CustomCommand") if not cmd: msg = tr("You have no custom command set up. Set it up in your preferences.") self.view.show_message(msg) return if not self.selected_dupes: return dupes = self.selected_dupes refs = [self.results.get_group_of_duplicate(dupe).ref for dupe in dupes] for dupe, ref in zip(dupes, refs): dupe_cmd = cmd.replace("%d", str(dupe.path)) dupe_cmd = dupe_cmd.replace("%r", str(ref.path)) match = re.match(r'"([^"]+)"(.*)', dupe_cmd) if match is not None: # This code here is because subprocess. Popen doesn't seem to accept, under Windows, # executable paths with spaces in it, *even* when they're enclosed in "". So this is # a workaround to make the damn thing work. exepath, args = match.groups() path, exename = op.split(exepath) p = subprocess.Popen(exename + args, shell=True, cwd=path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) output = p.stdout.read() logging.info("Custom command %s %s: %s", exename, args, output) else: p = subprocess.Popen(dupe_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) output = p.stdout.read() logging.info("Custom command %s: %s", dupe_cmd, output) def load(self): """Load directory selection and ignore list from files in appdata. This method is called during startup so that directory selection and ignore list, which is persistent data, is the same as when the last session was closed (when :meth:`save` was called). """ self.directories.load_from_file(op.join(self.appdata, "last_directories.xml")) self.notify("directories_changed") p = op.join(self.appdata, "ignore_list.xml") self.ignore_list.load_from_xml(p) self.ignore_list_dialog.refresh() p = op.join(self.appdata, "exclude_list.xml") self.exclude_list.load_from_xml(p) self.exclude_list_dialog.refresh() def load_directories(self, filepath): # Clear out previous entries self.directories.__init__() self.directories.load_from_file(filepath) self.notify("directories_changed") def load_from(self, filename): """Start an async job to load results from ``filename``. :param str filename: path of the XML file (created with :meth:`save_as`) to load """ def do(j): self.results.load_from_xml(filename, self._get_file, j) self._start_job(JobType.LOAD, do) def make_selected_reference(self): """Promote :attr:`selected_dupes` to reference position within their respective groups. Each selected dupe will become the :attr:`~core.engine.Group.ref` of its group. If there's more than one dupe selected for the same group, only the first (in the order currently shown in :attr:`result_table`) dupe will be promoted. """ dupes = self.without_ref(self.selected_dupes) changed_groups = set() for dupe in dupes: g = self.results.get_group_of_duplicate(dupe) if g not in changed_groups and self.results.make_ref(dupe): changed_groups.add(g) # It's not always obvious to users what this action does, so to make it a bit clearer, # we change our selection to the ref of all changed groups. However, we also want to keep # the files that were ref before and weren't changed by the action. In effect, what this # does is that we keep our old selection, but remove all non-ref dupes from it. # If no group was changed, however, we don't touch the selection. if not self.result_table.power_marker: if changed_groups: self.selected_dupes = [ d for d in self.selected_dupes if self.results.get_group_of_duplicate(d).ref is d ] self.notify("results_changed") else: # If we're in "Dupes Only" mode (previously called Power Marker), things are a bit # different. The refs are not shown in the table, and if our operation is successful, # this means that there's no way to follow our dupe selection. Then, the best thing to # do is to keep our selection index-wise (different dupe selection, but same index # selection). self.notify("results_changed_but_keep_selection") def mark_all(self): """Set all dupes in the results as marked.""" self.results.mark_all() self.notify("marking_changed") def mark_none(self): """Set all dupes in the results as unmarked.""" self.results.mark_none() self.notify("marking_changed") def mark_invert(self): """Invert the marked state of all dupes in the results.""" self.results.mark_invert() self.notify("marking_changed") def mark_dupe(self, dupe, marked): """Change marked status of ``dupe``. :param dupe: dupe to mark/unmark :type dupe: :class:`~core.fs.File` :param bool marked: True = mark, False = unmark """ if marked: self.results.mark(dupe) else: self.results.unmark(dupe) self.notify("marking_changed") def open_selected(self): """Open :attr:`selected_dupes` with their associated application.""" if len(self.selected_dupes) > 10 and not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN): return for dupe in self.selected_dupes: desktop.open_path(dupe.path) def purge_ignore_list(self): """Remove files that don't exist from :attr:`ignore_list`.""" self.ignore_list.filter(lambda f, s: op.exists(f) and op.exists(s)) self.ignore_list_dialog.refresh() def remove_directories(self, indexes): """Remove root directories at ``indexes`` from :attr:`directories`. :param indexes: Indexes of the directories to remove. :type indexes: list of int """ try: indexes = sorted(indexes, reverse=True) for index in indexes: del self.directories[index] self.notify("directories_changed") except IndexError: pass def remove_duplicates(self, duplicates): """Remove ``duplicates`` from :attr:`results`. Calls :meth:`~core.results.Results.remove_duplicates` and send appropriate notifications. :param duplicates: duplicates to remove. :type duplicates: list of :class:`~core.fs.File` """ self.results.remove_duplicates(self.without_ref(duplicates)) self.notify("results_changed_but_keep_selection") def remove_marked(self): """Removed marked duplicates from the results (without touching the files themselves).""" if not self.results.mark_count: self.view.show_message(MSG_NO_MARKED_DUPES) return msg = tr("You are about to remove %d files from results. Continue?") if not self.view.ask_yes_no(msg % self.results.mark_count): return self.results.perform_on_marked(lambda x: None, True) self._results_changed() def remove_selected(self): """Removed :attr:`selected_dupes` from the results (without touching the files themselves).""" dupes = self.without_ref(self.selected_dupes) if not dupes: self.view.show_message(MSG_NO_SELECTED_DUPES) return msg = tr("You are about to remove %d files from results. Continue?") if not self.view.ask_yes_no(msg % len(dupes)): return self.remove_duplicates(dupes) def rename_selected(self, newname): """Renames the selected dupes's file to ``newname``. If there's more than one selected dupes, the first one is used. :param str newname: The filename to rename the dupe's file to. """ try: d = self.selected_dupes[0] d.rename(newname) return True except (IndexError, fs.FSError) as e: logging.warning("dupeGuru Warning: %s" % str(e)) return False def reprioritize_groups(self, sort_key): """Sort dupes in each group (in :attr:`results`) according to ``sort_key``. Called by the re-prioritize dialog. Calls :meth:`~core.engine.Group.prioritize` and, once the sorting is done, show a message that confirms the action. :param sort_key: The key being sent to :meth:`~core.engine.Group.prioritize` :type sort_key: f(dupe) """ count = 0 for group in self.results.groups: if group.prioritize(key_func=sort_key): count += 1 if count: self.results.refresh_required = True self._results_changed() msg = tr("{} duplicate groups were changed by the re-prioritization.").format(count) self.view.show_message(msg) def reveal_selected(self): if self.selected_dupes: desktop.reveal_path(self.selected_dupes[0].path) def save(self): if not op.exists(self.appdata): os.makedirs(self.appdata) self.directories.save_to_file(op.join(self.appdata, "last_directories.xml")) p = op.join(self.appdata, "ignore_list.xml") self.ignore_list.save_to_xml(p) p = op.join(self.appdata, "exclude_list.xml") self.exclude_list.save_to_xml(p) self.notify("save_session") def close(self): fs.filesdb.close() def save_as(self, filename): """Save results in ``filename``. :param str filename: path of the file to save results (as XML) to. """ try: self.results.save_to_xml(filename) except OSError as e: self.view.show_message(tr("Couldn't write to file: {}").format(str(e))) def save_directories_as(self, filename): """Save directories in ``filename``. :param str filename: path of the file to save directories (as XML) to. """ try: self.directories.save_to_file(filename) except OSError as e: self.view.show_message(tr("Couldn't write to file: {}").format(str(e))) def start_scanning(self, profile_scan=False): """Starts an async job to scan for duplicates. Scans folders selected in :attr:`directories` and put the results in :attr:`results` """ scanner = self.SCANNER_CLASS() if not self.directories.has_any_file(): self.view.show_message(tr("The selected directories contain no scannable file.")) return # Send relevant options down to the scanner instance for k, v in self.options.items(): if hasattr(scanner, k): setattr(scanner, k, v) if self.app_mode == AppMode.PICTURE: scanner.cache_path = self._get_picture_cache_path() self.results.groups = [] self._recreate_result_table() self._results_changed() def do(j): if profile_scan: pr = cProfile.Profile() pr.enable() j.set_progress(0, tr("Collecting files to scan")) if scanner.scan_type == ScanType.FOLDERS: files = list(self.directories.get_folders(folderclass=se.fs.Folder, j=j)) else: files = list(self.directories.get_files(fileclasses=self.fileclasses, j=j)) if self.options["ignore_hardlink_matches"]: files = self._remove_hardlink_dupes(files) logging.info("Scanning %d files" % len(files)) self.results.groups = scanner.get_dupe_groups(files, self.ignore_list, j) self.discarded_file_count = scanner.discarded_file_count if profile_scan: pr.disable() pr.dump_stats(op.join(self.appdata, f"{datetime.datetime.now():%Y-%m-%d_%H-%M-%S}.profile")) self._start_job(JobType.SCAN, do) def toggle_selected_mark_state(self): selected = self.without_ref(self.selected_dupes) if not selected: return if allsame(self.results.is_marked(d) for d in selected): markfunc = self.results.mark_toggle else: markfunc = self.results.mark for dupe in selected: markfunc(dupe) self.notify("marking_changed") def without_ref(self, dupes): """Returns ``dupes`` with all reference elements removed.""" return [dupe for dupe in dupes if self.results.get_group_of_duplicate(dupe).ref is not dupe] def get_default(self, key, fallback_value=None): result = nonone(self.view.get_default(key), fallback_value) if fallback_value is not None and not isinstance(result, type(fallback_value)): # we don't want to end up with garbage values from the prefs try: result = type(fallback_value)(result) except Exception: result = fallback_value return result def set_default(self, key, value): self.view.set_default(key, value) # --- Properties @property def stat_line(self): result = self.results.stat_line if self.discarded_file_count: result = tr("%s (%d discarded)") % (result, self.discarded_file_count) return result @property def fileclasses(self): return self._get_fileclasses() @property def SCANNER_CLASS(self): if self.app_mode == AppMode.PICTURE: return pe.scanner.ScannerPE elif self.app_mode == AppMode.MUSIC: return me.scanner.ScannerME else: return se.scanner.ScannerSE @property def METADATA_TO_READ(self): if self.app_mode == AppMode.PICTURE: return ["size", "mtime", "dimensions", "exif_timestamp"] elif self.app_mode == AppMode.MUSIC: return [ "size", "mtime", "duration", "bitrate", "samplerate", "title", "artist", "album", "genre", "year", "track", "comment", ] else: return ["size", "mtime"] dupeguru-4.3.1/core/directories.py000066400000000000000000000246741426171743600172250ustar00rootroot00000000000000# Copyright 2017 Virgil Dupras # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import os from xml.etree import ElementTree as ET import logging from pathlib import Path from hscommon.jobprogress import job from hscommon.util import FileOrPath from hscommon.trans import tr from core import fs __all__ = [ "Directories", "DirectoryState", "AlreadyThereError", "InvalidPathError", ] class DirectoryState: """Enum describing how a folder should be considered. * DirectoryState.Normal: Scan all files normally * DirectoryState.Reference: Scan files, but make sure never to delete any of them * DirectoryState.Excluded: Don't scan this folder """ NORMAL = 0 REFERENCE = 1 EXCLUDED = 2 class AlreadyThereError(Exception): """The path being added is already in the directory list""" class InvalidPathError(Exception): """The path being added is invalid""" class Directories: """Holds user folder selection. Manages the selection that the user make through the folder selection dialog. It also manages folder states, and how recursion applies to them. Then, when the user starts the scan, :meth:`get_files` is called to retrieve all files (wrapped in :mod:`core.fs`) that have to be scanned according to the chosen folders/states. """ # ---Override def __init__(self, exclude_list=None): self._dirs = [] # {path: state} self.states = {} self._exclude_list = exclude_list def __contains__(self, path): for p in self._dirs: if path == p or p in path.parents: return True return False def __delitem__(self, key): self._dirs.__delitem__(key) def __getitem__(self, key): return self._dirs.__getitem__(key) def __len__(self): return len(self._dirs) # ---Private def _default_state_for_path(self, path): # New logic with regex filters if self._exclude_list is not None and self._exclude_list.mark_count > 0: # We iterate even if we only have one item here for denied_path_re in self._exclude_list.compiled: if denied_path_re.match(str(path.name)): return DirectoryState.EXCLUDED # return # We still use the old logic to force state on hidden dirs # Override this in subclasses to specify the state of some special folders. if path.name.startswith("."): return DirectoryState.EXCLUDED def _get_files(self, from_path, fileclasses, j): try: with os.scandir(from_path) as iter: root_path = Path(from_path) state = self.get_state(root_path) # if we have no un-excluded dirs under this directory skip going deeper skip_dirs = state == DirectoryState.EXCLUDED and not any( p.parts[: len(root_path.parts)] == root_path.parts for p in self.states ) count = 0 for item in iter: j.check_if_cancelled() try: if item.is_dir(): if skip_dirs: continue yield from self._get_files(item.path, fileclasses, j) continue elif state == DirectoryState.EXCLUDED: continue # File excluding or not if ( self._exclude_list is None or not self._exclude_list.mark_count or not self._exclude_list.is_excluded(str(from_path), item.name) ): file = fs.get_file(item, fileclasses=fileclasses) if file: file.is_ref = state == DirectoryState.REFERENCE count += 1 yield file except (OSError, fs.InvalidPath): pass logging.debug( "Collected %d files in folder %s", count, str(root_path), ) except OSError: pass def _get_folders(self, from_folder, j): j.check_if_cancelled() try: for subfolder in from_folder.subfolders: yield from self._get_folders(subfolder, j) state = self.get_state(from_folder.path) if state != DirectoryState.EXCLUDED: from_folder.is_ref = state == DirectoryState.REFERENCE logging.debug("Yielding Folder %r state: %d", from_folder, state) yield from_folder except (OSError, fs.InvalidPath): pass # ---Public def add_path(self, path): """Adds ``path`` to self, if not already there. Raises :exc:`AlreadyThereError` if ``path`` is already in self. If path is a directory containing some of the directories already present in self, ``path`` will be added, but all directories under it will be removed. Can also raise :exc:`InvalidPathError` if ``path`` does not exist. :param Path path: path to add """ if path in self: raise AlreadyThereError() if not path.exists(): raise InvalidPathError() self._dirs = [p for p in self._dirs if path not in p.parents] self._dirs.append(path) @staticmethod def get_subfolders(path): """Returns a sorted list of paths corresponding to subfolders in ``path``. :param Path path: get subfolders from there :rtype: list of Path """ try: subpaths = [p for p in path.glob("*") if p.is_dir()] subpaths.sort(key=lambda x: x.name.lower()) return subpaths except OSError: return [] def get_files(self, fileclasses=None, j=job.nulljob): """Returns a list of all files that are not excluded. Returned files also have their ``is_ref`` attr set if applicable. """ if fileclasses is None: fileclasses = [fs.File] file_count = 0 for path in self._dirs: for file in self._get_files(path, fileclasses=fileclasses, j=j): file_count += 1 if type(j) != job.NullJob: j.set_progress(-1, tr("Collected {} files to scan").format(file_count)) yield file def get_folders(self, folderclass=None, j=job.nulljob): """Returns a list of all folders that are not excluded. Returned folders also have their ``is_ref`` attr set if applicable. """ if folderclass is None: folderclass = fs.Folder folder_count = 0 for path in self._dirs: from_folder = folderclass(path) for folder in self._get_folders(from_folder, j): folder_count += 1 if type(j) != job.NullJob: j.set_progress(-1, tr("Collected {} folders to scan").format(folder_count)) yield folder def get_state(self, path): """Returns the state of ``path``. :rtype: :class:`DirectoryState` """ # direct match? easy result. if path in self.states: return self.states[path] state = self._default_state_for_path(path) or DirectoryState.NORMAL # Save non-default states in cache, necessary for _get_files() if state != DirectoryState.NORMAL: self.states[path] = state return state # find the longest parent path that is in states and return that state if found # NOTE: path.parents is ordered longest to shortest for parent_path in path.parents: if parent_path in self.states: return self.states[parent_path] return state def has_any_file(self): """Returns whether selected folders contain any file. Because it stops at the first file it finds, it's much faster than get_files(). :rtype: bool """ try: next(self.get_files()) return True except StopIteration: return False def load_from_file(self, infile): """Load folder selection from ``infile``. :param file infile: path or file pointer to XML generated through :meth:`save_to_file` """ try: root = ET.parse(infile).getroot() except Exception: return for rdn in root.iter("root_directory"): attrib = rdn.attrib if "path" not in attrib: continue path = attrib["path"] try: self.add_path(Path(path)) except (AlreadyThereError, InvalidPathError): pass for sn in root.iter("state"): attrib = sn.attrib if not ("path" in attrib and "value" in attrib): continue path = attrib["path"] state = attrib["value"] self.states[Path(path)] = int(state) def save_to_file(self, outfile): """Save folder selection as XML to ``outfile``. :param file outfile: path or file pointer to XML file to save to. """ with FileOrPath(outfile, "wb") as fp: root = ET.Element("directories") for root_path in self: root_path_node = ET.SubElement(root, "root_directory") root_path_node.set("path", str(root_path)) for path, state in self.states.items(): state_node = ET.SubElement(root, "state") state_node.set("path", str(path)) state_node.set("value", str(state)) tree = ET.ElementTree(root) tree.write(fp, encoding="utf-8") def set_state(self, path, state): """Set the state of folder at ``path``. :param Path path: path of the target folder :param state: state to set folder to :type state: :class:`DirectoryState` """ if self.get_state(path) == state: return for iter_path in list(self.states.keys()): if path in iter_path.parents: del self.states[iter_path] self.states[path] = state dupeguru-4.3.1/core/engine.py000066400000000000000000000467211426171743600161530ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2006/01/29 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import difflib import itertools import logging import string from collections import defaultdict, namedtuple from unicodedata import normalize from hscommon.util import flatten, multi_replace from hscommon.trans import tr from hscommon.jobprogress import job ( WEIGHT_WORDS, MATCH_SIMILAR_WORDS, NO_FIELD_ORDER, ) = range(3) JOB_REFRESH_RATE = 100 PROGRESS_MESSAGE = tr("%d matches found from %d groups") def getwords(s): # We decompose the string so that ascii letters with accents can be part of the word. s = normalize("NFD", s) s = multi_replace(s, "-_&+():;\\[]{}.,<>/?~!@#$*", " ").lower() # logging.debug(f"DEBUG chars for: {s}\n" # f"{[c for c in s if ord(c) != 32]}\n" # f"{[ord(c) for c in s if ord(c) != 32]}") # HACK We shouldn't ignore non-ascii characters altogether. Any Unicode char # above common european characters that cannot be "sanitized" (ie. stripped # of their accents, etc.) are preserved as is. The arbitrary limit is # obtained from this one: ord("\u037e") GREEK QUESTION MARK s = "".join( c for c in s if (ord(c) <= 894 and c in string.ascii_letters + string.digits + string.whitespace) or ord(c) > 894 ) return [_f for _f in s.split(" ") if _f] # remove empty elements def getfields(s): fields = [getwords(field) for field in s.split(" - ")] return [_f for _f in fields if _f] def unpack_fields(fields): result = [] for field in fields: if isinstance(field, list): result += field else: result.append(field) return result def compare(first, second, flags=()): """Returns the % of words that match between ``first`` and ``second`` The result is a ``int`` in the range 0..100. ``first`` and ``second`` can be either a string or a list (of words). """ if not (first and second): return 0 if any(isinstance(element, list) for element in first): return compare_fields(first, second, flags) second = second[:] # We must use a copy of second because we remove items from it match_similar = MATCH_SIMILAR_WORDS in flags weight_words = WEIGHT_WORDS in flags joined = first + second total_count = sum(len(word) for word in joined) if weight_words else len(joined) match_count = 0 in_order = True for word in first: if match_similar and (word not in second): similar = difflib.get_close_matches(word, second, 1, 0.8) if similar: word = similar[0] if word in second: if second[0] != word: in_order = False second.remove(word) match_count += len(word) if weight_words else 1 result = round(((match_count * 2) / total_count) * 100) if (result == 100) and (not in_order): result = 99 # We cannot consider a match exact unless the ordering is the same return result def compare_fields(first, second, flags=()): """Returns the score for the lowest matching :ref:`fields`. ``first`` and ``second`` must be lists of lists of string. Each sub-list is then compared with :func:`compare`. """ if len(first) != len(second): return 0 if NO_FIELD_ORDER in flags: results = [] # We don't want to remove field directly in the list. We must work on a copy. second = second[:] for field1 in first: max_score = 0 matched_field = None for field2 in second: r = compare(field1, field2, flags) if r > max_score: max_score = r matched_field = field2 results.append(max_score) if matched_field: second.remove(matched_field) else: results = [compare(field1, field2, flags) for field1, field2 in zip(first, second)] return min(results) if results else 0 def build_word_dict(objects, j=job.nulljob): """Returns a dict of objects mapped by their words. objects must have a ``words`` attribute being a list of strings or a list of lists of strings (:ref:`fields`). The result will be a dict with words as keys, lists of objects as values. """ result = defaultdict(set) for object in j.iter_with_progress(objects, "Prepared %d/%d files", JOB_REFRESH_RATE): for word in unpack_fields(object.words): result[word].add(object) return result def merge_similar_words(word_dict): """Take all keys in ``word_dict`` that are similar, and merge them together. ``word_dict`` has been built with :func:`build_word_dict`. Similarity is computed with Python's ``difflib.get_close_matches()``, which computes the number of edits that are necessary to make a word equal to the other. """ keys = list(word_dict.keys()) keys.sort(key=len) # we want the shortest word to stay while keys: key = keys.pop(0) similars = difflib.get_close_matches(key, keys, 100, 0.8) if not similars: continue objects = word_dict[key] for similar in similars: objects |= word_dict[similar] del word_dict[similar] keys.remove(similar) def reduce_common_words(word_dict, threshold): """Remove all objects from ``word_dict`` values where the object count >= ``threshold`` ``word_dict`` has been built with :func:`build_word_dict`. The exception to this removal are the objects where all the words of the object are common. Because if we remove them, we will miss some duplicates! """ uncommon_words = {word for word, objects in word_dict.items() if len(objects) < threshold} for word, objects in list(word_dict.items()): if len(objects) < threshold: continue reduced = set() for o in objects: if not any(w in uncommon_words for w in unpack_fields(o.words)): reduced.add(o) if reduced: word_dict[word] = reduced else: del word_dict[word] # Writing docstrings in a namedtuple is tricky. From Python 3.3, it's possible to set __doc__, but # some research allowed me to find a more elegant solution, which is what is done here. See # http://stackoverflow.com/questions/1606436/adding-docstrings-to-namedtuples-in-python class Match(namedtuple("Match", "first second percentage")): """Represents a match between two :class:`~core.fs.File`. Regarless of the matching method, when two files are determined to match, a Match pair is created, which holds, of course, the two matched files, but also their match "level". .. attribute:: first first file of the pair. .. attribute:: second second file of the pair. .. attribute:: percentage their match level according to the scan method which found the match. int from 1 to 100. For exact scan methods, such as Contents scans, this will always be 100. """ __slots__ = () def get_match(first, second, flags=()): # it is assumed here that first and second both have a "words" attribute percentage = compare(first.words, second.words, flags) return Match(first, second, percentage) def getmatches( objects, min_match_percentage=0, match_similar_words=False, weight_words=False, no_field_order=False, j=job.nulljob, ): """Returns a list of :class:`Match` within ``objects`` after fuzzily matching their words. :param objects: List of :class:`~core.fs.File` to match. :param int min_match_percentage: minimum % of words that have to match. :param bool match_similar_words: make similar words (see :func:`merge_similar_words`) match. :param bool weight_words: longer words are worth more in match % computations. :param bool no_field_order: match :ref:`fields` regardless of their order. :param j: A :ref:`job progress instance `. """ COMMON_WORD_THRESHOLD = 50 LIMIT = 5000000 j = j.start_subjob(2) sj = j.start_subjob(2) for o in objects: if not hasattr(o, "words"): o.words = getwords(o.name) word_dict = build_word_dict(objects, sj) reduce_common_words(word_dict, COMMON_WORD_THRESHOLD) if match_similar_words: merge_similar_words(word_dict) match_flags = [] if weight_words: match_flags.append(WEIGHT_WORDS) if match_similar_words: match_flags.append(MATCH_SIMILAR_WORDS) if no_field_order: match_flags.append(NO_FIELD_ORDER) j.start_job(len(word_dict), PROGRESS_MESSAGE % (0, 0)) compared = defaultdict(set) result = [] try: word_count = 0 # This whole 'popping' thing is there to avoid taking too much memory at the same time. while word_dict: items = word_dict.popitem()[1] while items: ref = items.pop() compared_already = compared[ref] to_compare = items - compared_already compared_already |= to_compare for other in to_compare: m = get_match(ref, other, match_flags) if m.percentage >= min_match_percentage: result.append(m) if len(result) >= LIMIT: return result word_count += 1 j.add_progress(desc=PROGRESS_MESSAGE % (len(result), word_count)) except MemoryError: # This is the place where the memory usage is at its peak during the scan. # Just continue the process with an incomplete list of matches. del compared # This should give us enough room to call logging. logging.warning("Memory Overflow. Matches: %d. Word dict: %d" % (len(result), len(word_dict))) return result return result def getmatches_by_contents(files, bigsize=0, j=job.nulljob): """Returns a list of :class:`Match` within ``files`` if their contents is the same. :param bigsize: The size in bytes over which we consider files big enough to justify taking samples of the file for hashing. If 0, compute digest as usual. :param j: A :ref:`job progress instance `. """ size2files = defaultdict(set) for f in files: size2files[f.size].add(f) del files possible_matches = [files for files in size2files.values() if len(files) > 1] del size2files result = [] j.start_job(len(possible_matches), PROGRESS_MESSAGE % (0, 0)) group_count = 0 for group in possible_matches: for first, second in itertools.combinations(group, 2): if first.is_ref and second.is_ref: continue # Don't spend time comparing two ref pics together. if first.size == 0 and second.size == 0: # skip hashing for zero length files result.append(Match(first, second, 100)) continue # if digests are the same (and not None) then files match if first.digest_partial == second.digest_partial and first.digest_partial is not None: if bigsize > 0 and first.size > bigsize: if first.digest_samples == second.digest_samples and first.digest_samples is not None: result.append(Match(first, second, 100)) else: if first.digest == second.digest and first.digest is not None: result.append(Match(first, second, 100)) group_count += 1 j.add_progress(desc=PROGRESS_MESSAGE % (len(result), group_count)) return result class Group: """A group of :class:`~core.fs.File` that match together. This manages match pairs into groups and ensures that all files in the group match to each other. .. attribute:: ref The "reference" file, which is the file among the group that isn't going to be deleted. .. attribute:: ordered Ordered list of duplicates in the group (including the :attr:`ref`). .. attribute:: unordered Set duplicates in the group (including the :attr:`ref`). .. attribute:: dupes An ordered list of the group's duplicate, without :attr:`ref`. Equivalent to ``ordered[1:]`` .. attribute:: percentage Average match percentage of match pairs containing :attr:`ref`. """ # ---Override def __init__(self): self._clear() def __contains__(self, item): return item in self.unordered def __getitem__(self, key): return self.ordered.__getitem__(key) def __iter__(self): return iter(self.ordered) def __len__(self): return len(self.ordered) # ---Private def _clear(self): self._percentage = None self._matches_for_ref = None self.matches = set() self.candidates = defaultdict(set) self.ordered = [] self.unordered = set() def _get_matches_for_ref(self): if self._matches_for_ref is None: ref = self.ref self._matches_for_ref = [match for match in self.matches if ref in match] return self._matches_for_ref # ---Public def add_match(self, match): """Adds ``match`` to internal match list and possibly add duplicates to the group. A duplicate can only be considered as such if it matches all other duplicates in the group. This method registers that pair (A, B) represented in ``match`` as possible candidates and, if A and/or B end up matching every other duplicates in the group, add these duplicates to the group. :param tuple match: pair of :class:`~core.fs.File` to add """ def add_candidate(item, match): matches = self.candidates[item] matches.add(match) if self.unordered <= matches: self.ordered.append(item) self.unordered.add(item) if match in self.matches: return self.matches.add(match) first, second, _ = match if first not in self.unordered: add_candidate(first, second) if second not in self.unordered: add_candidate(second, first) self._percentage = None self._matches_for_ref = None def discard_matches(self): """Remove all recorded matches that didn't result in a duplicate being added to the group. You can call this after the duplicate scanning process to free a bit of memory. """ discarded = {m for m in self.matches if not all(obj in self.unordered for obj in [m.first, m.second])} self.matches -= discarded self.candidates = defaultdict(set) return discarded def get_match_of(self, item): """Returns the match pair between ``item`` and :attr:`ref`.""" if item is self.ref: return for m in self._get_matches_for_ref(): if item in m: return m def prioritize(self, key_func, tie_breaker=None): """Reorders :attr:`ordered` according to ``key_func``. :param key_func: Key (f(x)) to be used for sorting :param tie_breaker: function to be used to select the reference position in case the top duplicates have the same key_func() result. """ # tie_breaker(ref, dupe) --> True if dupe should be ref # Returns True if anything changed during prioritization. new_order = sorted(self.ordered, key=lambda x: (-x.is_ref, key_func(x))) changed = new_order != self.ordered self.ordered = new_order if tie_breaker is None: return changed ref = self.ref key_value = key_func(ref) for dupe in self.dupes: if key_func(dupe) != key_value: break if tie_breaker(ref, dupe): ref = dupe if ref is not self.ref: self.switch_ref(ref) return True return changed def remove_dupe(self, item, discard_matches=True): try: self.ordered.remove(item) self.unordered.remove(item) self._percentage = None self._matches_for_ref = None if (len(self) > 1) and any(not getattr(item, "is_ref", False) for item in self): if discard_matches: self.matches = {m for m in self.matches if item not in m} else: self._clear() except ValueError: pass def switch_ref(self, with_dupe): """Make the :attr:`ref` dupe of the group switch position with ``with_dupe``.""" if self.ref.is_ref: return False try: self.ordered.remove(with_dupe) self.ordered.insert(0, with_dupe) self._percentage = None self._matches_for_ref = None return True except ValueError: return False dupes = property(lambda self: self[1:]) @property def percentage(self): if self._percentage is None: if self.dupes: matches = self._get_matches_for_ref() self._percentage = sum(match.percentage for match in matches) // len(matches) else: self._percentage = 0 return self._percentage @property def ref(self): if self: return self[0] def get_groups(matches): """Returns a list of :class:`Group` from ``matches``. Create groups out of match pairs in the smartest way possible. """ matches.sort(key=lambda match: -match.percentage) dupe2group = {} groups = [] try: for match in matches: first, second, _ = match first_group = dupe2group.get(first) second_group = dupe2group.get(second) if first_group: if second_group: if first_group is second_group: target_group = first_group else: continue else: target_group = first_group dupe2group[second] = target_group else: if second_group: target_group = second_group dupe2group[first] = target_group else: target_group = Group() groups.append(target_group) dupe2group[first] = target_group dupe2group[second] = target_group target_group.add_match(match) except MemoryError: del dupe2group del matches # should free enough memory to continue logging.warning(f"Memory Overflow. Groups: {len(groups)}") # Now that we have a group, we have to discard groups' matches and see if there're any "orphan" # matches, that is, matches that were candidate in a group but that none of their 2 files were # accepted in the group. With these orphan groups, it's safe to build additional groups matched_files = set(flatten(groups)) orphan_matches = [] for group in groups: orphan_matches += { m for m in group.discard_matches() if not any(obj in matched_files for obj in [m.first, m.second]) } if groups and orphan_matches: groups += get_groups(orphan_matches) # no job, as it isn't supposed to take a long time return groups dupeguru-4.3.1/core/exclude.py000066400000000000000000000435201426171743600163310ustar00rootroot00000000000000# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from core.markable import Markable from xml.etree import ElementTree as ET # TODO: perhaps use regex module for better Unicode support? https://pypi.org/project/regex/ # also https://pypi.org/project/re2/ # TODO update the Result list with newly added regexes if possible import re from os import sep import logging import functools from hscommon.util import FileOrPath from hscommon.plat import ISWINDOWS import time default_regexes = [ r"^thumbs\.db$", # Obsolete after WindowsXP r"^desktop\.ini$", # Windows metadata r"^\.DS_Store$", # MacOS metadata r"^\.Trash\-.*", # Linux trash directories r"^\$Recycle\.Bin$", # Windows r"^\..*", # Hidden files on Unix-like ] # These are too broad forbidden_regexes = [r".*", r"\/.*", r".*\/.*", r".*\\\\.*", r".*\..*"] def timer(func): @functools.wraps(func) def wrapper_timer(*args): start = time.perf_counter_ns() value = func(*args) end = time.perf_counter_ns() print(f"DEBUG: func {func.__name__!r} took {end - start} ns.") return value return wrapper_timer def memoize(func): func.cache = dict() @functools.wraps(func) def _memoize(*args): if args not in func.cache: func.cache[args] = func(*args) return func.cache[args] return _memoize class AlreadyThereException(Exception): """Expression already in the list""" def __init__(self, arg="Expression is already in excluded list."): super().__init__(arg) class ExcludeList(Markable): """A list of lists holding regular expression strings and the compiled re.Pattern""" # Used to filter out directories and files that we would rather avoid scanning. # The list() class allows us to preserve item order without too much hassle. # The downside is we have to compare strings every time we look for an item in the list # since we use regex strings as keys. # If _use_union is True, the compiled regexes will be combined into one single # Pattern instead of separate Patterns which may or may not give better # performance compared to looping through each Pattern individually. # ---Override def __init__(self, union_regex=True): Markable.__init__(self) self._use_union = union_regex # list([str regex, bool iscompilable, re.error exception, Pattern compiled], ...) self._excluded = [] self._excluded_compiled = set() self._dirty = True def __iter__(self): """Iterate in order.""" for item in self._excluded: regex = item[0] yield self.is_marked(regex), regex def __contains__(self, item): return self.has_entry(item) def __len__(self): """Returns the total number of regexes regardless of mark status.""" return len(self._excluded) def __getitem__(self, key): """Returns the list item corresponding to key.""" for item in self._excluded: if item[0] == key: return item raise KeyError(f"Key {key} is not in exclusion list.") def __setitem__(self, key, value): # TODO if necessary pass def __delitem__(self, key): # TODO if necessary pass def get_compiled(self, key): """Returns the (precompiled) Pattern for key""" return self.__getitem__(key)[3] def is_markable(self, regex): return self._is_markable(regex) def _is_markable(self, regex): """Return the cached result of "compilable" property""" for item in self._excluded: if item[0] == regex: return item[1] return False # should not be necessary, the regex SHOULD be in there def _did_mark(self, regex): self._add_compiled(regex) def _did_unmark(self, regex): self._remove_compiled(regex) def _add_compiled(self, regex): self._dirty = True if self._use_union: return for item in self._excluded: # FIXME probably faster to just rebuild the set from the compiled instead of comparing strings if item[0] == regex: # no need to test if already present since it's a set() self._excluded_compiled.add(item[3]) break def _remove_compiled(self, regex): self._dirty = True if self._use_union: return for item in self._excluded_compiled: if regex in item.pattern: self._excluded_compiled.remove(item) break # @timer @memoize def _do_compile(self, expr): return re.compile(expr) # @timer # @memoize # probably not worth memoizing this one if we memoize the above def compile_re(self, regex): compiled = None try: compiled = self._do_compile(regex) except Exception as e: return False, e, compiled return True, None, compiled def error(self, regex): """Return the compilation error Exception for regex. It should have a "msg" attr.""" for item in self._excluded: if item[0] == regex: return item[2] def build_compiled_caches(self, union=False): if not union: self._cached_compiled_files = [x for x in self._excluded_compiled if not has_sep(x.pattern)] self._cached_compiled_paths = [x for x in self._excluded_compiled if has_sep(x.pattern)] self._dirty = False return marked_count = [x for marked, x in self if marked] # If there is no item, the compiled Pattern will be '' and match everything! if not marked_count: self._cached_compiled_union_all = [] self._cached_compiled_union_files = [] self._cached_compiled_union_paths = [] else: # HACK returned as a tuple to get a free iterator and keep interface # the same regardless of whether the client asked for union or not self._cached_compiled_union_all = (re.compile("|".join(marked_count)),) files_marked = [x for x in marked_count if not has_sep(x)] if not files_marked: self._cached_compiled_union_files = tuple() else: self._cached_compiled_union_files = (re.compile("|".join(files_marked)),) paths_marked = [x for x in marked_count if has_sep(x)] if not paths_marked: self._cached_compiled_union_paths = tuple() else: self._cached_compiled_union_paths = (re.compile("|".join(paths_marked)),) self._dirty = False @property def compiled(self): """Should be used by other classes to retrieve the up-to-date list of patterns.""" if self._use_union: if self._dirty: self.build_compiled_caches(self._use_union) return self._cached_compiled_union_all return self._excluded_compiled @property def compiled_files(self): """When matching against filenames only, we probably won't be seeing any directory separator, so we filter out regexes with os.sep in them. The interface should be expected to be a generator, even if it returns only one item (one Pattern in the union case).""" if self._dirty: self.build_compiled_caches(self._use_union) return self._cached_compiled_union_files if self._use_union else self._cached_compiled_files @property def compiled_paths(self): """Returns patterns with only separators in them, for more precise filtering.""" if self._dirty: self.build_compiled_caches(self._use_union) return self._cached_compiled_union_paths if self._use_union else self._cached_compiled_paths # ---Public def add(self, regex, forced=False): """This interface should throw exceptions if there is an error during regex compilation""" if self.has_entry(regex): # This exception should never be ignored raise AlreadyThereException() if regex in forbidden_regexes: raise ValueError("Forbidden (dangerous) expression.") iscompilable, exception, compiled = self.compile_re(regex) if not iscompilable and not forced: # This exception can be ignored, but taken into account # to avoid adding to compiled set raise exception else: self._do_add(regex, iscompilable, exception, compiled) def _do_add(self, regex, iscompilable, exception, compiled): # We need to insert at the top self._excluded.insert(0, [regex, iscompilable, exception, compiled]) @property def marked_count(self): """Returns the number of marked regexes only.""" return len([x for marked, x in self if marked]) def has_entry(self, regex): for item in self._excluded: if regex == item[0]: return True return False def is_excluded(self, dirname, filename): """Return True if the file or the absolute path to file is supposed to be filtered out, False otherwise.""" matched = False for expr in self.compiled_files: if expr.fullmatch(filename): matched = True break if not matched: for expr in self.compiled_paths: if expr.fullmatch(dirname + sep + filename): matched = True break return matched def remove(self, regex): for item in self._excluded: if item[0] == regex: self._excluded.remove(item) self._remove_compiled(regex) def rename(self, regex, newregex): if regex == newregex: return found = False was_marked = False is_compilable = False for item in self._excluded: if item[0] == regex: found = True was_marked = self.is_marked(regex) is_compilable, exception, compiled = self.compile_re(newregex) # We overwrite the found entry self._excluded[self._excluded.index(item)] = [newregex, is_compilable, exception, compiled] self._remove_compiled(regex) break if not found: return if is_compilable: self._add_compiled(newregex) if was_marked: # Not marked by default when added, add it back self.mark(newregex) # def change_index(self, regex, new_index): # """Internal list must be a list, not dict.""" # item = self._excluded.pop(regex) # self._excluded.insert(new_index, item) def restore_defaults(self): for _, regex in self: if regex not in default_regexes: self.unmark(regex) for default_regex in default_regexes: if not self.has_entry(default_regex): self.add(default_regex) self.mark(default_regex) def load_from_xml(self, infile): """Loads the ignore list from a XML created with save_to_xml. infile can be a file object or a filename. """ try: root = ET.parse(infile).getroot() except Exception as e: logging.warning(f"Error while loading {infile}: {e}") self.restore_defaults() return e marked = set() exclude_elems = (e for e in root if e.tag == "exclude") for exclude_item in exclude_elems: regex_string = exclude_item.get("regex") if not regex_string: continue try: # "forced" avoids compilation exceptions and adds anyway self.add(regex_string, forced=True) except AlreadyThereException: logging.error( f'Regex "{regex_string}" \ loaded from XML was already present in the list.' ) continue if exclude_item.get("marked") == "y": marked.add(regex_string) for item in marked: self.mark(item) def save_to_xml(self, outfile): """Create a XML file that can be used by load_from_xml. outfile can be a file object or a filename.""" root = ET.Element("exclude_list") # reversed in order to keep order of entries when reloading from xml later for item in reversed(self._excluded): exclude_node = ET.SubElement(root, "exclude") exclude_node.set("regex", str(item[0])) exclude_node.set("marked", ("y" if self.is_marked(item[0]) else "n")) tree = ET.ElementTree(root) with FileOrPath(outfile, "wb") as fp: tree.write(fp, encoding="utf-8") class ExcludeDict(ExcludeList): """Exclusion list holding a set of regular expressions as keys, the compiled Pattern, compilation error and compilable boolean as values.""" # Implemntation around a dictionary instead of a list, which implies # to keep the index of each string-key as its sub-element and keep it updated # whenever insert/remove is done. def __init__(self, union_regex=False): Markable.__init__(self) self._use_union = union_regex # { "regex string": # { # "index": int, # "compilable": bool, # "error": str, # "compiled": Pattern or None # } # } self._excluded = {} self._excluded_compiled = set() self._dirty = True def __iter__(self): """Iterate in order.""" for regex in ordered_keys(self._excluded): yield self.is_marked(regex), regex def __getitem__(self, key): """Returns the dict item correponding to key""" return self._excluded.__getitem__(key) def get_compiled(self, key): """Returns the compiled item for key""" return self.__getitem__(key).get("compiled") def is_markable(self, regex): return self._is_markable(regex) def _is_markable(self, regex): """Return the cached result of "compilable" property""" exists = self._excluded.get(regex) if exists: return exists.get("compilable") return False def _add_compiled(self, regex): self._dirty = True if self._use_union: return try: self._excluded_compiled.add(self._excluded.get(regex).get("compiled")) except Exception as e: logging.error(f"Exception while adding regex {regex} to compiled set: {e}") return def is_compilable(self, regex): """Returns the cached "compilable" value""" return self._excluded[regex]["compilable"] def error(self, regex): """Return the compilation error message for regex string""" return self._excluded.get(regex).get("error") # ---Public def _do_add(self, regex, iscompilable, exception, compiled): # We always insert at the top, so index should be 0 # and other indices should be pushed by one for value in self._excluded.values(): value["index"] += 1 self._excluded[regex] = {"index": 0, "compilable": iscompilable, "error": exception, "compiled": compiled} def has_entry(self, regex): if regex in self._excluded.keys(): return True return False def remove(self, regex): old_value = self._excluded.pop(regex) # Bring down all indices which where above it index = old_value["index"] if index == len(self._excluded) - 1: # we start at 0... # Old index was at the end, no need to update other indices self._remove_compiled(regex) return for value in self._excluded.values(): if value.get("index") > old_value["index"]: value["index"] -= 1 self._remove_compiled(regex) def rename(self, regex, newregex): if regex == newregex or regex not in self._excluded.keys(): return was_marked = self.is_marked(regex) previous = self._excluded.pop(regex) iscompilable, error, compiled = self.compile_re(newregex) self._excluded[newregex] = { "index": previous.get("index"), "compilable": iscompilable, "error": error, "compiled": compiled, } self._remove_compiled(regex) if iscompilable: self._add_compiled(newregex) if was_marked: self.mark(newregex) def save_to_xml(self, outfile): """Create a XML file that can be used by load_from_xml. outfile can be a file object or a filename. """ root = ET.Element("exclude_list") # reversed in order to keep order of entries when reloading from xml later reversed_list = [] for key in ordered_keys(self._excluded): reversed_list.append(key) for item in reversed(reversed_list): exclude_node = ET.SubElement(root, "exclude") exclude_node.set("regex", str(item)) exclude_node.set("marked", ("y" if self.is_marked(item) else "n")) tree = ET.ElementTree(root) with FileOrPath(outfile, "wb") as fp: tree.write(fp, encoding="utf-8") def ordered_keys(_dict): """Returns an iterator over the keys of dictionary sorted by "index" key""" if not len(_dict): return list_of_items = [] for item in _dict.items(): list_of_items.append(item) list_of_items.sort(key=lambda x: x[1].get("index")) for item in list_of_items: yield item[0] if ISWINDOWS: def has_sep(regexp): return "\\" + sep in regexp else: def has_sep(regexp): return sep in regexp dupeguru-4.3.1/core/export.py000066400000000000000000000067551426171743600162320ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2006/09/16 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import os.path as op from tempfile import mkdtemp import csv # Yes, this is a very low-tech solution, but at least it doesn't have all these annoying dependency # and resource problems. MAIN_TEMPLATE = """ dupeGuru Results

dupeGuru Results

$colheaders $rows
""" COLHEADERS_TEMPLATE = "{name}" ROW_TEMPLATE = """ {filename}{cells} """ CELL_TEMPLATE = """{value}""" def export_to_xhtml(colnames, rows): # a row is a list of values with the first value being a flag indicating if the row should be indented if rows: assert len(rows[0]) == len(colnames) + 1 # + 1 is for the "indented" flag colheaders = "".join(COLHEADERS_TEMPLATE.format(name=name) for name in colnames) rendered_rows = [] previous_group_id = None for row in rows: # [2:] is to remove the indented flag + filename if row[0] != previous_group_id: # We've just changed dupe group, which means that this dupe is a ref. We don't indent it. indented = "" else: indented = "indented" filename = row[1] cells = "".join(CELL_TEMPLATE.format(value=value) for value in row[2:]) rendered_rows.append(ROW_TEMPLATE.format(indented=indented, filename=filename, cells=cells)) previous_group_id = row[0] rendered_rows = "".join(rendered_rows) # The main template can't use format because the css code uses {} content = MAIN_TEMPLATE.replace("$colheaders", colheaders).replace("$rows", rendered_rows) folder = mkdtemp() destpath = op.join(folder, "export.htm") fp = open(destpath, "wt", encoding="utf-8") fp.write(content) fp.close() return destpath def export_to_csv(dest, colnames, rows): writer = csv.writer(open(dest, "wt", encoding="utf-8")) writer.writerow(["Group ID"] + colnames) for row in rows: writer.writerow(row) dupeguru-4.3.1/core/fs.py000066400000000000000000000350301426171743600153050ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2009-10-22 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html # This is a fork from hsfs. The reason for this fork is that hsfs has been designed for musicGuru # and was re-used for dupeGuru. The problem is that hsfs is way over-engineered for dupeGuru, # resulting needless complexity and memory usage. It's been a while since I wanted to do that fork, # and I'm doing it now. import os from math import floor import logging import sqlite3 from threading import Lock from typing import Any, AnyStr, Union, Callable from pathlib import Path from hscommon.util import nonone, get_file_ext hasher: Callable try: import xxhash hasher = xxhash.xxh128 except ImportError: import hashlib hasher = hashlib.md5 __all__ = [ "File", "Folder", "get_file", "get_files", "FSError", "AlreadyExistsError", "InvalidPath", "InvalidDestinationError", "OperationError", ] NOT_SET = object() # The goal here is to not run out of memory on really big files. However, the chunk # size has to be large enough so that the python loop isn't too costly in terms of # CPU. CHUNK_SIZE = 1024 * 1024 # 1 MiB # Minimum size below which partial hashing is not used MIN_FILE_SIZE = 3 * CHUNK_SIZE # 3MiB, because we take 3 samples class FSError(Exception): cls_message = "An error has occured on '{name}' in '{parent}'" def __init__(self, fsobject, parent=None): message = self.cls_message if isinstance(fsobject, str): name = fsobject elif isinstance(fsobject, File): name = fsobject.name else: name = "" parentname = str(parent) if parent is not None else "" Exception.__init__(self, message.format(name=name, parent=parentname)) class AlreadyExistsError(FSError): "The directory or file name we're trying to add already exists" cls_message = "'{name}' already exists in '{parent}'" class InvalidPath(FSError): "The path of self is invalid, and cannot be worked with." cls_message = "'{name}' is invalid." class InvalidDestinationError(FSError): """A copy/move operation has been called, but the destination is invalid.""" cls_message = "'{name}' is an invalid destination for this operation." class OperationError(FSError): """A copy/move/delete operation has been called, but the checkup after the operation shows that it didn't work.""" cls_message = "Operation on '{name}' failed." class FilesDB: schema_version = 1 schema_version_description = "Changed from md5 to xxhash if available." create_table_query = "CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, size INTEGER, mtime_ns INTEGER, entry_dt DATETIME, digest BLOB, digest_partial BLOB, digest_samples BLOB)" drop_table_query = "DROP TABLE IF EXISTS files;" select_query = "SELECT {key} FROM files WHERE path=:path AND size=:size and mtime_ns=:mtime_ns" insert_query = """ INSERT INTO files (path, size, mtime_ns, entry_dt, {key}) VALUES (:path, :size, :mtime_ns, datetime('now'), :value) ON CONFLICT(path) DO UPDATE SET size=:size, mtime_ns=:mtime_ns, entry_dt=datetime('now'), {key}=:value; """ def __init__(self): self.conn = None self.cur = None self.lock = None def connect(self, path: Union[AnyStr, os.PathLike]) -> None: self.conn = sqlite3.connect(path, check_same_thread=False) self.cur = self.conn.cursor() self.lock = Lock() self._check_upgrade() def _check_upgrade(self) -> None: with self.lock: has_schema = self.cur.execute( "SELECT NAME FROM sqlite_master WHERE type='table' AND name='schema_version'" ).fetchall() version = None if has_schema: version = self.cur.execute("SELECT version FROM schema_version ORDER BY version DESC").fetchone()[0] else: self.cur.execute("CREATE TABLE schema_version (version int PRIMARY KEY, description TEXT)") if version != self.schema_version: self.cur.execute(self.drop_table_query) self.cur.execute( "INSERT OR REPLACE INTO schema_version VALUES (:version, :description)", {"version": self.schema_version, "description": self.schema_version_description}, ) self.cur.execute(self.create_table_query) self.conn.commit() def clear(self) -> None: with self.lock: self.cur.execute(self.drop_table_query) self.cur.execute(self.create_table_query) def get(self, path: Path, key: str) -> Union[bytes, None]: stat = path.stat() size = stat.st_size mtime_ns = stat.st_mtime_ns try: with self.lock: self.cur.execute( self.select_query.format(key=key), {"path": str(path), "size": size, "mtime_ns": mtime_ns} ) result = self.cur.fetchone() if result: return result[0] except Exception as ex: logging.warning(f"Couldn't get {key} for {path} w/{size}, {mtime_ns}: {ex}") return None def put(self, path: Path, key: str, value: Any) -> None: stat = path.stat() size = stat.st_size mtime_ns = stat.st_mtime_ns try: with self.lock: self.cur.execute( self.insert_query.format(key=key), {"path": str(path), "size": size, "mtime_ns": mtime_ns, "value": value}, ) except Exception as ex: logging.warning(f"Couldn't put {key} for {path} w/{size}, {mtime_ns}: {ex}") def commit(self) -> None: with self.lock: self.conn.commit() def close(self) -> None: with self.lock: self.cur.close() self.conn.close() filesdb = FilesDB() # Singleton class File: """Represents a file and holds metadata to be used for scanning.""" INITIAL_INFO = {"size": 0, "mtime": 0, "digest": b"", "digest_partial": b"", "digest_samples": b""} # Slots for File make us save quite a bit of memory. In a memory test I've made with a lot of # files, I saved 35% memory usage with "unread" files (no _read_info() call) and gains become # even greater when we take into account read attributes (70%!). Yeah, it's worth it. __slots__ = ("path", "is_ref", "words") + tuple(INITIAL_INFO.keys()) def __init__(self, path): for attrname in self.INITIAL_INFO: setattr(self, attrname, NOT_SET) if type(path) is os.DirEntry: self.path = Path(path.path) self.size = nonone(path.stat().st_size, 0) self.mtime = nonone(path.stat().st_mtime, 0) else: self.path = path def __repr__(self): return f"<{self.__class__.__name__} {str(self.path)}>" def __getattribute__(self, attrname): result = object.__getattribute__(self, attrname) if result is NOT_SET: try: self._read_info(attrname) except Exception as e: logging.warning("An error '%s' was raised while decoding '%s'", e, repr(self.path)) result = object.__getattribute__(self, attrname) if result is NOT_SET: result = self.INITIAL_INFO[attrname] return result def _calc_digest(self): # type: () -> bytes with self.path.open("rb") as fp: file_hash = hasher() # The goal here is to not run out of memory on really big files. However, the chunk # size has to be large enough so that the python loop isn't too costly in terms of # CPU. CHUNK_SIZE = 1024 * 1024 # 1 mb filedata = fp.read(CHUNK_SIZE) while filedata: file_hash.update(filedata) filedata = fp.read(CHUNK_SIZE) return file_hash.digest() def _calc_digest_partial(self): # type: () -> bytes # This offset is where we should start reading the file to get a partial hash # For audio file, it should be where audio data starts offset, size = (0x4000, 0x4000) with self.path.open("rb") as fp: fp.seek(offset) partial_data = fp.read(size) return hasher(partial_data).digest() def _calc_digest_samples(self) -> bytes: size = self.size with self.path.open("rb") as fp: # Chunk at 25% of the file fp.seek(floor(size * 25 / 100), 0) file_data = fp.read(CHUNK_SIZE) file_hash = hasher(file_data) # Chunk at 60% of the file fp.seek(floor(size * 60 / 100), 0) file_data = fp.read(CHUNK_SIZE) file_hash.update(file_data) # Last chunk of the file fp.seek(-CHUNK_SIZE, 2) file_data = fp.read(CHUNK_SIZE) file_hash.update(file_data) return file_hash.digest() def _read_info(self, field): # print(f"_read_info({field}) for {self}") if field in ("size", "mtime"): stats = self.path.stat() self.size = nonone(stats.st_size, 0) self.mtime = nonone(stats.st_mtime, 0) elif field == "digest_partial": self.digest_partial = filesdb.get(self.path, "digest_partial") if self.digest_partial is None: self.digest_partial = self._calc_digest_partial() filesdb.put(self.path, "digest_partial", self.digest_partial) elif field == "digest": self.digest = filesdb.get(self.path, "digest") if self.digest is None: self.digest = self._calc_digest() filesdb.put(self.path, "digest", self.digest) elif field == "digest_samples": size = self.size # Might as well hash such small files entirely. if size <= MIN_FILE_SIZE: setattr(self, field, self.digest) return self.digest_samples = filesdb.get(self.path, "digest_samples") if self.digest_samples is None: self.digest_samples = self._calc_digest_samples() filesdb.put(self.path, "digest_samples", self.digest_samples) def _read_all_info(self, attrnames=None): """Cache all possible info. If `attrnames` is not None, caches only attrnames. """ if attrnames is None: attrnames = self.INITIAL_INFO.keys() for attrname in attrnames: getattr(self, attrname) # --- Public @classmethod def can_handle(cls, path): """Returns whether this file wrapper class can handle ``path``.""" return not path.is_symlink() and path.is_file() def rename(self, newname): if newname == self.name: return destpath = self.path.parent.joinpath(newname) if destpath.exists(): raise AlreadyExistsError(newname, self.path.parent) try: self.path.rename(destpath) except OSError: raise OperationError(self) if not destpath.exists(): raise OperationError(self) self.path = destpath def get_display_info(self, group, delta): """Returns a display-ready dict of dupe's data.""" raise NotImplementedError() # --- Properties @property def extension(self): return get_file_ext(self.name) @property def name(self): return self.path.name @property def folder_path(self): return self.path.parent class Folder(File): """A wrapper around a folder path. It has the size/digest info of a File, but its value is the sum of its subitems. """ __slots__ = File.__slots__ + ("_subfolders",) def __init__(self, path): File.__init__(self, path) self.size = NOT_SET self._subfolders = None def _all_items(self): folders = self.subfolders files = get_files(self.path) return folders + files def _read_info(self, field): # print(f"_read_info({field}) for Folder {self}") if field in {"size", "mtime"}: size = sum((f.size for f in self._all_items()), 0) self.size = size stats = self.path.stat() self.mtime = nonone(stats.st_mtime, 0) elif field in {"digest", "digest_partial", "digest_samples"}: # What's sensitive here is that we must make sure that subfiles' # digest are always added up in the same order, but we also want a # different digest if a file gets moved in a different subdirectory. def get_dir_digest_concat(): items = self._all_items() items.sort(key=lambda f: f.path) digests = [getattr(f, field) for f in items] return b"".join(digests) digest = hasher(get_dir_digest_concat()).digest() setattr(self, field, digest) @property def subfolders(self): if self._subfolders is None: with os.scandir(self.path) as iter: subfolders = [p for p in iter if not p.is_symlink() and p.is_dir()] self._subfolders = [self.__class__(p) for p in subfolders] return self._subfolders @classmethod def can_handle(cls, path): return not path.is_symlink() and path.is_dir() def get_file(path, fileclasses=[File]): """Wraps ``path`` around its appropriate :class:`File` class. Whether a class is "appropriate" is decided by :meth:`File.can_handle` :param Path path: path to wrap :param fileclasses: List of candidate :class:`File` classes """ for fileclass in fileclasses: if fileclass.can_handle(path): return fileclass(path) def get_files(path, fileclasses=[File]): """Returns a list of :class:`File` for each file contained in ``path``. :param Path path: path to scan :param fileclasses: List of candidate :class:`File` classes """ assert all(issubclass(fileclass, File) for fileclass in fileclasses) try: result = [] with os.scandir(path) as iter: for item in iter: file = get_file(item, fileclasses=fileclasses) if file is not None: result.append(file) return result except OSError: raise InvalidPath(path) dupeguru-4.3.1/core/gui/000077500000000000000000000000001426171743600151065ustar00rootroot00000000000000dupeguru-4.3.1/core/gui/__init__.py000066400000000000000000000013471426171743600172240ustar00rootroot00000000000000""" Meta GUI elements in dupeGuru ----------------------------- dupeGuru is designed with a `cross-toolkit`_ approach in mind. It means that its core code (which doesn't depend on any GUI toolkit) has elements which preformat core information in a way that makes it easy for a UI layer to consume. For example, we have :class:`~core.gui.ResultTable` which takes information from :class:`~core.results.Results` and mashes it in rows and columns which are ready to be fetched by either Cocoa's ``NSTableView`` or Qt's ``QTableView``. It tells them which cell is supposed to be blue, which is supposed to be orange, does the sorting logic, holds selection, etc.. .. _cross-toolkit: http://www.hardcoded.net/articles/cross-toolkit-software """ dupeguru-4.3.1/core/gui/base.py000066400000000000000000000016471426171743600164020ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2010-02-06 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.notify import Listener class DupeGuruGUIObject(Listener): def __init__(self, app): Listener.__init__(self, app) self.app = app def directories_changed(self): # Implemented in child classes pass def dupes_selected(self): # Implemented in child classes pass def marking_changed(self): # Implemented in child classes pass def results_changed(self): # Implemented in child classes pass def results_changed_but_keep_selection(self): # Implemented in child classes pass dupeguru-4.3.1/core/gui/deletion_options.py000066400000000000000000000075171426171743600210500ustar00rootroot00000000000000# Created On: 2012-05-30 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import os from hscommon.gui.base import GUIObject from hscommon.trans import tr class DeletionOptionsView: """Expected interface for :class:`DeletionOptions`'s view. *Not actually used in the code. For documentation purposes only.* Our view presents the user with an appropriate way (probably a mix of checkboxes and radio buttons) to set the different flags in :class:`DeletionOptions`. Note that :attr:`DeletionOptions.use_hardlinks` is only relevant if :attr:`DeletionOptions.link_deleted` is true. This is why we toggle the "enabled" state of that flag. We expect the view to set :attr:`DeletionOptions.link_deleted` immediately as the user changes its value because it will toggle :meth:`set_hardlink_option_enabled` Other than the flags, there's also a prompt message which has a dynamic content, defined by :meth:`update_msg`. """ def update_msg(self, msg: str): """Update the dialog's prompt with ``str``.""" def show(self): """Show the dialog in a modal fashion. Returns whether the dialog was "accepted" (the user pressed OK). """ def set_hardlink_option_enabled(self, is_enabled: bool): """Enable or disable the widget controlling :attr:`DeletionOptions.use_hardlinks`.""" class DeletionOptions(GUIObject): """Present the user with deletion options before proceeding. When the user activates "Send to trash", we present him with a couple of options that changes the behavior of that deletion operation. """ def __init__(self): GUIObject.__init__(self) #: Whether symlinks or hardlinks are used when doing :attr:`link_deleted`. #: *bool*. *get/set* self.use_hardlinks = False #: Delete dupes directly and don't send to trash. #: *bool*. *get/set* self.direct = False def show(self, mark_count): """Prompt the user with a modal dialog offering our deletion options. :param int mark_count: Number of dupes marked for deletion. :rtype: bool :returns: Whether the user accepted the dialog (we cancel deletion if false). """ self._link_deleted = False self.view.set_hardlink_option_enabled(False) self.use_hardlinks = False self.direct = False msg = tr("You are sending {} file(s) to the Trash.").format(mark_count) self.view.update_msg(msg) return self.view.show() def supports_links(self): """Returns whether our platform supports symlinks.""" # When on a platform that doesn't implement it, calling os.symlink() (with the wrong number # of arguments) raises NotImplementedError, which allows us to gracefully check for the # feature. try: os.symlink() except NotImplementedError: # Windows XP, not supported return False except OSError: # Vista+, symbolic link privilege not held return False except TypeError: # wrong number of arguments return True @property def link_deleted(self): """Replace deleted dupes with symlinks (or hardlinks) to the dupe group reference. *bool*. *get/set* Whether the link is a symlink or hardlink is decided by :attr:`use_hardlinks`. """ return self._link_deleted @link_deleted.setter def link_deleted(self, value): self._link_deleted = value hardlinks_enabled = value and self.supports_links() self.view.set_hardlink_option_enabled(hardlinks_enabled) dupeguru-4.3.1/core/gui/details_panel.py000066400000000000000000000031601426171743600202640ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2010-02-05 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.gui.base import GUIObject from core.gui.base import DupeGuruGUIObject class DetailsPanel(GUIObject, DupeGuruGUIObject): def __init__(self, app): GUIObject.__init__(self, multibind=True) DupeGuruGUIObject.__init__(self, app) self._table = [] def _view_updated(self): self._refresh() self.view.refresh() # --- Private def _refresh(self): if self.app.selected_dupes: dupe = self.app.selected_dupes[0] group = self.app.results.get_group_of_duplicate(dupe) else: dupe = None group = None data1 = self.app.get_display_info(dupe, group, False) # we don't want the two sides of the table to display the stats for the same file ref = group.ref if group is not None and group.ref is not dupe else None data2 = self.app.get_display_info(ref, group, False) columns = self.app.result_table.COLUMNS[1:] # first column is the 'marked' column self._table = [(c.display, data1[c.name], data2[c.name]) for c in columns] # --- Public def row_count(self): return len(self._table) def row(self, row_index): return self._table[row_index] # --- Event Handlers def dupes_selected(self): self._view_updated() dupeguru-4.3.1/core/gui/directory_tree.py000066400000000000000000000064231426171743600205100ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2010-02-06 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.gui.tree import Tree, Node from core.directories import DirectoryState from core.gui.base import DupeGuruGUIObject STATE_ORDER = [DirectoryState.NORMAL, DirectoryState.REFERENCE, DirectoryState.EXCLUDED] # Lazily loads children class DirectoryNode(Node): def __init__(self, tree, path, name): Node.__init__(self, name) self._tree = tree self._directory_path = path self._loaded = False self._state = STATE_ORDER.index(self._tree.app.directories.get_state(path)) def __len__(self): if not self._loaded: self._load() return Node.__len__(self) def _load(self): self.clear() subpaths = self._tree.app.directories.get_subfolders(self._directory_path) for path in subpaths: self.append(DirectoryNode(self._tree, path, path.name)) self._loaded = True def update_all_states(self): self._state = STATE_ORDER.index(self._tree.app.directories.get_state(self._directory_path)) for node in self: node.update_all_states() # The state propery is an index to the combobox @property def state(self): return self._state @state.setter def state(self, value): if value == self._state: return self._state = value state = STATE_ORDER[value] self._tree.app.directories.set_state(self._directory_path, state) self._tree.update_all_states() class DirectoryTree(Tree, DupeGuruGUIObject): # --- model -> view calls: # refresh() # refresh_states() # when only states label need to be refreshed # def __init__(self, app): Tree.__init__(self) DupeGuruGUIObject.__init__(self, app) def _view_updated(self): self._refresh() self.view.refresh() def _refresh(self): self.clear() for path in self.app.directories: self.append(DirectoryNode(self, path, str(path))) def add_directory(self, path): self.app.add_directory(path) def remove_selected(self): selected_paths = self.selected_paths if not selected_paths: return to_delete = [path[0] for path in selected_paths if len(path) == 1] if to_delete: self.app.remove_directories(to_delete) else: # All selected nodes or on second-or-more level, exclude them. nodes = self.selected_nodes newstate = DirectoryState.EXCLUDED if all(node.state == DirectoryState.EXCLUDED for node in nodes): newstate = DirectoryState.NORMAL for node in nodes: node.state = newstate def select_all(self): self.selected_nodes = list(self) self.view.refresh() def update_all_states(self): for node in self: node.update_all_states() self.view.refresh_states() # --- Event Handlers def directories_changed(self): self._view_updated() dupeguru-4.3.1/core/gui/exclude_list_dialog.py000066400000000000000000000061241426171743600214660ustar00rootroot00000000000000# Created On: 2012/03/13 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from core.gui.exclude_list_table import ExcludeListTable from core.exclude import has_sep from os import sep import logging class ExcludeListDialogCore: def __init__(self, app): self.app = app self.exclude_list = self.app.exclude_list # Markable from exclude.py self.exclude_list_table = ExcludeListTable(self, app) # GUITable, this is the "model" def restore_defaults(self): self.exclude_list.restore_defaults() self.refresh() def refresh(self): self.exclude_list_table.refresh() def remove_selected(self): for row in self.exclude_list_table.selected_rows: self.exclude_list_table.remove(row) self.exclude_list.remove(row.regex) self.refresh() def rename_selected(self, newregex): """Rename the selected regex to ``newregex``. If there is more than one selected row, the first one is used. :param str newregex: The regex to rename the row's regex to. :return bool: true if success, false if error. """ try: r = self.exclude_list_table.selected_rows[0] self.exclude_list.rename(r.regex, newregex) self.refresh() return True except Exception as e: logging.warning(f"Error while renaming regex to {newregex}: {e}") return False def add(self, regex): self.exclude_list.add(regex) self.exclude_list.mark(regex) self.exclude_list_table.add(regex) def test_string(self, test_string): """Set the highlight property on each row when its regex matches the test_string supplied. Return True if any row matched.""" matched = False for row in self.exclude_list_table.rows: compiled_regex = self.exclude_list.get_compiled(row.regex) if self.is_match(test_string, compiled_regex): row.highlight = True matched = True else: row.highlight = False return matched def is_match(self, test_string, compiled_regex): # This method is like an inverted version of ExcludeList.is_excluded() if not compiled_regex: return False matched = False # Test only the filename portion of the path if not has_sep(compiled_regex.pattern) and sep in test_string: filename = test_string.rsplit(sep, 1)[1] if compiled_regex.fullmatch(filename): matched = True return matched # Test the entire path + filename if compiled_regex.fullmatch(test_string): matched = True return matched def reset_rows_highlight(self): for row in self.exclude_list_table.rows: row.highlight = False def show(self): self.view.show() dupeguru-4.3.1/core/gui/exclude_list_table.py000066400000000000000000000057301426171743600213200ustar00rootroot00000000000000# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from core.gui.base import DupeGuruGUIObject from hscommon.gui.table import GUITable, Row from hscommon.gui.column import Column, Columns from hscommon.trans import trget tr = trget("ui") class ExcludeListTable(GUITable, DupeGuruGUIObject): COLUMNS = [Column("marked", ""), Column("regex", tr("Regular Expressions"))] def __init__(self, exclude_list_dialog, app): GUITable.__init__(self) DupeGuruGUIObject.__init__(self, app) self._columns = Columns(self) self.dialog = exclude_list_dialog def rename_selected(self, newname): row = self.selected_row if row is None: return False row._data = None return self.dialog.rename_selected(newname) # --- Virtual def _do_add(self, regex): """(Virtual) Creates a new row, adds it in the table. Returns ``(row, insert_index)``.""" # Return index 0 to insert at the top return ExcludeListRow(self, self.dialog.exclude_list.is_marked(regex), regex), 0 def _do_delete(self): self.dialog.exclude_list.remove(self.selected_row.regex) # --- Override def add(self, regex): row, insert_index = self._do_add(regex) self.insert(insert_index, row) self.view.refresh() def _fill(self): for enabled, regex in self.dialog.exclude_list: self.append(ExcludeListRow(self, enabled, regex)) def refresh(self, refresh_view=True): """Override to avoid keeping previous selection in case of multiple rows selected previously.""" self.cancel_edits() del self[:] self._fill() if refresh_view: self.view.refresh() class ExcludeListRow(Row): def __init__(self, table, enabled, regex): Row.__init__(self, table) self._app = table.app self._data = None self.enabled = str(enabled) self.regex = str(regex) self.highlight = False @property def data(self): if self._data is None: self._data = {"marked": self.enabled, "regex": self.regex} return self._data @property def markable(self): return self._app.exclude_list.is_markable(self.regex) @property def marked(self): return self._app.exclude_list.is_marked(self.regex) @marked.setter def marked(self, value): if value: self._app.exclude_list.mark(self.regex) else: self._app.exclude_list.unmark(self.regex) @property def error(self): # This assumes error() returns an Exception() message = self._app.exclude_list.error(self.regex) if hasattr(message, "msg"): return self._app.exclude_list.error(self.regex).msg else: return message # Exception object dupeguru-4.3.1/core/gui/ignore_list_dialog.py000066400000000000000000000022741426171743600213220ustar00rootroot00000000000000# Created On: 2012/03/13 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.trans import tr from core.gui.ignore_list_table import IgnoreListTable class IgnoreListDialog: # --- View interface # show() # def __init__(self, app): self.app = app self.ignore_list = self.app.ignore_list self.ignore_list_table = IgnoreListTable(self) # GUITable def clear(self): if not self.ignore_list: return msg = tr("Do you really want to remove all %d items from the ignore list?") % len(self.ignore_list) if self.app.view.ask_yes_no(msg): self.ignore_list.clear() self.refresh() def refresh(self): self.ignore_list_table.refresh() def remove_selected(self): for row in self.ignore_list_table.selected_rows: self.ignore_list.remove(row.path1_original, row.path2_original) self.refresh() def show(self): self.view.show() dupeguru-4.3.1/core/gui/ignore_list_table.py000066400000000000000000000024031426171743600211440ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2012-03-13 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.gui.table import GUITable, Row from hscommon.gui.column import Column, Columns from hscommon.trans import trget coltr = trget("columns") class IgnoreListTable(GUITable): COLUMNS = [ # the str concat below saves us needless localization. Column("path1", coltr("File Path") + " 1"), Column("path2", coltr("File Path") + " 2"), ] def __init__(self, ignore_list_dialog): GUITable.__init__(self) self._columns = Columns(self) self.view = None self.dialog = ignore_list_dialog # --- Override def _fill(self): for path1, path2 in self.dialog.ignore_list: self.append(IgnoreListRow(self, path1, path2)) class IgnoreListRow(Row): def __init__(self, table, path1, path2): Row.__init__(self, table) self.path1_original = path1 self.path2_original = path2 self.path1 = str(path1) self.path2 = str(path2) dupeguru-4.3.1/core/gui/prioritize_dialog.py000066400000000000000000000057161426171743600212100ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2011-09-06 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.gui.base import GUIObject from hscommon.gui.selectable_list import GUISelectableList class CriterionCategoryList(GUISelectableList): def __init__(self, dialog): self.dialog = dialog GUISelectableList.__init__(self, [c.NAME for c in dialog.categories]) def _update_selection(self): self.dialog.select_category(self.dialog.categories[self.selected_index]) GUISelectableList._update_selection(self) class PrioritizationList(GUISelectableList): def __init__(self, dialog): self.dialog = dialog GUISelectableList.__init__(self) def _refresh_contents(self): self[:] = [crit.display for crit in self.dialog.prioritizations] def move_indexes(self, indexes, dest_index): indexes.sort() prilist = self.dialog.prioritizations selected = [prilist[i] for i in indexes] for i in reversed(indexes): del prilist[i] prilist[dest_index:dest_index] = selected self._refresh_contents() def remove_selected(self): prilist = self.dialog.prioritizations for i in sorted(self.selected_indexes, reverse=True): del prilist[i] self._refresh_contents() class PrioritizeDialog(GUIObject): def __init__(self, app): GUIObject.__init__(self) self.app = app self.categories = [cat(app.results) for cat in app._prioritization_categories()] self.category_list = CriterionCategoryList(self) self.criteria = [] self.criteria_list = GUISelectableList() self.prioritizations = [] self.prioritization_list = PrioritizationList(self) # --- Override def _view_updated(self): self.category_list.select(0) # --- Private def _sort_key(self, dupe): return tuple(crit.sort_key(dupe) for crit in self.prioritizations) # --- Public def select_category(self, category): self.criteria = category.criteria_list() self.criteria_list[:] = [c.display_value for c in self.criteria] def add_selected(self): # Add selected criteria in criteria_list to prioritization_list. if self.criteria_list.selected_index is None: return for i in self.criteria_list.selected_indexes: crit = self.criteria[i] self.prioritizations.append(crit) del crit self.prioritization_list[:] = [crit.display for crit in self.prioritizations] def remove_selected(self): self.prioritization_list.remove_selected() self.prioritization_list.select([]) def perform_reprioritization(self): self.app.reprioritize_groups(self._sort_key) dupeguru-4.3.1/core/gui/problem_dialog.py000066400000000000000000000015461426171743600204450ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2010-04-12 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon import desktop from core.gui.problem_table import ProblemTable class ProblemDialog: def __init__(self, app): self.app = app self._selected_dupe = None self.problem_table = ProblemTable(self) def refresh(self): self._selected_dupe = None self.problem_table.refresh() def reveal_selected_dupe(self): if self._selected_dupe is not None: desktop.reveal_path(self._selected_dupe.path) def select_dupe(self, dupe): self._selected_dupe = dupe dupeguru-4.3.1/core/gui/problem_table.py000066400000000000000000000024211426171743600202660ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2010-04-12 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.gui.table import GUITable, Row from hscommon.gui.column import Column, Columns from hscommon.trans import trget coltr = trget("columns") class ProblemTable(GUITable): COLUMNS = [ Column("path", coltr("File Path")), Column("msg", coltr("Error Message")), ] def __init__(self, problem_dialog): GUITable.__init__(self) self._columns = Columns(self) self.dialog = problem_dialog # --- Override def _update_selection(self): row = self.selected_row dupe = row.dupe if row is not None else None self.dialog.select_dupe(dupe) def _fill(self): problems = self.dialog.app.results.problems for dupe, msg in problems: self.append(ProblemRow(self, dupe, msg)) class ProblemRow(Row): def __init__(self, table, dupe, msg): Row.__init__(self, table) self.dupe = dupe self.msg = msg self.path = str(dupe.path) dupeguru-4.3.1/core/gui/result_table.py000066400000000000000000000141601426171743600201470ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2010-02-11 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from operator import attrgetter from hscommon.gui.table import GUITable, Row from hscommon.gui.column import Columns from core.gui.base import DupeGuruGUIObject class DupeRow(Row): def __init__(self, table, group, dupe): Row.__init__(self, table) self._app = table.app self._group = group self._dupe = dupe self._data = None self._data_delta = None self._delta_columns = None def is_cell_delta(self, column_name): """Returns whether a cell is in delta mode (orange color). If the result table is in delta mode, returns True if the column is one of the "delta columns", that is, one of the columns that display a a differential value rather than an absolute value. If not, returns True if the dupe's value is different from its ref value. """ if not self.table.delta_values: return False if self.isref: return False if self._delta_columns is None: # table.DELTA_COLUMNS are always "delta" self._delta_columns = self.table.DELTA_COLUMNS.copy() dupe_info = self.data if self._group.ref is None: return False ref_info = self._group.ref.get_display_info(group=self._group, delta=False) for key, value in dupe_info.items(): if (key not in self._delta_columns) and (ref_info[key].lower() != value.lower()): self._delta_columns.add(key) return column_name in self._delta_columns @property def data(self): if self._data is None: self._data = self._app.get_display_info(self._dupe, self._group, False) return self._data @property def data_delta(self): if self._data_delta is None: self._data_delta = self._app.get_display_info(self._dupe, self._group, True) return self._data_delta @property def isref(self): return self._dupe is self._group.ref @property def markable(self): return self._app.results.is_markable(self._dupe) @property def marked(self): return self._app.results.is_marked(self._dupe) @marked.setter def marked(self, value): self._app.mark_dupe(self._dupe, value) class ResultTable(GUITable, DupeGuruGUIObject): def __init__(self, app): GUITable.__init__(self) DupeGuruGUIObject.__init__(self, app) self._columns = Columns(self, prefaccess=app, savename="ResultTable") self._power_marker = False self._delta_values = False self._sort_descriptors = ("name", True) # --- Override def _view_updated(self): self._refresh_with_view() def _restore_selection(self, previous_selection): if self.app.selected_dupes: to_find = set(self.app.selected_dupes) indexes = [i for i, r in enumerate(self) if r._dupe in to_find] self.selected_indexes = indexes def _update_selection(self): rows = self.selected_rows self.app._select_dupes(list(map(attrgetter("_dupe"), rows))) def _fill(self): if not self.power_marker: for group in self.app.results.groups: self.append(DupeRow(self, group, group.ref)) for dupe in group.dupes: self.append(DupeRow(self, group, dupe)) else: for dupe in self.app.results.dupes: group = self.app.results.get_group_of_duplicate(dupe) self.append(DupeRow(self, group, dupe)) def _refresh_with_view(self): self.refresh() self.view.show_selected_row() # --- Public def get_row_value(self, index, column): try: row = self[index] except IndexError: return "---" if self.delta_values: return row.data_delta[column] else: return row.data[column] def rename_selected(self, newname): row = self.selected_row if row is None: # There's all kinds of way the current row can be swept off during rename. When it # happens, selected_row will be None. return False row._data = None row._data_delta = None return self.app.rename_selected(newname) def sort(self, key, asc): if self.power_marker: self.app.results.sort_dupes(key, asc, self.delta_values) else: self.app.results.sort_groups(key, asc) self._sort_descriptors = (key, asc) self._refresh_with_view() # --- Properties @property def power_marker(self): return self._power_marker @power_marker.setter def power_marker(self, value): if value == self._power_marker: return self._power_marker = value key, asc = self._sort_descriptors self.sort(key, asc) # no need to refresh, it has happened in sort() @property def delta_values(self): return self._delta_values @delta_values.setter def delta_values(self, value): if value == self._delta_values: return self._delta_values = value self.refresh() @property def selected_dupe_count(self): return sum(1 for row in self.selected_rows if not row.isref) # --- Event Handlers def marking_changed(self): self.view.invalidate_markings() def results_changed(self): self._refresh_with_view() def results_changed_but_keep_selection(self): # What we want to to here is that instead of restoring selected *dupes* after refresh, we # restore selected *paths*. indexes = self.selected_indexes self.refresh(refresh_view=False) self.select(indexes) self.view.refresh() def save_session(self): self._columns.save_columns() dupeguru-4.3.1/core/gui/stats_label.py000066400000000000000000000012011426171743600177470ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2010-02-11 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from core.gui.base import DupeGuruGUIObject class StatsLabel(DupeGuruGUIObject): def _view_updated(self): self.view.refresh() @property def display(self): return self.app.stat_line def results_changed(self): self.view.refresh() marking_changed = results_changed dupeguru-4.3.1/core/ignore.py000066400000000000000000000101411426171743600161540ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2006/05/02 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from xml.etree import ElementTree as ET from hscommon.util import FileOrPath class IgnoreList: """An ignore list implementation that is iterable, filterable and exportable to XML. Call Ignore to add an ignore list entry, and AreIgnore to check if 2 items are in the list. When iterated, 2 sized tuples will be returned, the tuples containing 2 items ignored together. """ # ---Override def __init__(self): self.clear() def __iter__(self): for first, seconds in self._ignored.items(): for second in seconds: yield (first, second) def __len__(self): return self._count # ---Public def are_ignored(self, first, second): def do_check(first, second): try: matches = self._ignored[first] return second in matches except KeyError: return False return do_check(first, second) or do_check(second, first) def clear(self): self._ignored = {} self._count = 0 def filter(self, func): """Applies a filter on all ignored items, and remove all matches where func(first,second) doesn't return True. """ filtered = IgnoreList() for first, second in self: if func(first, second): filtered.ignore(first, second) self._ignored = filtered._ignored self._count = filtered._count def ignore(self, first, second): if self.are_ignored(first, second): return try: matches = self._ignored[first] matches.add(second) except KeyError: try: matches = self._ignored[second] matches.add(first) except KeyError: matches = set() matches.add(second) self._ignored[first] = matches self._count += 1 def remove(self, first, second): def inner(first, second): try: matches = self._ignored[first] if second in matches: matches.discard(second) if not matches: del self._ignored[first] self._count -= 1 return True else: return False except KeyError: return False if not inner(first, second) and not inner(second, first): raise ValueError() def load_from_xml(self, infile): """Loads the ignore list from a XML created with save_to_xml. infile can be a file object or a filename. """ try: root = ET.parse(infile).getroot() except Exception: return file_elems = (e for e in root if e.tag == "file") for fn in file_elems: file_path = fn.get("path") if not file_path: continue subfile_elems = (e for e in fn if e.tag == "file") for sfn in subfile_elems: subfile_path = sfn.get("path") if subfile_path: self.ignore(file_path, subfile_path) def save_to_xml(self, outfile): """Create a XML file that can be used by load_from_xml. outfile can be a file object or a filename. """ root = ET.Element("ignore_list") for filename, subfiles in self._ignored.items(): file_node = ET.SubElement(root, "file") file_node.set("path", filename) for subfilename in subfiles: subfile_node = ET.SubElement(file_node, "file") subfile_node.set("path", subfilename) tree = ET.ElementTree(root) with FileOrPath(outfile, "wb") as fp: tree.write(fp, encoding="utf-8") dupeguru-4.3.1/core/markable.py000066400000000000000000000057701426171743600164630ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2006/02/23 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html class Markable: def __init__(self): self.__marked = set() self.__inverted = False # ---Virtual # About did_mark and did_unmark: They only happen what an object is actually added/removed # in self.__marked, and is not affected by __inverted. Thus, self.mark while __inverted # is True will launch _DidUnmark. def _did_mark(self, o): # Implemented in child classes pass def _did_unmark(self, o): # Implemented in child classes pass def _get_markable_count(self): return 0 def _is_markable(self, o): return True # ---Protected def _remove_mark_flag(self, o): try: self.__marked.remove(o) self._did_unmark(o) except KeyError: pass # ---Public def is_marked(self, o): if not self._is_markable(o): return False is_marked = o in self.__marked if self.__inverted: is_marked = not is_marked return is_marked def mark(self, o): if self.is_marked(o): return False if not self._is_markable(o): return False return self.mark_toggle(o) def mark_multiple(self, objects): for o in objects: self.mark(o) def mark_all(self): self.mark_none() self.__inverted = True def mark_invert(self): self.__inverted = not self.__inverted def mark_none(self): for o in self.__marked: self._did_unmark(o) self.__marked = set() self.__inverted = False def mark_toggle(self, o): try: self.__marked.remove(o) self._did_unmark(o) except KeyError: if not self._is_markable(o): return False self.__marked.add(o) self._did_mark(o) return True def mark_toggle_multiple(self, objects): for o in objects: self.mark_toggle(o) def unmark(self, o): if not self.is_marked(o): return False return self.mark_toggle(o) def unmark_multiple(self, objects): for o in objects: self.unmark(o) # --- Properties @property def mark_count(self): if self.__inverted: return self._get_markable_count() - len(self.__marked) else: return len(self.__marked) @property def mark_inverted(self): return self.__inverted class MarkableList(list, Markable): def __init__(self): list.__init__(self) Markable.__init__(self) def _get_markable_count(self): return len(self) def _is_markable(self, o): return o in self dupeguru-4.3.1/core/me/000077500000000000000000000000001426171743600147235ustar00rootroot00000000000000dupeguru-4.3.1/core/me/__init__.py000066400000000000000000000001021426171743600170250ustar00rootroot00000000000000from core.me import fs, prioritize, result_table, scanner # noqa dupeguru-4.3.1/core/me/fs.py000066400000000000000000000076501426171743600157150ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2009-10-23 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import mutagen from hscommon.util import get_file_ext, format_size, format_time from core.util import format_timestamp, format_perc, format_words, format_dupe_count from core import fs TAG_FIELDS = { "audiosize", "duration", "bitrate", "samplerate", "title", "artist", "album", "genre", "year", "track", "comment", } # This is a temporary workaround for migration from hsaudiotag for the can_handle method SUPPORTED_EXTS = {"mp3", "wma", "m4a", "m4p", "ogg", "flac", "aif", "aiff", "aifc"} class MusicFile(fs.File): INITIAL_INFO = fs.File.INITIAL_INFO.copy() INITIAL_INFO.update( { "audiosize": 0, "bitrate": 0, "duration": 0, "samplerate": 0, "artist": "", "album": "", "title": "", "genre": "", "comment": "", "year": "", "track": 0, } ) __slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys()) @classmethod def can_handle(cls, path): if not fs.File.can_handle(path): return False return get_file_ext(path.name) in SUPPORTED_EXTS def get_display_info(self, group, delta): size = self.size duration = self.duration bitrate = self.bitrate samplerate = self.samplerate mtime = self.mtime m = group.get_match_of(self) if m: percentage = m.percentage dupe_count = 0 if delta: r = group.ref size -= r.size duration -= r.duration bitrate -= r.bitrate samplerate -= r.samplerate mtime -= r.mtime else: percentage = group.percentage dupe_count = len(group.dupes) dupe_folder_path = getattr(self, "display_folder_path", self.folder_path) return { "name": self.name, "folder_path": str(dupe_folder_path), "size": format_size(size, 2, 2, False), "duration": format_time(duration, with_hours=False), "bitrate": str(bitrate), "samplerate": str(samplerate), "extension": self.extension, "mtime": format_timestamp(mtime, delta and m), "title": self.title, "artist": self.artist, "album": self.album, "genre": self.genre, "year": self.year, "track": str(self.track), "comment": self.comment, "percentage": format_perc(percentage), "words": format_words(self.words) if hasattr(self, "words") else "", "dupe_count": format_dupe_count(dupe_count), } def _read_info(self, field): fs.File._read_info(self, field) if field in TAG_FIELDS: # The various conversions here are to make this look like the previous implementation file = mutagen.File(str(self.path), easy=True) self.audiosize = self.path.stat().st_size self.bitrate = file.info.bitrate / 1000 self.duration = file.info.length self.samplerate = file.info.sample_rate self.artist = ", ".join(file.tags.get("artist") or []) self.album = ", ".join(file.tags.get("album") or []) self.title = ", ".join(file.tags.get("title") or []) self.genre = ", ".join(file.tags.get("genre") or []) self.comment = ", ".join(file.tags.get("comment") or [""]) self.year = ", ".join(file.tags.get("date") or []) self.track = (file.tags.get("tracknumber") or [""])[0] dupeguru-4.3.1/core/me/prioritize.py000066400000000000000000000022251426171743600174760ustar00rootroot00000000000000# Created On: 2011/09/16 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.trans import trget from core.prioritize import ( KindCategory, FolderCategory, FilenameCategory, NumericalCategory, SizeCategory, MtimeCategory, ) coltr = trget("columns") class DurationCategory(NumericalCategory): NAME = coltr("Duration") def extract_value(self, dupe): return dupe.duration class BitrateCategory(NumericalCategory): NAME = coltr("Bitrate") def extract_value(self, dupe): return dupe.bitrate class SamplerateCategory(NumericalCategory): NAME = coltr("Samplerate") def extract_value(self, dupe): return dupe.samplerate def all_categories(): return [ KindCategory, FolderCategory, FilenameCategory, SizeCategory, DurationCategory, BitrateCategory, SamplerateCategory, MtimeCategory, ] dupeguru-4.3.1/core/me/result_table.py000066400000000000000000000035241426171743600177660ustar00rootroot00000000000000# Created On: 2011-11-27 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.gui.column import Column from hscommon.trans import trget from core.gui.result_table import ResultTable as ResultTableBase coltr = trget("columns") class ResultTable(ResultTableBase): COLUMNS = [ Column("marked", ""), Column("name", coltr("Filename")), Column("folder_path", coltr("Folder"), visible=False, optional=True), Column("size", coltr("Size (MB)"), optional=True), Column("duration", coltr("Time"), optional=True), Column("bitrate", coltr("Bitrate"), optional=True), Column("samplerate", coltr("Sample Rate"), visible=False, optional=True), Column("extension", coltr("Kind"), optional=True), Column("mtime", coltr("Modification"), visible=False, optional=True), Column("title", coltr("Title"), visible=False, optional=True), Column("artist", coltr("Artist"), visible=False, optional=True), Column("album", coltr("Album"), visible=False, optional=True), Column("genre", coltr("Genre"), visible=False, optional=True), Column("year", coltr("Year"), visible=False, optional=True), Column("track", coltr("Track Number"), visible=False, optional=True), Column("comment", coltr("Comment"), visible=False, optional=True), Column("percentage", coltr("Match %"), optional=True), Column("words", coltr("Words Used"), visible=False, optional=True), Column("dupe_count", coltr("Dupe Count"), visible=False, optional=True), ] DELTA_COLUMNS = {"size", "duration", "bitrate", "samplerate", "mtime"} dupeguru-4.3.1/core/me/scanner.py000066400000000000000000000016211426171743600167260ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.trans import tr from core.scanner import Scanner as ScannerBase, ScanOption, ScanType class ScannerME(ScannerBase): @staticmethod def _key_func(dupe): return (-dupe.bitrate, -dupe.size) @staticmethod def get_scan_options(): return [ ScanOption(ScanType.FILENAME, tr("Filename")), ScanOption(ScanType.FIELDS, tr("Filename - Fields")), ScanOption(ScanType.FIELDSNOORDER, tr("Filename - Fields (No Order)")), ScanOption(ScanType.TAG, tr("Tags")), ScanOption(ScanType.CONTENTS, tr("Contents")), ] dupeguru-4.3.1/core/pe/000077500000000000000000000000001426171743600147265ustar00rootroot00000000000000dupeguru-4.3.1/core/pe/__init__.py000066400000000000000000000002311426171743600170330ustar00rootroot00000000000000from core.pe import ( # noqa block, cache, exif, matchblock, matchexif, photo, prioritize, result_table, scanner, ) dupeguru-4.3.1/core/pe/block.py000066400000000000000000000104711426171743600163750ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2006/09/01 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from core.pe._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 # NOQA # Converted to C # def getblock(image): # """Returns a 3 sized tuple containing the mean color of 'image'. # # image: a PIL image or crop. # """ # if image.size[0]: # pixel_count = image.size[0] * image.size[1] # red = green = blue = 0 # for r,g,b in image.getdata(): # red += r # green += g # blue += b # return (red // pixel_count, green // pixel_count, blue // pixel_count) # else: # return (0,0,0) # This is not used anymore # def getblocks(image,blocksize): # """Returns a list of blocks (3 sized tuples). # # image: A PIL image to base the blocks on. # blocksize: The size of the blocks to be create. This is a single integer, defining # both width and height (blocks are square). # """ # if min(image.size) < blocksize: # return () # result = [] # for i in xrange(image.size[1] // blocksize): # for j in xrange(image.size[0] // blocksize): # box = (blocksize * j, blocksize * i, blocksize * (j + 1), blocksize * (i + 1)) # crop = image.crop(box) # result.append(getblock(crop)) # return result # Converted to C # def getblocks2(image,block_count_per_side): # """Returns a list of blocks (3 sized tuples). # # image: A PIL image to base the blocks on. # block_count_per_side: This integer determine the number of blocks the function will return. # If it is 10, for example, 100 blocks will be returns (10 width, 10 height). The blocks will not # necessarely cover square areas. The area covered by each block will be proportional to the image # itself. # """ # if not image.size[0]: # return [] # width,height = image.size # block_width = max(width // block_count_per_side,1) # block_height = max(height // block_count_per_side,1) # result = [] # for ih in range(block_count_per_side): # top = min(ih * block_height, height - block_height) # bottom = top + block_height # for iw in range(block_count_per_side): # left = min(iw * block_width, width - block_width) # right = left + block_width # box = (left,top,right,bottom) # crop = image.crop(box) # result.append(getblock(crop)) # return result # Converted to C # def diff(first, second): # """Returns the difference between the first block and the second. # # It returns an absolute sum of the 3 differences (RGB). # """ # r1, g1, b1 = first # r2, g2, b2 = second # return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2) # Converted to C # def avgdiff(first, second, limit=768, min_iterations=1): # """Returns the average diff between first blocks and seconds. # # If the result surpasses limit, limit + 1 is returned, except if less than min_iterations # iterations have been made in the blocks. # """ # if len(first) != len(second): # raise DifferentBlockCountError # if not first: # raise NoBlocksError # count = len(first) # sum = 0 # zipped = izip(xrange(1, count + 1), first, second) # for i, first, second in zipped: # sum += diff(first, second) # if sum > limit * i and i >= min_iterations: # return limit + 1 # result = sum // count # if (not result) and sum: # result = 1 # return result # This is not used anymore # def maxdiff(first,second,limit=768): # """Returns the max diff between first blocks and seconds. # # If the result surpasses limit, the first max being over limit is returned. # """ # if len(first) != len(second): # raise DifferentBlockCountError # if not first: # raise NoBlocksError # result = 0 # zipped = zip(first,second) # for first,second in zipped: # result = max(result,diff(first,second)) # if result > limit: # return result # return result dupeguru-4.3.1/core/pe/block.pyi000066400000000000000000000010611426171743600165410ustar00rootroot00000000000000from typing import Tuple, List, Union, Sequence _block = Tuple[int, int, int] class NoBlocksError(Exception): ... # noqa: E302, E701 class DifferentBlockCountError(Exception): ... # noqa E701 def getblock(image: object) -> Union[_block, None]: ... # noqa: E302 def getblocks2(image: object, block_count_per_side: int) -> Union[List[_block], None]: ... def diff(first: _block, second: _block) -> int: ... def avgdiff( # noqa: E302 first: Sequence[_block], second: Sequence[_block], limit: int = 768, min_iterations: int = 1 ) -> Union[int, None]: ... dupeguru-4.3.1/core/pe/cache.py000066400000000000000000000016371426171743600163520ustar00rootroot00000000000000# Copyright 2016 Virgil Dupras # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from core.pe._cache import string_to_colors # noqa def colors_to_string(colors): """Transform the 3 sized tuples 'colors' into a hex string. [(0,100,255)] --> 0064ff [(1,2,3),(4,5,6)] --> 010203040506 """ return "".join("{:02x}{:02x}{:02x}".format(r, g, b) for r, g, b in colors) # This function is an important bottleneck of dupeGuru PE. It has been converted to C. # def string_to_colors(s): # """Transform the string 's' in a list of 3 sized tuples. # """ # result = [] # for i in xrange(0, len(s), 6): # number = int(s[i:i+6], 16) # result.append((number >> 16, (number >> 8) & 0xff, number & 0xff)) # return result dupeguru-4.3.1/core/pe/cache.pyi000066400000000000000000000003121426171743600165100ustar00rootroot00000000000000from typing import Union, Tuple, List _block = Tuple[int, int, int] def colors_to_string(colors: List[_block]) -> str: ... # noqa: E302 def string_to_colors(s: str) -> Union[List[_block], None]: ... dupeguru-4.3.1/core/pe/cache_shelve.py000066400000000000000000000100711426171743600177100ustar00rootroot00000000000000# Copyright 2016 Virgil Dupras # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import os import os.path as op import shelve import tempfile from collections import namedtuple from core.pe.cache import string_to_colors, colors_to_string def wrap_path(path): return f"path:{path}" def unwrap_path(key): return key[5:] def wrap_id(path): return f"id:{path}" def unwrap_id(key): return int(key[3:]) CacheRow = namedtuple("CacheRow", "id path blocks mtime") class ShelveCache: """A class to cache picture blocks in a shelve backend.""" def __init__(self, db=None, readonly=False): self.istmp = db is None if self.istmp: self.dtmp = tempfile.mkdtemp() self.ftmp = db = op.join(self.dtmp, "tmpdb") flag = "r" if readonly else "c" self.shelve = shelve.open(db, flag) self.maxid = self._compute_maxid() def __contains__(self, key): return wrap_path(key) in self.shelve def __delitem__(self, key): row = self.shelve[wrap_path(key)] del self.shelve[wrap_path(key)] del self.shelve[wrap_id(row.id)] def __getitem__(self, key): if isinstance(key, int): skey = self.shelve[wrap_id(key)] else: skey = wrap_path(key) return string_to_colors(self.shelve[skey].blocks) def __iter__(self): return (unwrap_path(k) for k in self.shelve if k.startswith("path:")) def __len__(self): return sum(1 for k in self.shelve if k.startswith("path:")) def __setitem__(self, path_str, blocks): blocks = colors_to_string(blocks) if op.exists(path_str): mtime = int(os.stat(path_str).st_mtime) else: mtime = 0 if path_str in self: rowid = self.shelve[wrap_path(path_str)].id else: rowid = self._get_new_id() row = CacheRow(rowid, path_str, blocks, mtime) self.shelve[wrap_path(path_str)] = row self.shelve[wrap_id(rowid)] = wrap_path(path_str) def _compute_maxid(self): return max((unwrap_id(k) for k in self.shelve if k.startswith("id:")), default=1) def _get_new_id(self): self.maxid += 1 return self.maxid def clear(self): self.shelve.clear() def close(self): if self.shelve is not None: self.shelve.close() if self.istmp: os.remove(self.ftmp) os.rmdir(self.dtmp) self.shelve = None def filter(self, func): to_delete = [key for key in self if not func(key)] for key in to_delete: del self[key] def get_id(self, path): if path in self: return self.shelve[wrap_path(path)].id else: raise ValueError(path) def get_multiple(self, rowids): for rowid in rowids: try: skey = self.shelve[wrap_id(rowid)] except KeyError: continue yield (rowid, string_to_colors(self.shelve[skey].blocks)) def purge_outdated(self): """Go through the cache and purge outdated records. A record is outdated if the picture doesn't exist or if its mtime is greater than the one in the db. """ todelete = [] for path in self: row = self.shelve[wrap_path(path)] if row.mtime and op.exists(path): picture_mtime = os.stat(path).st_mtime if int(picture_mtime) <= row.mtime: # not outdated continue todelete.append(path) for path in todelete: try: del self[path] except KeyError: # I have no idea why a KeyError sometimes happen, but it does, as we can see in # #402 and #439. I don't think it hurts to silently ignore the error, so that's # what we do pass dupeguru-4.3.1/core/pe/cache_sqlite.py000066400000000000000000000120601426171743600177230ustar00rootroot00000000000000# Copyright 2016 Virgil Dupras # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import os import os.path as op import logging import sqlite3 as sqlite from core.pe.cache import string_to_colors, colors_to_string class SqliteCache: """A class to cache picture blocks in a sqlite backend.""" def __init__(self, db=":memory:", readonly=False): # readonly is not used in the sqlite version of the cache self.dbname = db self.con = None self._create_con() def __contains__(self, key): sql = "select count(*) from pictures where path = ?" result = self.con.execute(sql, [key]).fetchall() return result[0][0] > 0 def __delitem__(self, key): if key not in self: raise KeyError(key) sql = "delete from pictures where path = ?" self.con.execute(sql, [key]) # Optimized def __getitem__(self, key): if isinstance(key, int): sql = "select blocks from pictures where rowid = ?" else: sql = "select blocks from pictures where path = ?" result = self.con.execute(sql, [key]).fetchone() if result: result = string_to_colors(result[0]) return result else: raise KeyError(key) def __iter__(self): sql = "select path from pictures" result = self.con.execute(sql) return (row[0] for row in result) def __len__(self): sql = "select count(*) from pictures" result = self.con.execute(sql).fetchall() return result[0][0] def __setitem__(self, path_str, blocks): blocks = colors_to_string(blocks) if op.exists(path_str): mtime = int(os.stat(path_str).st_mtime) else: mtime = 0 if path_str in self: sql = "update pictures set blocks = ?, mtime = ? where path = ?" else: sql = "insert into pictures(blocks,mtime,path) values(?,?,?)" try: self.con.execute(sql, [blocks, mtime, path_str]) except sqlite.OperationalError: logging.warning("Picture cache could not set value for key %r", path_str) except sqlite.DatabaseError as e: logging.warning("DatabaseError while setting value for key %r: %s", path_str, str(e)) def _create_con(self, second_try=False): def create_tables(): logging.debug("Creating picture cache tables.") self.con.execute("drop table if exists pictures") self.con.execute("drop index if exists idx_path") self.con.execute("create table pictures(path TEXT, mtime INTEGER, blocks TEXT)") self.con.execute("create index idx_path on pictures (path)") self.con = sqlite.connect(self.dbname, isolation_level=None) try: self.con.execute("select path, mtime, blocks from pictures where 1=2") except sqlite.OperationalError: # new db create_tables() except sqlite.DatabaseError as e: # corrupted db if second_try: raise # Something really strange is happening logging.warning("Could not create picture cache because of an error: %s", str(e)) self.con.close() os.remove(self.dbname) self._create_con(second_try=True) def clear(self): self.close() if self.dbname != ":memory:": os.remove(self.dbname) self._create_con() def close(self): if self.con is not None: self.con.close() self.con = None def filter(self, func): to_delete = [key for key in self if not func(key)] for key in to_delete: del self[key] def get_id(self, path): sql = "select rowid from pictures where path = ?" result = self.con.execute(sql, [path]).fetchone() if result: return result[0] else: raise ValueError(path) def get_multiple(self, rowids): sql = "select rowid, blocks from pictures where rowid in (%s)" % ",".join(map(str, rowids)) cur = self.con.execute(sql) return ((rowid, string_to_colors(blocks)) for rowid, blocks in cur) def purge_outdated(self): """Go through the cache and purge outdated records. A record is outdated if the picture doesn't exist or if its mtime is greater than the one in the db. """ todelete = [] sql = "select rowid, path, mtime from pictures" cur = self.con.execute(sql) for rowid, path_str, mtime in cur: if mtime and op.exists(path_str): picture_mtime = os.stat(path_str).st_mtime if int(picture_mtime) <= mtime: # not outdated continue todelete.append(rowid) if todelete: sql = "delete from pictures where rowid in (%s)" % ",".join(map(str, todelete)) self.con.execute(sql) dupeguru-4.3.1/core/pe/exif.py000066400000000000000000000252501426171743600162370ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2011-04-20 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html # Heavily based on http://topo.math.u-psud.fr/~bousch/exifdump.py by Thierry Bousch (Public Domain) import logging EXIF_TAGS = { 0x0100: "ImageWidth", 0x0101: "ImageLength", 0x0102: "BitsPerSample", 0x0103: "Compression", 0x0106: "PhotometricInterpretation", 0x010A: "FillOrder", 0x010D: "DocumentName", 0x010E: "ImageDescription", 0x010F: "Make", 0x0110: "Model", 0x0111: "StripOffsets", 0x0112: "Orientation", 0x0115: "SamplesPerPixel", 0x0116: "RowsPerStrip", 0x0117: "StripByteCounts", 0x011A: "XResolution", 0x011B: "YResolution", 0x011C: "PlanarConfiguration", 0x0128: "ResolutionUnit", 0x012D: "TransferFunction", 0x0131: "Software", 0x0132: "DateTime", 0x013B: "Artist", 0x013E: "WhitePoint", 0x013F: "PrimaryChromaticities", 0x0156: "TransferRange", 0x0200: "JPEGProc", 0x0201: "JPEGInterchangeFormat", 0x0202: "JPEGInterchangeFormatLength", 0x0211: "YCbCrCoefficients", 0x0212: "YCbCrSubSampling", 0x0213: "YCbCrPositioning", 0x0214: "ReferenceBlackWhite", 0x828F: "BatteryLevel", 0x8298: "Copyright", 0x829A: "ExposureTime", 0x829D: "FNumber", 0x83BB: "IPTC/NAA", 0x8769: "ExifIFDPointer", 0x8773: "InterColorProfile", 0x8822: "ExposureProgram", 0x8824: "SpectralSensitivity", 0x8825: "GPSInfoIFDPointer", 0x8827: "ISOSpeedRatings", 0x8828: "OECF", 0x9000: "ExifVersion", 0x9003: "DateTimeOriginal", 0x9004: "DateTimeDigitized", 0x9101: "ComponentsConfiguration", 0x9102: "CompressedBitsPerPixel", 0x9201: "ShutterSpeedValue", 0x9202: "ApertureValue", 0x9203: "BrightnessValue", 0x9204: "ExposureBiasValue", 0x9205: "MaxApertureValue", 0x9206: "SubjectDistance", 0x9207: "MeteringMode", 0x9208: "LightSource", 0x9209: "Flash", 0x920A: "FocalLength", 0x9214: "SubjectArea", 0x927C: "MakerNote", 0x9286: "UserComment", 0x9290: "SubSecTime", 0x9291: "SubSecTimeOriginal", 0x9292: "SubSecTimeDigitized", 0xA000: "FlashPixVersion", 0xA001: "ColorSpace", 0xA002: "PixelXDimension", 0xA003: "PixelYDimension", 0xA004: "RelatedSoundFile", 0xA005: "InteroperabilityIFDPointer", 0xA20B: "FlashEnergy", # 0x920B in TIFF/EP 0xA20C: "SpatialFrequencyResponse", # 0x920C - - 0xA20E: "FocalPlaneXResolution", # 0x920E - - 0xA20F: "FocalPlaneYResolution", # 0x920F - - 0xA210: "FocalPlaneResolutionUnit", # 0x9210 - - 0xA214: "SubjectLocation", # 0x9214 - - 0xA215: "ExposureIndex", # 0x9215 - - 0xA217: "SensingMethod", # 0x9217 - - 0xA300: "FileSource", 0xA301: "SceneType", 0xA302: "CFAPattern", # 0x828E in TIFF/EP 0xA401: "CustomRendered", 0xA402: "ExposureMode", 0xA403: "WhiteBalance", 0xA404: "DigitalZoomRatio", 0xA405: "FocalLengthIn35mmFilm", 0xA406: "SceneCaptureType", 0xA407: "GainControl", 0xA408: "Contrast", 0xA409: "Saturation", 0xA40A: "Sharpness", 0xA40B: "DeviceSettingDescription", 0xA40C: "SubjectDistanceRange", 0xA420: "ImageUniqueID", } INTR_TAGS = { 0x0001: "InteroperabilityIndex", 0x0002: "InteroperabilityVersion", 0x1000: "RelatedImageFileFormat", 0x1001: "RelatedImageWidth", 0x1002: "RelatedImageLength", } GPS_TA0GS = { 0x00: "GPSVersionID", 0x01: "GPSLatitudeRef", 0x02: "GPSLatitude", 0x03: "GPSLongitudeRef", 0x04: "GPSLongitude", 0x05: "GPSAltitudeRef", 0x06: "GPSAltitude", 0x07: "GPSTimeStamp", 0x08: "GPSSatellites", 0x09: "GPSStatus", 0x0A: "GPSMeasureMode", 0x0B: "GPSDOP", 0x0C: "GPSSpeedRef", 0x0D: "GPSSpeed", 0x0E: "GPSTrackRef", 0x0F: "GPSTrack", 0x10: "GPSImgDirectionRef", 0x11: "GPSImgDirection", 0x12: "GPSMapDatum", 0x13: "GPSDestLatitudeRef", 0x14: "GPSDestLatitude", 0x15: "GPSDestLongitudeRef", 0x16: "GPSDestLongitude", 0x17: "GPSDestBearingRef", 0x18: "GPSDestBearing", 0x19: "GPSDestDistanceRef", 0x1A: "GPSDestDistance", 0x1B: "GPSProcessingMethod", 0x1C: "GPSAreaInformation", 0x1D: "GPSDateStamp", 0x1E: "GPSDifferential", } INTEL_ENDIAN = ord("I") MOTOROLA_ENDIAN = ord("M") # About MAX_COUNT: It's possible to have corrupted exif tags where the entry count is way too high # and thus makes us loop, not endlessly, but for heck of a long time for nothing. Therefore, we put # an arbitrary limit on the entry count we'll allow ourselves to read and any IFD reporting more # entries than that will be considered corrupt. MAX_COUNT = 0xFFFF def s2n_motorola(bytes): x = 0 for c in bytes: x = (x << 8) | c return x def s2n_intel(bytes): x = 0 y = 0 for c in bytes: x = x | (c << y) y = y + 8 return x class Fraction: def __init__(self, num, den): self.num = num self.den = den def __repr__(self): return "%d/%d" % (self.num, self.den) class TIFF_file: def __init__(self, data): self.data = data self.endian = data[0] self.s2nfunc = s2n_intel if self.endian == INTEL_ENDIAN else s2n_motorola def s2n(self, offset, length, signed=0, debug=False): data_slice = self.data[offset : offset + length] val = self.s2nfunc(data_slice) # Sign extension ? if signed: msb = 1 << (8 * length - 1) if val & msb: val = val - (msb << 1) if debug: logging.debug(self.endian) logging.debug( "Slice for offset %d length %d: %r and value: %d", offset, length, data_slice, val, ) return val def first_IFD(self): return self.s2n(4, 4) def next_IFD(self, ifd): entries = self.s2n(ifd, 2) return self.s2n(ifd + 2 + 12 * entries, 4) def list_IFDs(self): i = self.first_IFD() a = [] while i: a.append(i) i = self.next_IFD(i) return a def dump_IFD(self, ifd): entries = self.s2n(ifd, 2) logging.debug("Entries for IFD %d: %d", ifd, entries) if entries > MAX_COUNT: logging.debug("Probably corrupt. Aborting.") return [] a = [] for i in range(entries): entry = ifd + 2 + 12 * i tag = self.s2n(entry, 2) entry_type = self.s2n(entry + 2, 2) if not 1 <= entry_type <= 10: continue # not handled typelen = [1, 1, 2, 4, 8, 1, 1, 2, 4, 8][entry_type - 1] count = self.s2n(entry + 4, 4) if count > MAX_COUNT: logging.debug("Probably corrupt. Aborting.") return [] offset = entry + 8 if count * typelen > 4: offset = self.s2n(offset, 4) if entry_type == 2: # Special case: nul-terminated ASCII string values = str(self.data[offset : offset + count - 1], encoding="latin-1") else: values = [] signed = entry_type == 6 or entry_type >= 8 for _ in range(count): if entry_type in {5, 10}: # The type is either 5 or 10 value_j = Fraction(self.s2n(offset, 4, signed), self.s2n(offset + 4, 4, signed)) else: # Not a fraction value_j = self.s2n(offset, typelen, signed) values.append(value_j) offset = offset + typelen # Now "values" is either a string or an array a.append((tag, entry_type, values)) return a def read_exif_header(fp): # If `fp`'s first bytes are not exif, it tries to find it in the next 4kb def isexif(data): return data[0:4] == b"\377\330\377\341" and data[6:10] == b"Exif" data = fp.read(12) if isexif(data): return data # ok, not exif, try to find it large_data = fp.read(4096) try: index = large_data.index(b"Exif") data = large_data[index - 6 : index + 6] # large_data omits the first 12 bytes, and the index is at the middle of the header, so we # must seek index + 18 fp.seek(index + 18) return data except ValueError: raise ValueError("Not an Exif file") def get_fields(fp): data = read_exif_header(fp) length = data[4] * 256 + data[5] logging.debug("Exif header length: %d bytes", length) data = fp.read(length - 8) data_format = data[0] logging.debug("%s format", {INTEL_ENDIAN: "Intel", MOTOROLA_ENDIAN: "Motorola"}[data_format]) T = TIFF_file(data) # There may be more than one IFD per file, but we only read the first one because others are # most likely thumbnails. main_ifd_offset = T.first_IFD() result = {} def add_tag_to_result(tag, values): try: stag = EXIF_TAGS[tag] except KeyError: stag = "0x%04X" % tag if stag in result: return # don't overwrite data result[stag] = values logging.debug("IFD at offset %d", main_ifd_offset) IFD = T.dump_IFD(main_ifd_offset) exif_off = gps_off = 0 for tag, type, values in IFD: if tag == 0x8769: exif_off = values[0] continue if tag == 0x8825: gps_off = values[0] continue add_tag_to_result(tag, values) if exif_off: logging.debug("Exif SubIFD at offset %d:", exif_off) IFD = T.dump_IFD(exif_off) # Recent digital cameras have a little subdirectory # here, pointed to by tag 0xA005. Apparently, it's the # "Interoperability IFD", defined in Exif 2.1 and DCF. intr_off = 0 for tag, type, values in IFD: if tag == 0xA005: intr_off = values[0] continue add_tag_to_result(tag, values) if intr_off: logging.debug("Exif Interoperability SubSubIFD at offset %d:", intr_off) IFD = T.dump_IFD(intr_off) for tag, type, values in IFD: add_tag_to_result(tag, values) if gps_off: logging.debug("GPS SubIFD at offset %d:", gps_off) IFD = T.dump_IFD(gps_off) for tag, type, values in IFD: add_tag_to_result(tag, values) return result dupeguru-4.3.1/core/pe/matchblock.py000066400000000000000000000254351426171743600174200ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2007/02/25 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import logging import multiprocessing from itertools import combinations from hscommon.util import extract, iterconsume from hscommon.trans import tr from hscommon.jobprogress import job from core.engine import Match from core.pe.block import avgdiff, DifferentBlockCountError, NoBlocksError # OPTIMIZATION NOTES: # The bottleneck of the matching phase is CPU, which is why we use multiprocessing. However, another # bottleneck that shows up when a lot of pictures are involved is Disk IO's because blocks # constantly have to be read from disks by subprocesses. This problem is especially big on CPUs # with a lot of cores. Therefore, we must minimize Disk IOs. The best way to achieve that is to # separate the files to scan in "chunks" and it's by chunk that blocks are read in memory and # compared to each other. Each file in a chunk has to be compared to each other, of course, but also # to files in other chunks. So chunkifying doesn't save us any actual comparison, but the advantage # is that instead of reading blocks from disk number_of_files**2 times, we read it # number_of_files*number_of_chunks times. # Determining the right chunk size is tricky, bceause if it's too big, too many blocks will be in # memory at the same time and we might end up with memory trashing, which is awfully slow. So, # because our *real* bottleneck is CPU, the chunk size must simply be enough so that the CPU isn't # starved by Disk IOs. MIN_ITERATIONS = 3 BLOCK_COUNT_PER_SIDE = 15 DEFAULT_CHUNK_SIZE = 1000 MIN_CHUNK_SIZE = 100 # Enough so that we're sure that the main thread will not wait after a result.get() call # cpucount+1 should be enough to be sure that the spawned process will not wait after the results # collection made by the main process. try: RESULTS_QUEUE_LIMIT = multiprocessing.cpu_count() + 1 except Exception: # I had an IOError on app launch once. It seems to be a freak occurrence. In any case, we want # the app to launch, so let's just put an arbitrary value. logging.warning("Had problems to determine cpu count on launch.") RESULTS_QUEUE_LIMIT = 8 def get_cache(cache_path, readonly=False): if cache_path.endswith("shelve"): from core.pe.cache_shelve import ShelveCache return ShelveCache(cache_path, readonly=readonly) else: from core.pe.cache_sqlite import SqliteCache return SqliteCache(cache_path, readonly=readonly) def prepare_pictures(pictures, cache_path, with_dimensions, j=job.nulljob): # The MemoryError handlers in there use logging without first caring about whether or not # there is enough memory left to carry on the operation because it is assumed that the # MemoryError happens when trying to read an image file, which is freed from memory by the # time that MemoryError is raised. cache = get_cache(cache_path) cache.purge_outdated() prepared = [] # only pictures for which there was no error getting blocks try: for picture in j.iter_with_progress(pictures, tr("Analyzed %d/%d pictures")): if not picture.path: # XXX Find the root cause of this. I've received reports of crashes where we had # "Analyzing picture at " (without a path) in the debug log. It was an iPhoto scan. # For now, I'm simply working around the crash by ignoring those, but it would be # interesting to know exactly why this happens. I'm suspecting a malformed # entry in iPhoto library. logging.warning("We have a picture with a null path here") continue picture.unicode_path = str(picture.path) logging.debug("Analyzing picture at %s", picture.unicode_path) if with_dimensions: picture.dimensions # pre-read dimensions try: if picture.unicode_path not in cache: blocks = picture.get_blocks(BLOCK_COUNT_PER_SIDE) cache[picture.unicode_path] = blocks prepared.append(picture) except (OSError, ValueError) as e: logging.warning(str(e)) except MemoryError: logging.warning( "Ran out of memory while reading %s of size %d", picture.unicode_path, picture.size, ) if picture.size < 10 * 1024 * 1024: # We're really running out of memory raise except MemoryError: logging.warning("Ran out of memory while preparing pictures") cache.close() return prepared def get_chunks(pictures): min_chunk_count = multiprocessing.cpu_count() * 2 # have enough chunks to feed all subprocesses chunk_count = len(pictures) // DEFAULT_CHUNK_SIZE chunk_count = max(min_chunk_count, chunk_count) chunk_size = (len(pictures) // chunk_count) + 1 chunk_size = max(MIN_CHUNK_SIZE, chunk_size) logging.info( "Creating %d chunks with a chunk size of %d for %d pictures", chunk_count, chunk_size, len(pictures), ) chunks = [pictures[i : i + chunk_size] for i in range(0, len(pictures), chunk_size)] return chunks def get_match(first, second, percentage): if percentage < 0: percentage = 0 return Match(first, second, percentage) def async_compare(ref_ids, other_ids, dbname, threshold, picinfo): # The list of ids in ref_ids have to be compared to the list of ids in other_ids. other_ids # can be None. In this case, ref_ids has to be compared with itself # picinfo is a dictionary {pic_id: (dimensions, is_ref)} cache = get_cache(dbname, readonly=True) limit = 100 - threshold ref_pairs = list(cache.get_multiple(ref_ids)) if other_ids is not None: other_pairs = list(cache.get_multiple(other_ids)) comparisons_to_do = [(r, o) for r in ref_pairs for o in other_pairs] else: comparisons_to_do = list(combinations(ref_pairs, 2)) results = [] for (ref_id, ref_blocks), (other_id, other_blocks) in comparisons_to_do: ref_dimensions, ref_is_ref = picinfo[ref_id] other_dimensions, other_is_ref = picinfo[other_id] if ref_is_ref and other_is_ref: continue if ref_dimensions != other_dimensions: continue try: diff = avgdiff(ref_blocks, other_blocks, limit, MIN_ITERATIONS) percentage = 100 - diff except (DifferentBlockCountError, NoBlocksError): percentage = 0 if percentage >= threshold: results.append((ref_id, other_id, percentage)) cache.close() return results def getmatches(pictures, cache_path, threshold, match_scaled=False, j=job.nulljob): def get_picinfo(p): if match_scaled: return (None, p.is_ref) else: return (p.dimensions, p.is_ref) def collect_results(collect_all=False): # collect results and wait until the queue is small enough to accomodate a new results. nonlocal async_results, matches, comparison_count, comparisons_to_do limit = 0 if collect_all else RESULTS_QUEUE_LIMIT while len(async_results) > limit: ready, working = extract(lambda r: r.ready(), async_results) for result in ready: matches += result.get() async_results.remove(result) comparison_count += 1 # About the NOQA below: I think there's a bug in pyflakes. To investigate... progress_msg = tr("Performed %d/%d chunk matches") % ( comparison_count, len(comparisons_to_do), ) # NOQA j.set_progress(comparison_count, progress_msg) j = j.start_subjob([3, 7]) pictures = prepare_pictures(pictures, cache_path, with_dimensions=not match_scaled, j=j) j = j.start_subjob([9, 1], tr("Preparing for matching")) cache = get_cache(cache_path) id2picture = {} for picture in pictures: try: picture.cache_id = cache.get_id(picture.unicode_path) id2picture[picture.cache_id] = picture except ValueError: pass cache.close() pictures = [p for p in pictures if hasattr(p, "cache_id")] pool = multiprocessing.Pool() async_results = [] matches = [] chunks = get_chunks(pictures) # We add a None element at the end of the chunk list because each chunk has to be compared # with itself. Thus, each chunk will show up as a ref_chunk having other_chunk set to None once. comparisons_to_do = list(combinations(chunks + [None], 2)) comparison_count = 0 j.start_job(len(comparisons_to_do)) try: for ref_chunk, other_chunk in comparisons_to_do: picinfo = {p.cache_id: get_picinfo(p) for p in ref_chunk} ref_ids = [p.cache_id for p in ref_chunk] if other_chunk is not None: other_ids = [p.cache_id for p in other_chunk] picinfo.update({p.cache_id: get_picinfo(p) for p in other_chunk}) else: other_ids = None args = (ref_ids, other_ids, cache_path, threshold, picinfo) async_results.append(pool.apply_async(async_compare, args)) collect_results() collect_results(collect_all=True) except MemoryError: # Rare, but possible, even in 64bit situations (ref #264). What do we do now? We free us # some wiggle room, log about the incident, and stop matching right here. We then process # the matches we have. The rest of the process doesn't allocate much and we should be # alright. del ( comparisons_to_do, chunks, pictures, ) # some wiggle room for the next statements logging.warning("Ran out of memory when scanning! We had %d matches.", len(matches)) del matches[-len(matches) // 3 :] # some wiggle room to ensure we don't run out of memory again. pool.close() result = [] myiter = j.iter_with_progress( iterconsume(matches, reverse=False), tr("Verified %d/%d matches"), every=10, count=len(matches), ) for ref_id, other_id, percentage in myiter: ref = id2picture[ref_id] other = id2picture[other_id] if percentage == 100 and ref.digest != other.digest: percentage = 99 if percentage >= threshold: ref.dimensions # pre-read dimensions for display in results other.dimensions result.append(get_match(ref, other, percentage)) pool.join() return result multiprocessing.freeze_support() dupeguru-4.3.1/core/pe/matchexif.py000066400000000000000000000021621426171743600172510ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2011-04-20 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from collections import defaultdict from itertools import combinations from hscommon.trans import tr from core.engine import Match def getmatches(files, match_scaled, j): timestamp2pic = defaultdict(set) for picture in j.iter_with_progress(files, tr("Read EXIF of %d/%d pictures")): timestamp = picture.exif_timestamp if timestamp: timestamp2pic[timestamp].add(picture) if "0000:00:00 00:00:00" in timestamp2pic: # very likely false matches del timestamp2pic["0000:00:00 00:00:00"] matches = [] for pictures in timestamp2pic.values(): for p1, p2 in combinations(pictures, 2): if (not match_scaled) and (p1.dimensions != p2.dimensions): continue matches.append(Match(p1, p2, 100)) return matches dupeguru-4.3.1/core/pe/modules/000077500000000000000000000000001426171743600163765ustar00rootroot00000000000000dupeguru-4.3.1/core/pe/modules/block.c000066400000000000000000000156441426171743600176460ustar00rootroot00000000000000/* Created By: Virgil Dupras * Created On: 2010-01-30 * Copyright 2014 Hardcoded Software (http://www.hardcoded.net) * * This software is licensed under the "BSD" License as described in the * "LICENSE" file, which should be included with this package. The terms are * also available at http://www.hardcoded.net/licenses/bsd_license */ #include "common.h" /* avgdiff/maxdiff has been called with empty lists */ static PyObject *NoBlocksError; /* avgdiff/maxdiff has been called with 2 block lists of different size. */ static PyObject *DifferentBlockCountError; /* Returns a 3 sized tuple containing the mean color of 'image'. * image: a PIL image or crop. */ static PyObject *getblock(PyObject *image) { int i, totr, totg, totb; Py_ssize_t pixel_count; PyObject *ppixels; totr = totg = totb = 0; ppixels = PyObject_CallMethod(image, "getdata", NULL); if (ppixels == NULL) { return NULL; } pixel_count = PySequence_Length(ppixels); for (i = 0; i < pixel_count; i++) { PyObject *ppixel, *pr, *pg, *pb; int r, g, b; ppixel = PySequence_ITEM(ppixels, i); pr = PySequence_ITEM(ppixel, 0); pg = PySequence_ITEM(ppixel, 1); pb = PySequence_ITEM(ppixel, 2); Py_DECREF(ppixel); r = PyLong_AsLong(pr); g = PyLong_AsLong(pg); b = PyLong_AsLong(pb); Py_DECREF(pr); Py_DECREF(pg); Py_DECREF(pb); totr += r; totg += g; totb += b; } Py_DECREF(ppixels); if (pixel_count) { totr /= pixel_count; totg /= pixel_count; totb /= pixel_count; } return inttuple(3, totr, totg, totb); } /* Returns the difference between the first block and the second. * It returns an absolute sum of the 3 differences (RGB). */ static int diff(PyObject *first, PyObject *second) { int r1, g1, b1, r2, b2, g2; PyObject *pr, *pg, *pb; pr = PySequence_ITEM(first, 0); pg = PySequence_ITEM(first, 1); pb = PySequence_ITEM(first, 2); r1 = PyLong_AsLong(pr); g1 = PyLong_AsLong(pg); b1 = PyLong_AsLong(pb); Py_DECREF(pr); Py_DECREF(pg); Py_DECREF(pb); pr = PySequence_ITEM(second, 0); pg = PySequence_ITEM(second, 1); pb = PySequence_ITEM(second, 2); r2 = PyLong_AsLong(pr); g2 = PyLong_AsLong(pg); b2 = PyLong_AsLong(pb); Py_DECREF(pr); Py_DECREF(pg); Py_DECREF(pb); return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2); } PyDoc_STRVAR(block_getblocks2_doc, "Returns a list of blocks (3 sized tuples).\n\ \n\ image: A PIL image to base the blocks on.\n\ block_count_per_side: This integer determine the number of blocks the function will return.\n\ If it is 10, for example, 100 blocks will be returns (10 width, 10 height). The blocks will not\n\ necessarely cover square areas. The area covered by each block will be proportional to the image\n\ itself.\n"); static PyObject *block_getblocks2(PyObject *self, PyObject *args) { int block_count_per_side, width, height, block_width, block_height, ih; PyObject *image; PyObject *pimage_size, *pwidth, *pheight; PyObject *result; if (!PyArg_ParseTuple(args, "Oi", &image, &block_count_per_side)) { return NULL; } pimage_size = PyObject_GetAttrString(image, "size"); pwidth = PySequence_ITEM(pimage_size, 0); pheight = PySequence_ITEM(pimage_size, 1); width = PyLong_AsLong(pwidth); height = PyLong_AsLong(pheight); Py_DECREF(pimage_size); Py_DECREF(pwidth); Py_DECREF(pheight); if (!(width && height)) { return PyList_New(0); } block_width = max(width / block_count_per_side, 1); block_height = max(height / block_count_per_side, 1); result = PyList_New((Py_ssize_t)block_count_per_side * block_count_per_side); if (result == NULL) { return NULL; } for (ih = 0; ih < block_count_per_side; ih++) { int top, bottom, iw; top = min(ih * block_height, height - block_height); bottom = top + block_height; for (iw = 0; iw < block_count_per_side; iw++) { int left, right; PyObject *pbox; PyObject *pmethodname; PyObject *pcrop; PyObject *pblock; left = min(iw * block_width, width - block_width); right = left + block_width; pbox = inttuple(4, left, top, right, bottom); pmethodname = PyUnicode_FromString("crop"); pcrop = PyObject_CallMethodObjArgs(image, pmethodname, pbox, NULL); Py_DECREF(pmethodname); Py_DECREF(pbox); if (pcrop == NULL) { Py_DECREF(result); return NULL; } pblock = getblock(pcrop); Py_DECREF(pcrop); if (pblock == NULL) { Py_DECREF(result); return NULL; } PyList_SET_ITEM(result, ih * block_count_per_side + iw, pblock); } } return result; } PyDoc_STRVAR(block_avgdiff_doc, "Returns the average diff between first blocks and seconds.\n\ \n\ If the result surpasses limit, limit + 1 is returned, except if less than min_iterations\n\ iterations have been made in the blocks.\n"); static PyObject *block_avgdiff(PyObject *self, PyObject *args) { PyObject *first, *second; int limit, min_iterations; Py_ssize_t count; int sum, i, result; if (!PyArg_ParseTuple(args, "OOii", &first, &second, &limit, &min_iterations)) { return NULL; } count = PySequence_Length(first); if (count != PySequence_Length(second)) { PyErr_SetString(DifferentBlockCountError, ""); return NULL; } if (!count) { PyErr_SetString(NoBlocksError, ""); return NULL; } sum = 0; for (i = 0; i < count; i++) { int iteration_count; PyObject *item1, *item2; iteration_count = i + 1; item1 = PySequence_ITEM(first, i); item2 = PySequence_ITEM(second, i); sum += diff(item1, item2); Py_DECREF(item1); Py_DECREF(item2); if ((sum > limit * iteration_count) && (iteration_count >= min_iterations)) { return PyLong_FromLong(limit + 1); } } result = sum / count; if (!result && sum) { result = 1; } return PyLong_FromLong(result); } static PyMethodDef BlockMethods[] = { {"getblocks2", block_getblocks2, METH_VARARGS, block_getblocks2_doc}, {"avgdiff", block_avgdiff, METH_VARARGS, block_avgdiff_doc}, {NULL, NULL, 0, NULL} /* Sentinel */ }; static struct PyModuleDef BlockDef = {PyModuleDef_HEAD_INIT, "_block", NULL, -1, BlockMethods, NULL, NULL, NULL, NULL}; PyObject *PyInit__block(void) { PyObject *m = PyModule_Create(&BlockDef); if (m == NULL) { return NULL; } NoBlocksError = PyErr_NewException("_block.NoBlocksError", NULL, NULL); PyModule_AddObject(m, "NoBlocksError", NoBlocksError); DifferentBlockCountError = PyErr_NewException("_block.DifferentBlockCountError", NULL, NULL); PyModule_AddObject(m, "DifferentBlockCountError", DifferentBlockCountError); return m; }dupeguru-4.3.1/core/pe/modules/block_osx.m000066400000000000000000000213171426171743600205430ustar00rootroot00000000000000/* Created By: Virgil Dupras * Created On: 2010-02-04 * Copyright 2015 Hardcoded Software (http://www.hardcoded.net) * * This software is licensed under the "GPLv3" License as described in the "LICENSE" file, * which should be included with this package. The terms are also available at * http://www.gnu.org/licenses/gpl-3.0.html **/ #include "common.h" #import #import #import #define RADIANS( degrees ) ( degrees * M_PI / 180 ) static CFStringRef pystring2cfstring(PyObject *pystring) { PyObject *encoded; UInt8 *s; CFIndex size; CFStringRef result; if (PyUnicode_Check(pystring)) { encoded = PyUnicode_AsUTF8String(pystring); if (encoded == NULL) { return NULL; } } else { encoded = pystring; Py_INCREF(encoded); } s = (UInt8*)PyBytes_AS_STRING(encoded); size = PyBytes_GET_SIZE(encoded); result = CFStringCreateWithBytes(NULL, s, size, kCFStringEncodingUTF8, FALSE); Py_DECREF(encoded); return result; } static PyObject* block_osx_get_image_size(PyObject *self, PyObject *args) { PyObject *path; CFStringRef image_path; CFURLRef image_url; CGImageSourceRef source; CGImageRef image; long width, height; PyObject *pwidth, *pheight; PyObject *result; width = 0; height = 0; if (!PyArg_ParseTuple(args, "O", &path)) { return NULL; } image_path = pystring2cfstring(path); if (image_path == NULL) { return PyErr_NoMemory(); } image_url = CFURLCreateWithFileSystemPath(NULL, image_path, kCFURLPOSIXPathStyle, FALSE); CFRelease(image_path); source = CGImageSourceCreateWithURL(image_url, NULL); CFRelease(image_url); if (source != NULL) { image = CGImageSourceCreateImageAtIndex(source, 0, NULL); if (image != NULL) { width = CGImageGetWidth(image); height = CGImageGetHeight(image); CGImageRelease(image); } CFRelease(source); } pwidth = PyLong_FromLong(width); if (pwidth == NULL) { return NULL; } pheight = PyLong_FromLong(height); if (pheight == NULL) { return NULL; } result = PyTuple_Pack(2, pwidth, pheight); Py_DECREF(pwidth); Py_DECREF(pheight); return result; } static CGContextRef MyCreateBitmapContext(int width, int height) { CGContextRef context = NULL; CGColorSpaceRef colorSpace; void *bitmapData; int bitmapByteCount; int bitmapBytesPerRow; bitmapBytesPerRow = (width * 4); bitmapByteCount = (bitmapBytesPerRow * height); colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); // calloc() must be used to allocate bitmapData here because the buffer has to be zeroed. // If it's not zeroes, when images with transparency are drawn in the context, this buffer // will stay with undefined pixels, which means that two pictures with the same pixels will // most likely have different blocks (which is not supposed to happen). bitmapData = calloc(bitmapByteCount, 1); if (bitmapData == NULL) { fprintf(stderr, "Memory not allocated!"); return NULL; } context = CGBitmapContextCreate(bitmapData, width, height, 8, bitmapBytesPerRow, colorSpace, (CGBitmapInfo)kCGImageAlphaNoneSkipLast); if (context== NULL) { free(bitmapData); fprintf(stderr, "Context not created!"); return NULL; } CGColorSpaceRelease(colorSpace); return context; } static PyObject* getblock(unsigned char *imageData, int imageWidth, int imageHeight, int boxX, int boxY, int boxW, int boxH) { int i,j, totalR, totalG, totalB; totalR = totalG = totalB = 0; for(i=boxY; i 8) || (orientation < 0)) { orientation = 0; // simplifies checks later since we can only have values in 0-8 } image_path = pystring2cfstring(path); if (image_path == NULL) { return PyErr_NoMemory(); } image_url = CFURLCreateWithFileSystemPath(NULL, image_path, kCFURLPOSIXPathStyle, FALSE); CFRelease(image_path); source = CGImageSourceCreateWithURL(image_url, NULL); CFRelease(image_url); if (source == NULL) { return PyErr_NoMemory(); } image = CGImageSourceCreateImageAtIndex(source, 0, NULL); if (image == NULL) { CFRelease(source); return PyErr_NoMemory(); } width = image_width = CGImageGetWidth(image); height = image_height = CGImageGetHeight(image); if (orientation >= 5) { // orientations 5-8 rotate the photo sideways, so we have to swap width and height width = image_height; height = image_width; } CGContextRef context = MyCreateBitmapContext(width, height); if (orientation == 2) { // Flip X CGContextTranslateCTM(context, width, 0); CGContextScaleCTM(context, -1, 1); } else if (orientation == 3) { // Rot 180 CGContextTranslateCTM(context, width, height); CGContextRotateCTM(context, RADIANS(180)); } else if (orientation == 4) { // Flip Y CGContextTranslateCTM(context, 0, height); CGContextScaleCTM(context, 1, -1); } else if (orientation == 5) { // Flip X + Rot CW 90 CGContextTranslateCTM(context, width, 0); CGContextScaleCTM(context, -1, 1); CGContextTranslateCTM(context, 0, height); CGContextRotateCTM(context, RADIANS(-90)); } else if (orientation == 6) { // Rot CW 90 CGContextTranslateCTM(context, 0, height); CGContextRotateCTM(context, RADIANS(-90)); } else if (orientation == 7) { // Rot CCW 90 + Flip X CGContextTranslateCTM(context, width, 0); CGContextScaleCTM(context, -1, 1); CGContextTranslateCTM(context, width, 0); CGContextRotateCTM(context, RADIANS(90)); } else if (orientation == 8) { // Rot CCW 90 CGContextTranslateCTM(context, width, 0); CGContextRotateCTM(context, RADIANS(90)); } CGRect myBoundingBox = CGRectMake(0, 0, image_width, image_height); CGContextDrawImage(context, myBoundingBox, image); unsigned char *bitmapData = CGBitmapContextGetData(context); CGContextRelease(context); CGImageRelease(image); CFRelease(source); if (bitmapData == NULL) { return PyErr_NoMemory(); } block_width = max(width/block_count, 1); block_height = max(height/block_count, 1); result = PyList_New(block_count * block_count); if (result == NULL) { return NULL; } for(i=0; i= 48) && (c <= 57)) { /* 0-9 */ return c - 48; } else if ((c >= 65) && (c <= 70)) { /* A-F */ return c - 55; } else if ((c >= 97) && (c <= 102)) { /* a-f */ return c - 87; } return 0; } static PyObject* cache_string_to_colors(PyObject *self, PyObject *args) { char *s; Py_ssize_t char_count, color_count, i; PyObject *result; if (!PyArg_ParseTuple(args, "s#", &s, &char_count)) { return NULL; } color_count = (char_count / 6); result = PyList_New(color_count); if (result == NULL) { return NULL; } for (i=0; i a ? b : a; } int min(int a, int b) { return b < a ? b : a; } #endif PyObject* inttuple(int n, ...) { int i; PyObject *pnumber; PyObject *result; va_list numbers; va_start(numbers, n); result = PyTuple_New(n); for (i=0; i ".join(self.__filters) return result def __recalculate_stats(self): self.__total_size = 0 self.__total_count = 0 for group in self.groups: markable = [dupe for dupe in group.dupes if self._is_markable(dupe)] self.__total_count += len(markable) self.__total_size += sum(dupe.size for dupe in markable) def __set_groups(self, new_groups): self.mark_none() self.__groups = new_groups self.__group_of_duplicate = {} for g in self.__groups: for dupe in g: self.__group_of_duplicate[dupe] = g if not hasattr(dupe, "is_ref"): dupe.is_ref = False self.is_modified = bool(self.__groups) old_filters = nonone(self.__filters, []) self.apply_filter(None) for filter_str in old_filters: self.apply_filter(filter_str) # ---Public def apply_filter(self, filter_str): """Applies a filter ``filter_str`` to :attr:`groups` When you apply the filter, only dupes with the filename matching ``filter_str`` will be in in the results. To cancel the filter, just call apply_filter with ``filter_str`` to None, and the results will go back to normal. If call apply_filter on a filtered results, the filter will be applied *on the filtered results*. :param str filter_str: a string containing a regexp to filter dupes with. """ if not filter_str: self.__filtered_dupes = None self.__filtered_groups = None self.__filters = None else: if not self.__filters: self.__filters = [] try: filter_re = re.compile(filter_str, re.IGNORECASE) except re.error: return # don't apply this filter. self.__filters.append(filter_str) if self.__filtered_dupes is None: self.__filtered_dupes = flatten(g[:] for g in self.groups) self.__filtered_dupes = {dupe for dupe in self.__filtered_dupes if filter_re.search(str(dupe.path))} filtered_groups = set() for dupe in self.__filtered_dupes: filtered_groups.add(self.get_group_of_duplicate(dupe)) self.__filtered_groups = list(filtered_groups) self.__recalculate_stats() sd = self.__groups_sort_descriptor if sd: self.sort_groups(sd[0], sd[1]) self.__dupes = None def get_group_of_duplicate(self, dupe): """Returns :class:`~core.engine.Group` in which ``dupe`` belongs.""" try: return self.__group_of_duplicate[dupe] except (TypeError, KeyError): return None is_markable = _is_markable def load_from_xml(self, infile, get_file, j=nulljob): """Load results from ``infile``. :param infile: a file or path pointing to an XML file created with :meth:`save_to_xml`. :param get_file: a function f(path) returning a :class:`~core.fs.File` wrapping the path. :param j: A :ref:`job progress instance `. """ def do_match(ref_file, other_files, group): if not other_files: return for other_file in other_files: group.add_match(engine.get_match(ref_file, other_file)) do_match(other_files[0], other_files[1:], group) self.apply_filter(None) root = ET.parse(infile).getroot() group_elems = list(root.iter("group")) groups = [] marked = set() for group_elem in j.iter_with_progress(group_elems, every=100): group = engine.Group() dupes = [] for file_elem in group_elem.iter("file"): path = file_elem.get("path") words = file_elem.get("words", "") if not path: continue file = get_file(path) if file is None: continue file.words = words.split(",") file.is_ref = file_elem.get("is_ref") == "y" dupes.append(file) if file_elem.get("marked") == "y": marked.add(file) for match_elem in group_elem.iter("match"): try: attrs = match_elem.attrib first_file = dupes[int(attrs["first"])] second_file = dupes[int(attrs["second"])] percentage = int(attrs["percentage"]) group.add_match(engine.Match(first_file, second_file, percentage)) except (IndexError, KeyError, ValueError): # Covers missing attr, non-int values and indexes out of bounds pass if (not group.matches) and (len(dupes) >= 2): do_match(dupes[0], dupes[1:], group) group.prioritize(lambda x: dupes.index(x)) if len(group): groups.append(group) j.add_progress() self.groups = groups for dupe_file in marked: self.mark(dupe_file) self.is_modified = False def make_ref(self, dupe): """Make ``dupe`` take the :attr:`~core.engine.Group.ref` position of its group.""" g = self.get_group_of_duplicate(dupe) r = g.ref if not g.switch_ref(dupe): return False self._remove_mark_flag(dupe) if not r.is_ref: self.__total_count += 1 self.__total_size += r.size if not dupe.is_ref: self.__total_count -= 1 self.__total_size -= dupe.size self.__dupes = None self.is_modified = True return True def perform_on_marked(self, func, remove_from_results): """Performs ``func`` on all marked dupes. If an ``EnvironmentError`` is raised during the call, the problematic dupe is added to self.problems. :param bool remove_from_results: If true, dupes which had ``func`` applied and didn't cause any problem. """ self.problems = [] to_remove = [] marked = (dupe for dupe in self.dupes if self.is_marked(dupe)) for dupe in marked: try: func(dupe) to_remove.append(dupe) except (OSError, UnicodeEncodeError) as e: self.problems.append((dupe, str(e))) if remove_from_results: self.remove_duplicates(to_remove) self.mark_none() for dupe, _ in self.problems: self.mark(dupe) def remove_duplicates(self, dupes): """Remove ``dupes`` from their respective :class:`~core.engine.Group`. Also, remove the group from :attr:`groups` if it ends up empty. """ affected_groups = set() for dupe in dupes: group = self.get_group_of_duplicate(dupe) if dupe not in group.dupes: return ref = group.ref group.remove_dupe(dupe, False) del self.__group_of_duplicate[dupe] self._remove_mark_flag(dupe) self.__total_count -= 1 self.__total_size -= dupe.size if not group: del self.__group_of_duplicate[ref] self.__groups.remove(group) if self.__filtered_groups: self.__filtered_groups.remove(group) else: affected_groups.add(group) for group in affected_groups: group.discard_matches() self.__dupes = None self.is_modified = bool(self.__groups) def save_to_xml(self, outfile): """Save results to ``outfile`` in XML. :param outfile: file object or path. """ self.apply_filter(None) root = ET.Element("results") for g in self.groups: group_elem = ET.SubElement(root, "group") dupe2index = {} for index, d in enumerate(g): dupe2index[d] = index try: words = engine.unpack_fields(d.words) except AttributeError: words = () file_elem = ET.SubElement(group_elem, "file") try: file_elem.set("path", str(d.path)) file_elem.set("words", ",".join(words)) except ValueError: # If there's an invalid character, just skip the file file_elem.set("path", "") file_elem.set("is_ref", ("y" if d.is_ref else "n")) file_elem.set("marked", ("y" if self.is_marked(d) else "n")) for match in g.matches: match_elem = ET.SubElement(group_elem, "match") match_elem.set("first", str(dupe2index[match.first])) match_elem.set("second", str(dupe2index[match.second])) match_elem.set("percentage", str(int(match.percentage))) tree = ET.ElementTree(root) def do_write(outfile): with FileOrPath(outfile, "wb") as fp: tree.write(fp, encoding="utf-8") try: do_write(outfile) except OSError as e: # If our OSError is because dest is already a directory, we want to handle that. 21 is # the code we get on OS X and Linux, 13 is what we get on Windows. if e.errno in {21, 13}: p = str(outfile) dirname, basename = op.split(p) otherfiles = os.listdir(dirname) newname = get_conflicted_name(otherfiles, basename) do_write(op.join(dirname, newname)) else: raise self.is_modified = False def sort_dupes(self, key, asc=True, delta=False): """Sort :attr:`dupes` according to ``key``. :param str key: key attribute name to sort with. :param bool asc: If false, sorting is reversed. :param bool delta: If true, sorting occurs using :ref:`delta values `. """ if not self.__dupes: self.__get_dupe_list() self.__dupes.sort( key=lambda d: self.app._get_dupe_sort_key(d, lambda: self.get_group_of_duplicate(d), key, delta), reverse=not asc, ) self.__dupes_sort_descriptor = (key, asc, delta) def sort_groups(self, key, asc=True): """Sort :attr:`groups` according to ``key``. The :attr:`~core.engine.Group.ref` of each group is used to extract values for sorting. :param str key: key attribute name to sort with. :param bool asc: If false, sorting is reversed. """ self.groups.sort(key=lambda g: self.app._get_group_sort_key(g, key), reverse=not asc) self.__groups_sort_descriptor = (key, asc) # ---Properties dupes = property(__get_dupe_list) groups = property(__get_groups, __set_groups) stat_line = property(__get_stat_line) dupeguru-4.3.1/core/scanner.py000066400000000000000000000213641426171743600163330ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import logging import re import os.path as op from collections import namedtuple from hscommon.jobprogress import job from hscommon.util import dedupe, rem_file_ext, get_file_ext from hscommon.trans import tr from core import engine # It's quite ugly to have scan types from all editions all put in the same class, but because there's # there will be some nasty bugs popping up (ScanType is used in core when in should exclusively be # used in core_*). One day I'll clean this up. class ScanType: FILENAME = 0 FIELDS = 1 FIELDSNOORDER = 2 TAG = 3 FOLDERS = 4 CONTENTS = 5 # PE FUZZYBLOCK = 10 EXIFTIMESTAMP = 11 ScanOption = namedtuple("ScanOption", "scan_type label") SCANNABLE_TAGS = ["track", "artist", "album", "title", "genre", "year"] RE_DIGIT_ENDING = re.compile(r"\d+|\(\d+\)|\[\d+\]|{\d+}") def is_same_with_digit(name, refname): # Returns True if name is the same as refname, but with digits (with brackets or not) at the end if not name.startswith(refname): return False end = name[len(refname) :].strip() return RE_DIGIT_ENDING.match(end) is not None def remove_dupe_paths(files): # Returns files with duplicates-by-path removed. Files with the exact same path are considered # duplicates and only the first file to have a path is kept. In certain cases, we have files # that have the same path, but not with the same case, that's why we normalize. However, we also # have case-sensitive filesystems, and in those, we don't want to falsely remove duplicates, # that's why we have a `samefile` mechanism. result = [] path2file = {} for f in files: normalized = str(f.path).lower() if normalized in path2file: try: if op.samefile(normalized, str(path2file[normalized].path)): continue # same file, it's a dupe else: pass # We don't treat them as dupes except OSError: continue # File doesn't exist? Well, treat them as dupes else: path2file[normalized] = f result.append(f) return result class Scanner: def __init__(self): self.discarded_file_count = 0 def _getmatches(self, files, j): if ( self.size_threshold or self.large_size_threshold or self.scan_type in { ScanType.CONTENTS, ScanType.FOLDERS, } ): j = j.start_subjob([2, 8]) for f in j.iter_with_progress(files, tr("Read size of %d/%d files")): f.size # pre-read, makes a smoother progress if read here (especially for bundles) if self.size_threshold: files = [f for f in files if f.size >= self.size_threshold] if self.large_size_threshold: files = [f for f in files if f.size <= self.large_size_threshold] if self.scan_type in {ScanType.CONTENTS, ScanType.FOLDERS}: return engine.getmatches_by_contents(files, bigsize=self.big_file_size_threshold, j=j) else: j = j.start_subjob([2, 8]) kw = {} kw["match_similar_words"] = self.match_similar_words kw["weight_words"] = self.word_weighting kw["min_match_percentage"] = self.min_match_percentage if self.scan_type == ScanType.FIELDSNOORDER: self.scan_type = ScanType.FIELDS kw["no_field_order"] = True func = { ScanType.FILENAME: lambda f: engine.getwords(rem_file_ext(f.name)), ScanType.FIELDS: lambda f: engine.getfields(rem_file_ext(f.name)), ScanType.TAG: lambda f: [ engine.getwords(str(getattr(f, attrname))) for attrname in SCANNABLE_TAGS if attrname in self.scanned_tags ], }[self.scan_type] for f in j.iter_with_progress(files, tr("Read metadata of %d/%d files")): logging.debug("Reading metadata of %s", f.path) f.words = func(f) return engine.getmatches(files, j=j, **kw) @staticmethod def _key_func(dupe): return -dupe.size @staticmethod def _tie_breaker(ref, dupe): refname = rem_file_ext(ref.name).lower() dupename = rem_file_ext(dupe.name).lower() if "copy" in dupename: return False if "copy" in refname: return True if is_same_with_digit(dupename, refname): return False if is_same_with_digit(refname, dupename): return True return len(dupe.path.parts) > len(ref.path.parts) @staticmethod def get_scan_options(): """Returns a list of scanning options for this scanner. Returns a list of ``ScanOption``. """ raise NotImplementedError() def get_dupe_groups(self, files, ignore_list=None, j=job.nulljob): for f in (f for f in files if not hasattr(f, "is_ref")): f.is_ref = False files = remove_dupe_paths(files) logging.info("Getting matches. Scan type: %d", self.scan_type) matches = self._getmatches(files, j) logging.info("Found %d matches" % len(matches)) j.set_progress(100, tr("Almost done! Fiddling with results...")) # In removing what we call here "false matches", we first want to remove, if we scan by # folders, we want to remove folder matches for which the parent is also in a match (they're # "duplicated duplicates if you will). Then, we also don't want mixed file kinds if the # option isn't enabled, we want matches for which both files exist and, lastly, we don't # want matches with both files as ref. if self.scan_type == ScanType.FOLDERS and matches: allpath = {m.first.path for m in matches} allpath |= {m.second.path for m in matches} sortedpaths = sorted(allpath) toremove = set() last_parent_path = sortedpaths[0] for p in sortedpaths[1:]: if last_parent_path in p.parents: toremove.add(p) else: last_parent_path = p matches = [m for m in matches if m.first.path not in toremove or m.second.path not in toremove] if not self.mix_file_kind: matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)] matches = [m for m in matches if m.first.path.exists() and m.second.path.exists()] matches = [m for m in matches if not (m.first.is_ref and m.second.is_ref)] if ignore_list: matches = [m for m in matches if not ignore_list.are_ignored(str(m.first.path), str(m.second.path))] logging.info("Grouping matches") groups = engine.get_groups(matches) if self.scan_type in { ScanType.FILENAME, ScanType.FIELDS, ScanType.FIELDSNOORDER, ScanType.TAG, }: matched_files = dedupe([m.first for m in matches] + [m.second for m in matches]) self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups) else: # Ticket #195 # To speed up the scan, we don't bother comparing contents of files that are both ref # files. However, this messes up "discarded" counting because there's a missing match # in cases where we end up with a dupe group anyway (with a non-ref file). Because it's # impossible to have discarded matches in exact dupe scans, we simply set it at 0, thus # bypassing our tricky problem. # Also, although ScanType.FuzzyBlock is not always doing exact comparisons, we also # bypass ref comparison, thus messing up with our "discarded" count. So we're # effectively disabling the "discarded" feature in PE, but it's better than falsely # reporting discarded matches. self.discarded_file_count = 0 groups = [g for g in groups if any(not f.is_ref for f in g)] logging.info("Created %d groups" % len(groups)) for g in groups: g.prioritize(self._key_func, self._tie_breaker) return groups match_similar_words = False min_match_percentage = 80 mix_file_kind = True scan_type = ScanType.FILENAME scanned_tags = {"artist", "title"} size_threshold = 0 large_size_threshold = 0 big_file_size_threshold = 0 word_weighting = False dupeguru-4.3.1/core/se/000077500000000000000000000000001426171743600147315ustar00rootroot00000000000000dupeguru-4.3.1/core/se/__init__.py000066400000000000000000000000661426171743600170440ustar00rootroot00000000000000from core.se import fs, result_table, scanner # noqa dupeguru-4.3.1/core/se/fs.py000066400000000000000000000027371426171743600157240ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2013-07-14 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.util import format_size from core import fs from core.util import format_timestamp, format_perc, format_words, format_dupe_count def get_display_info(dupe, group, delta): size = dupe.size mtime = dupe.mtime m = group.get_match_of(dupe) if m: percentage = m.percentage dupe_count = 0 if delta: r = group.ref size -= r.size mtime -= r.mtime else: percentage = group.percentage dupe_count = len(group.dupes) return { "name": dupe.name, "folder_path": str(dupe.folder_path), "size": format_size(size, 0, 1, False), "extension": dupe.extension, "mtime": format_timestamp(mtime, delta and m), "percentage": format_perc(percentage), "words": format_words(dupe.words) if hasattr(dupe, "words") else "", "dupe_count": format_dupe_count(dupe_count), } class File(fs.File): def get_display_info(self, group, delta): return get_display_info(self, group, delta) class Folder(fs.Folder): def get_display_info(self, group, delta): return get_display_info(self, group, delta) dupeguru-4.3.1/core/se/result_table.py000066400000000000000000000021531426171743600177710ustar00rootroot00000000000000# Created On: 2011-11-27 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.gui.column import Column from hscommon.trans import trget from core.gui.result_table import ResultTable as ResultTableBase coltr = trget("columns") class ResultTable(ResultTableBase): COLUMNS = [ Column("marked", ""), Column("name", coltr("Filename")), Column("folder_path", coltr("Folder"), optional=True), Column("size", coltr("Size (KB)"), optional=True), Column("extension", coltr("Kind"), visible=False, optional=True), Column("mtime", coltr("Modification"), visible=False, optional=True), Column("percentage", coltr("Match %"), optional=True), Column("words", coltr("Words Used"), visible=False, optional=True), Column("dupe_count", coltr("Dupe Count"), visible=False, optional=True), ] DELTA_COLUMNS = {"size", "mtime"} dupeguru-4.3.1/core/se/scanner.py000066400000000000000000000012221426171743600167310ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.trans import tr from core.scanner import Scanner as ScannerBase, ScanOption, ScanType class ScannerSE(ScannerBase): @staticmethod def get_scan_options(): return [ ScanOption(ScanType.FILENAME, tr("Filename")), ScanOption(ScanType.CONTENTS, tr("Contents")), ScanOption(ScanType.FOLDERS, tr("Folders")), ] dupeguru-4.3.1/core/tests/000077500000000000000000000000001426171743600154645ustar00rootroot00000000000000dupeguru-4.3.1/core/tests/__init__.py000066400000000000000000000000001426171743600175630ustar00rootroot00000000000000dupeguru-4.3.1/core/tests/app_test.py000066400000000000000000000502041426171743600176560ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import os import os.path as op import logging import tempfile import pytest from pathlib import Path import hscommon.conflict import hscommon.util from hscommon.testutil import eq_, log_calls from hscommon.jobprogress.job import Job from core.tests.base import TestApp from core.tests.results_test import GetTestGroups from core import app, fs, engine from core.scanner import ScanType def add_fake_files_to_directories(directories, files): directories.get_files = lambda j=None: iter(files) directories._dirs.append("this is just so Scan() doesn't return 3") class TestCaseDupeGuru: def test_apply_filter_calls_results_apply_filter(self, monkeypatch): dgapp = TestApp().app monkeypatch.setattr(dgapp.results, "apply_filter", log_calls(dgapp.results.apply_filter)) dgapp.apply_filter("foo") eq_(2, len(dgapp.results.apply_filter.calls)) call = dgapp.results.apply_filter.calls[0] assert call["filter_str"] is None call = dgapp.results.apply_filter.calls[1] eq_("foo", call["filter_str"]) def test_apply_filter_escapes_regexp(self, monkeypatch): dgapp = TestApp().app monkeypatch.setattr(dgapp.results, "apply_filter", log_calls(dgapp.results.apply_filter)) dgapp.apply_filter("()[]\\.|+?^abc") call = dgapp.results.apply_filter.calls[1] eq_("\\(\\)\\[\\]\\\\\\.\\|\\+\\?\\^abc", call["filter_str"]) dgapp.apply_filter("(*)") # In "simple mode", we want the * to behave as a wildcard call = dgapp.results.apply_filter.calls[3] eq_(r"\(.*\)", call["filter_str"]) dgapp.options["escape_filter_regexp"] = False dgapp.apply_filter("(abc)") call = dgapp.results.apply_filter.calls[5] eq_("(abc)", call["filter_str"]) def test_copy_or_move(self, tmpdir, monkeypatch): # The goal here is just to have a test for a previous blowup I had. I know my test coverage # for this unit is pathetic. What's done is done. My approach now is to add tests for # every change I want to make. The blowup was caused by a missing import. p = Path(str(tmpdir)) p.joinpath("foo").touch() monkeypatch.setattr( hscommon.conflict, "smart_copy", log_calls(lambda source_path, dest_path: None), ) # XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher. monkeypatch.setattr(app, "smart_copy", hscommon.conflict.smart_copy) monkeypatch.setattr(os, "makedirs", lambda path: None) # We don't want the test to create that fake directory dgapp = TestApp().app dgapp.directories.add_path(p) [f] = dgapp.directories.get_files() with tempfile.TemporaryDirectory() as tmp_dir: dgapp.copy_or_move(f, True, tmp_dir, 0) eq_(1, len(hscommon.conflict.smart_copy.calls)) call = hscommon.conflict.smart_copy.calls[0] eq_(call["dest_path"], Path(tmp_dir, "foo")) eq_(call["source_path"], f.path) def test_copy_or_move_clean_empty_dirs(self, tmpdir, monkeypatch): tmppath = Path(str(tmpdir)) sourcepath = tmppath.joinpath("source") sourcepath.mkdir() sourcepath.joinpath("myfile").touch() app = TestApp().app app.directories.add_path(tmppath) [myfile] = app.directories.get_files() monkeypatch.setattr(app, "clean_empty_dirs", log_calls(lambda path: None)) app.copy_or_move(myfile, False, tmppath.joinpath("dest"), 0) calls = app.clean_empty_dirs.calls eq_(1, len(calls)) eq_(sourcepath, calls[0]["path"]) def test_scan_with_objects_evaluating_to_false(self): class FakeFile(fs.File): def __bool__(self): return False # At some point, any() was used in a wrong way that made Scan() wrongly return 1 app = TestApp().app f1, f2 = (FakeFile("foo") for _ in range(2)) f1.is_ref, f2.is_ref = (False, False) assert not (bool(f1) and bool(f2)) add_fake_files_to_directories(app.directories, [f1, f2]) app.start_scanning() # no exception @pytest.mark.skipif("not hasattr(os, 'link')") def test_ignore_hardlink_matches(self, tmpdir): # If the ignore_hardlink_matches option is set, don't match files hardlinking to the same # inode. tmppath = Path(str(tmpdir)) tmppath.joinpath("myfile").open("wt").write("foo") os.link(str(tmppath.joinpath("myfile")), str(tmppath.joinpath("hardlink"))) app = TestApp().app app.directories.add_path(tmppath) app.options["scan_type"] = ScanType.CONTENTS app.options["ignore_hardlink_matches"] = True app.start_scanning() eq_(len(app.results.groups), 0) def test_rename_when_nothing_is_selected(self): # Issue #140 # It's possible that rename operation has its selected row swept off from under it, thus # making the selected row None. Don't crash when it happens. dgapp = TestApp().app # selected_row is None because there's no result. assert not dgapp.result_table.rename_selected("foo") # no crash class TestCaseDupeGuruCleanEmptyDirs: @pytest.fixture def do_setup(self, request): monkeypatch = request.getfixturevalue("monkeypatch") monkeypatch.setattr( hscommon.util, "delete_if_empty", log_calls(lambda path, files_to_delete=[]: None), ) # XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher. monkeypatch.setattr(app, "delete_if_empty", hscommon.util.delete_if_empty) self.app = TestApp().app def test_option_off(self, do_setup): self.app.clean_empty_dirs(Path("/foo/bar")) eq_(0, len(hscommon.util.delete_if_empty.calls)) def test_option_on(self, do_setup): self.app.options["clean_empty_dirs"] = True self.app.clean_empty_dirs(Path("/foo/bar")) calls = hscommon.util.delete_if_empty.calls eq_(1, len(calls)) eq_(Path("/foo/bar"), calls[0]["path"]) eq_([".DS_Store"], calls[0]["files_to_delete"]) def test_recurse_up(self, do_setup, monkeypatch): # delete_if_empty must be recursively called up in the path until it returns False @log_calls def mock_delete_if_empty(path, files_to_delete=[]): return len(path.parts) > 1 monkeypatch.setattr(hscommon.util, "delete_if_empty", mock_delete_if_empty) # XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher. monkeypatch.setattr(app, "delete_if_empty", mock_delete_if_empty) self.app.options["clean_empty_dirs"] = True self.app.clean_empty_dirs(Path("not-empty/empty/empty")) calls = hscommon.util.delete_if_empty.calls eq_(3, len(calls)) eq_(Path("not-empty/empty/empty"), calls[0]["path"]) eq_(Path("not-empty/empty"), calls[1]["path"]) eq_(Path("not-empty"), calls[2]["path"]) class TestCaseDupeGuruWithResults: @pytest.fixture def do_setup(self, request): app = TestApp() self.app = app.app self.objects, self.matches, self.groups = GetTestGroups() self.app.results.groups = self.groups self.dpanel = app.dpanel self.dtree = app.dtree self.rtable = app.rtable self.rtable.refresh() tmpdir = request.getfixturevalue("tmpdir") tmppath = Path(str(tmpdir)) tmppath.joinpath("foo").mkdir() tmppath.joinpath("bar").mkdir() self.app.directories.add_path(tmppath) def test_get_objects(self, do_setup): objects = self.objects groups = self.groups r = self.rtable[0] assert r._group is groups[0] assert r._dupe is objects[0] r = self.rtable[1] assert r._group is groups[0] assert r._dupe is objects[1] r = self.rtable[4] assert r._group is groups[1] assert r._dupe is objects[4] def test_get_objects_after_sort(self, do_setup): objects = self.objects groups = self.groups[:] # we need an un-sorted reference self.rtable.sort("name", False) r = self.rtable[1] assert r._group is groups[1] assert r._dupe is objects[4] def test_selected_result_node_paths_after_deletion(self, do_setup): # cases where the selected dupes aren't there are correctly handled self.rtable.select([1, 2, 3]) self.app.remove_selected() # The first 2 dupes have been removed. The 3rd one is a ref. it stays there, in first pos. eq_(self.rtable.selected_indexes, [1]) # no exception def test_select_result_node_paths(self, do_setup): app = self.app objects = self.objects self.rtable.select([1, 2]) eq_(len(app.selected_dupes), 2) assert app.selected_dupes[0] is objects[1] assert app.selected_dupes[1] is objects[2] def test_select_result_node_paths_with_ref(self, do_setup): app = self.app objects = self.objects self.rtable.select([1, 2, 3]) eq_(len(app.selected_dupes), 3) assert app.selected_dupes[0] is objects[1] assert app.selected_dupes[1] is objects[2] assert app.selected_dupes[2] is self.groups[1].ref def test_select_result_node_paths_after_sort(self, do_setup): app = self.app objects = self.objects groups = self.groups[:] # To keep the old order in memory self.rtable.sort("name", False) # 0 # Now, the group order is supposed to be reversed self.rtable.select([1, 2, 3]) eq_(len(app.selected_dupes), 3) assert app.selected_dupes[0] is objects[4] assert app.selected_dupes[1] is groups[0].ref assert app.selected_dupes[2] is objects[1] def test_selected_powermarker_node_paths(self, do_setup): # app.selected_dupes is correctly converted into paths self.rtable.power_marker = True self.rtable.select([0, 1, 2]) self.rtable.power_marker = False eq_(self.rtable.selected_indexes, [1, 2, 4]) def test_selected_powermarker_node_paths_after_deletion(self, do_setup): # cases where the selected dupes aren't there are correctly handled app = self.app self.rtable.power_marker = True self.rtable.select([0, 1, 2]) app.remove_selected() eq_(self.rtable.selected_indexes, []) # no exception def test_select_powermarker_rows_after_sort(self, do_setup): app = self.app objects = self.objects self.rtable.power_marker = True self.rtable.sort("name", False) self.rtable.select([0, 1, 2]) eq_(len(app.selected_dupes), 3) assert app.selected_dupes[0] is objects[4] assert app.selected_dupes[1] is objects[2] assert app.selected_dupes[2] is objects[1] def test_toggle_selected_mark_state(self, do_setup): app = self.app objects = self.objects app.toggle_selected_mark_state() eq_(app.results.mark_count, 0) self.rtable.select([1, 4]) app.toggle_selected_mark_state() eq_(app.results.mark_count, 2) assert not app.results.is_marked(objects[0]) assert app.results.is_marked(objects[1]) assert not app.results.is_marked(objects[2]) assert not app.results.is_marked(objects[3]) assert app.results.is_marked(objects[4]) def test_toggle_selected_mark_state_with_different_selected_state(self, do_setup): # When marking selected dupes with a heterogenous selection, mark all selected dupes. When # it's homogenous, simply toggle. app = self.app self.rtable.select([1]) app.toggle_selected_mark_state() # index 0 is unmarkable, but we throw it in the bunch to be sure that it doesn't make the # selection heterogenoug when it shouldn't. self.rtable.select([0, 1, 4]) app.toggle_selected_mark_state() eq_(app.results.mark_count, 2) app.toggle_selected_mark_state() eq_(app.results.mark_count, 0) def test_refresh_details_with_selected(self, do_setup): self.rtable.select([1, 4]) eq_(self.dpanel.row(0), ("Filename", "bar bleh", "foo bar")) self.dpanel.view.check_gui_calls(["refresh"]) self.rtable.select([]) eq_(self.dpanel.row(0), ("Filename", "---", "---")) self.dpanel.view.check_gui_calls(["refresh"]) def test_make_selected_reference(self, do_setup): app = self.app objects = self.objects groups = self.groups self.rtable.select([1, 4]) app.make_selected_reference() assert groups[0].ref is objects[1] assert groups[1].ref is objects[4] def test_make_selected_reference_by_selecting_two_dupes_in_the_same_group(self, do_setup): app = self.app objects = self.objects groups = self.groups self.rtable.select([1, 2, 4]) # Only [0, 0] and [1, 0] must go ref, not [0, 1] because it is a part of the same group app.make_selected_reference() assert groups[0].ref is objects[1] assert groups[1].ref is objects[4] def test_remove_selected(self, do_setup): app = self.app self.rtable.select([1, 4]) app.remove_selected() eq_(len(app.results.dupes), 1) # the first path is now selected app.remove_selected() eq_(len(app.results.dupes), 0) def test_add_directory_simple(self, do_setup): # There's already a directory in self.app, so adding another once makes 2 of em app = self.app # any other path that isn't a parent or child of the already added path otherpath = Path(op.dirname(__file__)) app.add_directory(otherpath) eq_(len(app.directories), 2) def test_add_directory_already_there(self, do_setup): app = self.app otherpath = Path(op.dirname(__file__)) app.add_directory(otherpath) app.add_directory(otherpath) eq_(len(app.view.messages), 1) assert "already" in app.view.messages[0] def test_add_directory_does_not_exist(self, do_setup): app = self.app app.add_directory("/does_not_exist") eq_(len(app.view.messages), 1) assert "exist" in app.view.messages[0] def test_ignore(self, do_setup): app = self.app self.rtable.select([4]) # The dupe of the second, 2 sized group app.add_selected_to_ignore_list() eq_(len(app.ignore_list), 1) self.rtable.select([1]) # first dupe of the 3 dupes group app.add_selected_to_ignore_list() # BOTH the ref and the other dupe should have been added eq_(len(app.ignore_list), 3) def test_purge_ignorelist(self, do_setup, tmpdir): app = self.app p1 = str(tmpdir.join("file1")) p2 = str(tmpdir.join("file2")) open(p1, "w").close() open(p2, "w").close() dne = "/does_not_exist" app.ignore_list.ignore(dne, p1) app.ignore_list.ignore(p2, dne) app.ignore_list.ignore(p1, p2) app.purge_ignore_list() eq_(1, len(app.ignore_list)) assert app.ignore_list.are_ignored(p1, p2) assert not app.ignore_list.are_ignored(dne, p1) def test_only_unicode_is_added_to_ignore_list(self, do_setup): def fake_ignore(first, second): if not isinstance(first, str): self.fail() if not isinstance(second, str): self.fail() app = self.app app.ignore_list.ignore = fake_ignore self.rtable.select([4]) app.add_selected_to_ignore_list() def test_cancel_scan_with_previous_results(self, do_setup): # When doing a scan with results being present prior to the scan, correctly invalidate the # results table. app = self.app app.JOB = Job(1, lambda *args, **kw: False) # Cancels the task add_fake_files_to_directories(app.directories, self.objects) # We want the scan to at least start app.start_scanning() # will be cancelled immediately eq_(len(app.result_table), 0) def test_selected_dupes_after_removal(self, do_setup): # Purge the app's `selected_dupes` attribute when removing dupes, or else it might cause a # crash later with None refs. app = self.app app.results.mark_all() self.rtable.select([0, 1, 2, 3, 4]) app.remove_marked() eq_(len(self.rtable), 0) eq_(app.selected_dupes, []) def test_dont_crash_on_delta_powermarker_dupecount_sort(self, do_setup): # Don't crash when sorting by dupe count or percentage while delta+powermarker are enabled. # Ref #238 self.rtable.delta_values = True self.rtable.power_marker = True self.rtable.sort("dupe_count", False) # don't crash self.rtable.sort("percentage", False) # don't crash class TestCaseDupeGuruRenameSelected: @pytest.fixture def do_setup(self, request): tmpdir = request.getfixturevalue("tmpdir") p = Path(str(tmpdir)) p.joinpath("foo bar 1").touch() p.joinpath("foo bar 2").touch() p.joinpath("foo bar 3").touch() files = fs.get_files(p) for f in files: f.is_ref = False matches = engine.getmatches(files) groups = engine.get_groups(matches) g = groups[0] g.prioritize(lambda x: x.name) app = TestApp() app.app.results.groups = groups self.app = app.app self.rtable = app.rtable self.rtable.refresh() self.groups = groups self.p = p self.files = files def test_simple(self, do_setup): app = self.app g = self.groups[0] self.rtable.select([1]) assert app.rename_selected("renamed") names = [p.name for p in self.p.glob("*")] assert "renamed" in names assert "foo bar 2" not in names eq_(g.dupes[0].name, "renamed") def test_none_selected(self, do_setup, monkeypatch): app = self.app g = self.groups[0] self.rtable.select([]) monkeypatch.setattr(logging, "warning", log_calls(lambda msg: None)) assert not app.rename_selected("renamed") msg = logging.warning.calls[0]["msg"] eq_("dupeGuru Warning: list index out of range", msg) names = [p.name for p in self.p.glob("*")] assert "renamed" not in names assert "foo bar 2" in names eq_(g.dupes[0].name, "foo bar 2") def test_name_already_exists(self, do_setup, monkeypatch): app = self.app g = self.groups[0] self.rtable.select([1]) monkeypatch.setattr(logging, "warning", log_calls(lambda msg: None)) assert not app.rename_selected("foo bar 1") msg = logging.warning.calls[0]["msg"] assert msg.startswith("dupeGuru Warning: 'foo bar 1' already exists in") names = [p.name for p in self.p.glob("*")] assert "foo bar 1" in names assert "foo bar 2" in names eq_(g.dupes[0].name, "foo bar 2") class TestAppWithDirectoriesInTree: @pytest.fixture def do_setup(self, request): tmpdir = request.getfixturevalue("tmpdir") p = Path(str(tmpdir)) p.joinpath("sub1").mkdir() p.joinpath("sub2").mkdir() p.joinpath("sub3").mkdir() app = TestApp() self.app = app.app self.dtree = app.dtree self.dtree.add_directory(p) self.dtree.view.clear_calls() def test_set_root_as_ref_makes_subfolders_ref_as_well(self, do_setup): # Setting a node state to something also affect subnodes. These subnodes must be correctly # refreshed. node = self.dtree[0] eq_(len(node), 3) # a len() call is required for subnodes to be loaded node.state = 1 # the state property is a state index node = self.dtree[0] eq_(len(node), 3) subnode = node[0] eq_(subnode.state, 1) self.dtree.view.check_gui_calls(["refresh_states"]) dupeguru-4.3.1/core/tests/base.py000066400000000000000000000132401426171743600167500ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.testutil import TestApp as TestAppBase, CallLogger, eq_, with_app # noqa from pathlib import Path from hscommon.util import get_file_ext, format_size from hscommon.gui.column import Column from hscommon.jobprogress.job import nulljob, JobCancelled from core import engine, prioritize from core.engine import getwords from core.app import DupeGuru as DupeGuruBase from core.gui.result_table import ResultTable as ResultTableBase from core.gui.prioritize_dialog import PrioritizeDialog class DupeGuruView: JOB = nulljob def __init__(self): self.messages = [] def start_job(self, jobid, func, args=()): try: func(self.JOB, *args) except JobCancelled: return def get_default(self, key_name): return None def set_default(self, key_name, value): pass def show_message(self, msg): self.messages.append(msg) def ask_yes_no(self, prompt): return True # always answer yes def create_results_window(self): pass class ResultTable(ResultTableBase): COLUMNS = [ Column("marked", ""), Column("name", "Filename"), Column("folder_path", "Directory"), Column("size", "Size (KB)"), Column("extension", "Kind"), ] DELTA_COLUMNS = { "size", } class DupeGuru(DupeGuruBase): NAME = "dupeGuru" METADATA_TO_READ = ["size"] def __init__(self): DupeGuruBase.__init__(self, DupeGuruView()) self.appdata = "/tmp" self._recreate_result_table() def _prioritization_categories(self): return prioritize.all_categories() def _recreate_result_table(self): if self.result_table is not None: self.result_table.disconnect() self.result_table = ResultTable(self) self.result_table.view = CallLogger() self.result_table.connect() class NamedObject: def __init__(self, name="foobar", with_words=False, size=1, folder=None): self.name = name if folder is None: folder = "basepath" self._folder = Path(folder) self.size = size self.digest_partial = name self.digest = name self.digest_samples = name if with_words: self.words = getwords(name) self.is_ref = False def __bool__(self): return False # Make sure that operations are made correctly when the bool value of files is false. def get_display_info(self, group, delta): size = self.size m = group.get_match_of(self) if m and delta: r = group.ref size -= r.size return { "name": self.name, "folder_path": str(self.folder_path), "size": format_size(size, 0, 1, False), "extension": self.extension if hasattr(self, "extension") else "---", } @property def path(self): return self._folder.joinpath(self.name) @property def folder_path(self): return self.path.parent @property def extension(self): return get_file_ext(self.name) # Returns a group set that looks like that: # "foo bar" (1) # "bar bleh" (1024) # "foo bleh" (1) # "ibabtu" (1) # "ibabtu" (1) def GetTestGroups(): objects = [ NamedObject("foo bar"), NamedObject("bar bleh"), NamedObject("foo bleh"), NamedObject("ibabtu"), NamedObject("ibabtu"), ] objects[1].size = 1024 matches = engine.getmatches(objects) # we should have 5 matches groups = engine.get_groups(matches) # We should have 2 groups for g in groups: g.prioritize(lambda x: objects.index(x)) # We want the dupes to be in the same order as the list is groups.sort(key=len, reverse=True) # We want the group with 3 members to be first. return (objects, matches, groups) class TestApp(TestAppBase): __test__ = False def __init__(self): def link_gui(gui): gui.view = self.make_logger() if hasattr(gui, "_columns"): # tables gui._columns.view = self.make_logger() return gui TestAppBase.__init__(self) self.app = DupeGuru() self.default_parent = self.app self.dtree = link_gui(self.app.directory_tree) self.dpanel = link_gui(self.app.details_panel) self.slabel = link_gui(self.app.stats_label) self.pdialog = PrioritizeDialog(self.app) link_gui(self.pdialog.category_list) link_gui(self.pdialog.criteria_list) link_gui(self.pdialog.prioritization_list) link_gui(self.app.ignore_list_dialog) link_gui(self.app.ignore_list_dialog.ignore_list_table) link_gui(self.app.progress_window) link_gui(self.app.progress_window.jobdesc_textfield) link_gui(self.app.progress_window.progressdesc_textfield) @property def rtable(self): # rtable is a property because its instance can be replaced during execution return self.app.result_table # --- Helpers def select_pri_criterion(self, name): # Select a main prioritize criterion by name instead of by index. Makes tests more # maintainable. index = self.pdialog.category_list.index(name) self.pdialog.category_list.select(index) def add_pri_criterion(self, name, index): self.select_pri_criterion(name) self.pdialog.criteria_list.select([index]) self.pdialog.add_selected() dupeguru-4.3.1/core/tests/block_test.py000066400000000000000000000130171426171743600201710ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html # The commented out tests are tests for function that have been converted to pure C for speed from pytest import raises, skip from hscommon.testutil import eq_ try: from core.pe.block import avgdiff, getblocks2, NoBlocksError, DifferentBlockCountError except ImportError: skip("Can't import the block module, probably hasn't been compiled.") def my_avgdiff(first, second, limit=768, min_iter=3): # this is so I don't have to re-write every call return avgdiff(first, second, limit, min_iter) BLACK = (0, 0, 0) RED = (0xFF, 0, 0) GREEN = (0, 0xFF, 0) BLUE = (0, 0, 0xFF) class FakeImage: def __init__(self, size, data): self.size = size self.data = data def getdata(self): return self.data def crop(self, box): pixels = [] for i in range(box[1], box[3]): for j in range(box[0], box[2]): pixel = self.data[i * self.size[0] + j] pixels.append(pixel) return FakeImage((box[2] - box[0], box[3] - box[1]), pixels) def empty(): return FakeImage((0, 0), []) def single_pixel(): # one red pixel return FakeImage((1, 1), [(0xFF, 0, 0)]) def four_pixels(): pixels = [RED, (0, 0x80, 0xFF), (0x80, 0, 0), (0, 0x40, 0x80)] return FakeImage((2, 2), pixels) class TestCasegetblock: def test_single_pixel(self): im = single_pixel() [b] = getblocks2(im, 1) eq_(RED, b) def test_no_pixel(self): im = empty() eq_([], getblocks2(im, 1)) def test_four_pixels(self): im = four_pixels() [b] = getblocks2(im, 1) meanred = (0xFF + 0x80) // 4 meangreen = (0x80 + 0x40) // 4 meanblue = (0xFF + 0x80) // 4 eq_((meanred, meangreen, meanblue), b) class TestCasegetblocks2: def test_empty_image(self): im = empty() blocks = getblocks2(im, 1) eq_(0, len(blocks)) def test_one_block_image(self): im = four_pixels() blocks = getblocks2(im, 1) eq_(1, len(blocks)) block = blocks[0] meanred = (0xFF + 0x80) // 4 meangreen = (0x80 + 0x40) // 4 meanblue = (0xFF + 0x80) // 4 eq_((meanred, meangreen, meanblue), block) def test_four_blocks_all_black(self): im = FakeImage((2, 2), [BLACK, BLACK, BLACK, BLACK]) blocks = getblocks2(im, 2) eq_(4, len(blocks)) for block in blocks: eq_(BLACK, block) def test_two_pixels_image_horizontal(self): pixels = [RED, BLUE] im = FakeImage((2, 1), pixels) blocks = getblocks2(im, 2) eq_(4, len(blocks)) eq_(RED, blocks[0]) eq_(BLUE, blocks[1]) eq_(RED, blocks[2]) eq_(BLUE, blocks[3]) def test_two_pixels_image_vertical(self): pixels = [RED, BLUE] im = FakeImage((1, 2), pixels) blocks = getblocks2(im, 2) eq_(4, len(blocks)) eq_(RED, blocks[0]) eq_(RED, blocks[1]) eq_(BLUE, blocks[2]) eq_(BLUE, blocks[3]) class TestCaseavgdiff: def test_empty(self): with raises(NoBlocksError): my_avgdiff([], []) def test_two_blocks(self): b1 = (5, 10, 15) b2 = (255, 250, 245) b3 = (0, 0, 0) b4 = (255, 0, 255) blocks1 = [b1, b2] blocks2 = [b3, b4] expected1 = 5 + 10 + 15 expected2 = 0 + 250 + 10 expected = (expected1 + expected2) // 2 eq_(expected, my_avgdiff(blocks1, blocks2)) def test_blocks_not_the_same_size(self): b = (0, 0, 0) with raises(DifferentBlockCountError): my_avgdiff([b, b], [b]) def test_first_arg_is_empty_but_not_second(self): # Don't return 0 (as when the 2 lists are empty), raise! b = (0, 0, 0) with raises(DifferentBlockCountError): my_avgdiff([], [b]) def test_limit(self): ref = (0, 0, 0) b1 = (10, 10, 10) # avg 30 b2 = (20, 20, 20) # avg 45 b3 = (30, 30, 30) # avg 60 blocks1 = [ref, ref, ref] blocks2 = [b1, b2, b3] eq_(45, my_avgdiff(blocks1, blocks2, 44)) def test_min_iterations(self): ref = (0, 0, 0) b1 = (10, 10, 10) # avg 30 b2 = (20, 20, 20) # avg 45 b3 = (10, 10, 10) # avg 40 blocks1 = [ref, ref, ref] blocks2 = [b1, b2, b3] eq_(40, my_avgdiff(blocks1, blocks2, 45 - 1, 3)) # Bah, I don't know why this test fails, but I don't think it matters very much # def test_just_over_the_limit(self): # #A score just over the limit might return exactly the limit due to truncating. We should # #ceil() the result in this case. # ref = (0, 0, 0) # b1 = (10, 0, 0) # b2 = (11, 0, 0) # blocks1 = [ref, ref] # blocks2 = [b1, b2] # eq_(11, my_avgdiff(blocks1, blocks2, 10)) # def test_return_at_least_1_at_the_slightest_difference(self): ref = (0, 0, 0) b1 = (1, 0, 0) blocks1 = [ref for _ in range(250)] blocks2 = [ref for _ in range(250)] blocks2[0] = b1 eq_(1, my_avgdiff(blocks1, blocks2)) def test_return_0_if_there_is_no_difference(self): ref = (0, 0, 0) blocks1 = [ref, ref] blocks2 = [ref, ref] eq_(0, my_avgdiff(blocks1, blocks2)) dupeguru-4.3.1/core/tests/cache_test.py000066400000000000000000000111231426171743600201360ustar00rootroot00000000000000# Copyright 2016 Virgil Dupras # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import logging from pytest import raises, skip from hscommon.testutil import eq_ try: from core.pe.cache import colors_to_string, string_to_colors from core.pe.cache_sqlite import SqliteCache from core.pe.cache_shelve import ShelveCache except ImportError: skip("Can't import the cache module, probably hasn't been compiled.") class TestCaseColorsToString: def test_no_color(self): eq_("", colors_to_string([])) def test_single_color(self): eq_("000000", colors_to_string([(0, 0, 0)])) eq_("010101", colors_to_string([(1, 1, 1)])) eq_("0a141e", colors_to_string([(10, 20, 30)])) def test_two_colors(self): eq_("000102030405", colors_to_string([(0, 1, 2), (3, 4, 5)])) class TestCaseStringToColors: def test_empty(self): eq_([], string_to_colors("")) def test_single_color(self): eq_([(0, 0, 0)], string_to_colors("000000")) eq_([(2, 3, 4)], string_to_colors("020304")) eq_([(10, 20, 30)], string_to_colors("0a141e")) def test_two_colors(self): eq_([(10, 20, 30), (40, 50, 60)], string_to_colors("0a141e28323c")) def test_incomplete_color(self): # don't return anything if it's not a complete color eq_([], string_to_colors("102")) class BaseTestCaseCache: def get_cache(self, dbname=None): raise NotImplementedError() def test_empty(self): c = self.get_cache() eq_(0, len(c)) with raises(KeyError): c["foo"] def test_set_then_retrieve_blocks(self): c = self.get_cache() b = [(0, 0, 0), (1, 2, 3)] c["foo"] = b eq_(b, c["foo"]) def test_delitem(self): c = self.get_cache() c["foo"] = "" del c["foo"] assert "foo" not in c with raises(KeyError): del c["foo"] def test_persistance(self, tmpdir): DBNAME = tmpdir.join("hstest.db") c = self.get_cache(str(DBNAME)) c["foo"] = [(1, 2, 3)] del c c = self.get_cache(str(DBNAME)) eq_([(1, 2, 3)], c["foo"]) def test_filter(self): c = self.get_cache() c["foo"] = "" c["bar"] = "" c["baz"] = "" c.filter(lambda p: p != "bar") # only 'bar' is removed eq_(2, len(c)) assert "foo" in c assert "baz" in c assert "bar" not in c def test_clear(self): c = self.get_cache() c["foo"] = "" c["bar"] = "" c["baz"] = "" c.clear() eq_(0, len(c)) assert "foo" not in c assert "baz" not in c assert "bar" not in c def test_by_id(self): # it's possible to use the cache by referring to the files by their row_id c = self.get_cache() b = [(0, 0, 0), (1, 2, 3)] c["foo"] = b foo_id = c.get_id("foo") eq_(c[foo_id], b) class TestCaseSqliteCache(BaseTestCaseCache): def get_cache(self, dbname=None): if dbname: return SqliteCache(dbname) else: return SqliteCache() def test_corrupted_db(self, tmpdir, monkeypatch): # If we don't do this monkeypatching, we get a weird exception about trying to flush a # closed file. I've tried setting logging level and stuff, but nothing worked. So, there we # go, a dirty monkeypatch. monkeypatch.setattr(logging, "warning", lambda *args, **kw: None) dbname = str(tmpdir.join("foo.db")) fp = open(dbname, "w") fp.write("invalid sqlite content") fp.close() c = self.get_cache(dbname) # should not raise a DatabaseError c["foo"] = [(1, 2, 3)] del c c = self.get_cache(dbname) eq_(c["foo"], [(1, 2, 3)]) class TestCaseShelveCache(BaseTestCaseCache): def get_cache(self, dbname=None): return ShelveCache(dbname) class TestCaseCacheSQLEscape: def get_cache(self): return SqliteCache() def test_contains(self): c = self.get_cache() assert "foo'bar" not in c def test_getitem(self): c = self.get_cache() with raises(KeyError): c["foo'bar"] def test_setitem(self): c = self.get_cache() c["foo'bar"] = [] def test_delitem(self): c = self.get_cache() c["foo'bar"] = [] try: del c["foo'bar"] except KeyError: assert False dupeguru-4.3.1/core/tests/conftest.py000066400000000000000000000000521426171743600176600ustar00rootroot00000000000000from hscommon.testutil import app # noqa dupeguru-4.3.1/core/tests/directories_test.py000066400000000000000000000515641426171743600214240ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import os import time import tempfile import shutil from pytest import raises from pathlib import Path from hscommon.testutil import eq_ from hscommon.plat import ISWINDOWS from core.fs import File from core.directories import ( Directories, DirectoryState, AlreadyThereError, InvalidPathError, ) from core.exclude import ExcludeList, ExcludeDict def create_fake_fs(rootpath): # We have it as a separate function because other units are using it. rootpath = rootpath.joinpath("fs") rootpath.mkdir() rootpath.joinpath("dir1").mkdir() rootpath.joinpath("dir2").mkdir() rootpath.joinpath("dir3").mkdir() with rootpath.joinpath("file1.test").open("wt") as fp: fp.write("1") with rootpath.joinpath("file2.test").open("wt") as fp: fp.write("12") with rootpath.joinpath("file3.test").open("wt") as fp: fp.write("123") with rootpath.joinpath("dir1", "file1.test").open("wt") as fp: fp.write("1") with rootpath.joinpath("dir2", "file2.test").open("wt") as fp: fp.write("12") with rootpath.joinpath("dir3", "file3.test").open("wt") as fp: fp.write("123") return rootpath testpath = None def setup_module(module): # In this unit, we have tests depending on two directory structure. One with only one file in it # and another with a more complex structure. testpath = Path(tempfile.mkdtemp()) module.testpath = testpath rootpath = testpath.joinpath("onefile") rootpath.mkdir() with rootpath.joinpath("test.txt").open("wt") as fp: fp.write("test_data") create_fake_fs(testpath) def teardown_module(module): shutil.rmtree(str(module.testpath)) def test_empty(): d = Directories() eq_(len(d), 0) assert "foobar" not in d def test_add_path(): d = Directories() p = testpath.joinpath("onefile") d.add_path(p) eq_(1, len(d)) assert p in d assert (p.joinpath("foobar")) in d assert p.parent not in d p = testpath.joinpath("fs") d.add_path(p) eq_(2, len(d)) assert p in d def test_add_path_when_path_is_already_there(): d = Directories() p = testpath.joinpath("onefile") d.add_path(p) with raises(AlreadyThereError): d.add_path(p) with raises(AlreadyThereError): d.add_path(p.joinpath("foobar")) eq_(1, len(d)) def test_add_path_containing_paths_already_there(): d = Directories() d.add_path(testpath.joinpath("onefile")) eq_(1, len(d)) d.add_path(testpath) eq_(len(d), 1) eq_(d[0], testpath) def test_add_path_non_latin(tmpdir): p = Path(str(tmpdir)) to_add = p.joinpath("unicode\u201a") os.mkdir(str(to_add)) d = Directories() try: d.add_path(to_add) except UnicodeDecodeError: assert False def test_del(): d = Directories() d.add_path(testpath.joinpath("onefile")) try: del d[1] assert False except IndexError: pass d.add_path(testpath.joinpath("fs")) del d[1] eq_(1, len(d)) def test_states(): d = Directories() p = testpath.joinpath("onefile") d.add_path(p) eq_(DirectoryState.NORMAL, d.get_state(p)) d.set_state(p, DirectoryState.REFERENCE) eq_(DirectoryState.REFERENCE, d.get_state(p)) eq_(DirectoryState.REFERENCE, d.get_state(p.joinpath("dir1"))) eq_(1, len(d.states)) eq_(p, list(d.states.keys())[0]) eq_(DirectoryState.REFERENCE, d.states[p]) def test_get_state_with_path_not_there(): # When the path's not there, just return DirectoryState.Normal d = Directories() d.add_path(testpath.joinpath("onefile")) eq_(d.get_state(testpath), DirectoryState.NORMAL) def test_states_overwritten_when_larger_directory_eat_smaller_ones(): # ref #248 # When setting the state of a folder, we overwrite previously set states for subfolders. d = Directories() p = testpath.joinpath("onefile") d.add_path(p) d.set_state(p, DirectoryState.EXCLUDED) d.add_path(testpath) d.set_state(testpath, DirectoryState.REFERENCE) eq_(d.get_state(p), DirectoryState.REFERENCE) eq_(d.get_state(p.joinpath("dir1")), DirectoryState.REFERENCE) eq_(d.get_state(testpath), DirectoryState.REFERENCE) def test_get_files(): d = Directories() p = testpath.joinpath("fs") d.add_path(p) d.set_state(p.joinpath("dir1"), DirectoryState.REFERENCE) d.set_state(p.joinpath("dir2"), DirectoryState.EXCLUDED) files = list(d.get_files()) eq_(5, len(files)) for f in files: if f.path.parent == p.joinpath("dir1"): assert f.is_ref else: assert not f.is_ref def test_get_files_with_folders(): # When fileclasses handle folders, return them and stop recursing! class FakeFile(File): @classmethod def can_handle(cls, path): return True d = Directories() p = testpath.joinpath("fs") d.add_path(p) files = list(d.get_files(fileclasses=[FakeFile])) # We have the 3 root files and the 3 root dirs eq_(6, len(files)) def test_get_folders(): d = Directories() p = testpath.joinpath("fs") d.add_path(p) d.set_state(p.joinpath("dir1"), DirectoryState.REFERENCE) d.set_state(p.joinpath("dir2"), DirectoryState.EXCLUDED) folders = list(d.get_folders()) eq_(len(folders), 3) ref = [f for f in folders if f.is_ref] not_ref = [f for f in folders if not f.is_ref] eq_(len(ref), 1) eq_(ref[0].path, p.joinpath("dir1")) eq_(len(not_ref), 2) eq_(ref[0].size, 1) def test_get_files_with_inherited_exclusion(): d = Directories() p = testpath.joinpath("onefile") d.add_path(p) d.set_state(p, DirectoryState.EXCLUDED) eq_([], list(d.get_files())) def test_save_and_load(tmpdir): d1 = Directories() d2 = Directories() p1 = Path(str(tmpdir.join("p1"))) p1.mkdir() p2 = Path(str(tmpdir.join("p2"))) p2.mkdir() d1.add_path(p1) d1.add_path(p2) d1.set_state(p1, DirectoryState.REFERENCE) d1.set_state(p1.joinpath("dir1"), DirectoryState.EXCLUDED) tmpxml = str(tmpdir.join("directories_testunit.xml")) d1.save_to_file(tmpxml) d2.load_from_file(tmpxml) eq_(2, len(d2)) eq_(DirectoryState.REFERENCE, d2.get_state(p1)) eq_(DirectoryState.EXCLUDED, d2.get_state(p1.joinpath("dir1"))) def test_invalid_path(): d = Directories() p = Path("does_not_exist") with raises(InvalidPathError): d.add_path(p) eq_(0, len(d)) def test_set_state_on_invalid_path(): d = Directories() try: d.set_state( Path( "foobar", ), DirectoryState.NORMAL, ) except LookupError: assert False def test_load_from_file_with_invalid_path(tmpdir): # This test simulates a load from file resulting in a # InvalidPath raise. Other directories must be loaded. d1 = Directories() d1.add_path(testpath.joinpath("onefile")) # Will raise InvalidPath upon loading p = Path(str(tmpdir.join("toremove"))) p.mkdir() d1.add_path(p) p.rmdir() tmpxml = str(tmpdir.join("directories_testunit.xml")) d1.save_to_file(tmpxml) d2 = Directories() d2.load_from_file(tmpxml) eq_(1, len(d2)) def test_unicode_save(tmpdir): d = Directories() p1 = Path(str(tmpdir), "hello\xe9") p1.mkdir() p1.joinpath("foo\xe9").mkdir() d.add_path(p1) d.set_state(p1.joinpath("foo\xe9"), DirectoryState.EXCLUDED) tmpxml = str(tmpdir.join("directories_testunit.xml")) try: d.save_to_file(tmpxml) except UnicodeDecodeError: assert False def test_get_files_refreshes_its_directories(): d = Directories() p = testpath.joinpath("fs") d.add_path(p) files = d.get_files() eq_(6, len(list(files))) time.sleep(1) os.remove(str(p.joinpath("dir1", "file1.test"))) files = d.get_files() eq_(5, len(list(files))) def test_get_files_does_not_choke_on_non_existing_directories(tmpdir): d = Directories() p = Path(str(tmpdir)) d.add_path(p) shutil.rmtree(str(p)) eq_([], list(d.get_files())) def test_get_state_returns_excluded_by_default_for_hidden_directories(tmpdir): d = Directories() p = Path(str(tmpdir)) hidden_dir_path = p.joinpath(".foo") p.joinpath(".foo").mkdir() d.add_path(p) eq_(d.get_state(hidden_dir_path), DirectoryState.EXCLUDED) # But it can be overriden d.set_state(hidden_dir_path, DirectoryState.NORMAL) eq_(d.get_state(hidden_dir_path), DirectoryState.NORMAL) def test_default_path_state_override(tmpdir): # It's possible for a subclass to override the default state of a path class MyDirectories(Directories): def _default_state_for_path(self, path): if "foobar" in path.parts: return DirectoryState.EXCLUDED d = MyDirectories() p1 = Path(str(tmpdir)) p1.joinpath("foobar").mkdir() p1.joinpath("foobar/somefile").touch() p1.joinpath("foobaz").mkdir() p1.joinpath("foobaz/somefile").touch() d.add_path(p1) eq_(d.get_state(p1.joinpath("foobaz")), DirectoryState.NORMAL) eq_(d.get_state(p1.joinpath("foobar")), DirectoryState.EXCLUDED) eq_(len(list(d.get_files())), 1) # only the 'foobaz' file is there # However, the default state can be changed d.set_state(p1.joinpath("foobar"), DirectoryState.NORMAL) eq_(d.get_state(p1.joinpath("foobar")), DirectoryState.NORMAL) eq_(len(list(d.get_files())), 2) class TestExcludeList: def setup_method(self, method): self.d = Directories(exclude_list=ExcludeList(union_regex=False)) def get_files_and_expect_num_result(self, num_result): """Calls get_files(), get the filenames only, print for debugging. num_result is how many files are expected as a result.""" print( f"EXCLUDED REGEX: paths {self.d._exclude_list.compiled_paths} \ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled}" ) files = list(self.d.get_files()) files = [file.name for file in files] print(f"FINAL FILES {files}") eq_(len(files), num_result) return files def test_exclude_recycle_bin_by_default(self, tmpdir): regex = r"^.*Recycle\.Bin$" self.d._exclude_list.add(regex) self.d._exclude_list.mark(regex) p1 = Path(str(tmpdir)) p1.joinpath("$Recycle.Bin").mkdir() p1.joinpath("$Recycle.Bin", "subdir").mkdir() self.d.add_path(p1) eq_(self.d.get_state(p1.joinpath("$Recycle.Bin")), DirectoryState.EXCLUDED) # By default, subdirs should be excluded too, but this can be overridden separately eq_(self.d.get_state(p1.joinpath("$Recycle.Bin", "subdir")), DirectoryState.EXCLUDED) self.d.set_state(p1.joinpath("$Recycle.Bin", "subdir"), DirectoryState.NORMAL) eq_(self.d.get_state(p1.joinpath("$Recycle.Bin", "subdir")), DirectoryState.NORMAL) def test_exclude_refined(self, tmpdir): regex1 = r"^\$Recycle\.Bin$" self.d._exclude_list.add(regex1) self.d._exclude_list.mark(regex1) p1 = Path(str(tmpdir)) p1.joinpath("$Recycle.Bin").mkdir() p1.joinpath("$Recycle.Bin", "somefile.png").touch() p1.joinpath("$Recycle.Bin", "some_unwanted_file.jpg").touch() p1.joinpath("$Recycle.Bin", "subdir").mkdir() p1.joinpath("$Recycle.Bin", "subdir", "somesubdirfile.png").touch() p1.joinpath("$Recycle.Bin", "subdir", "unwanted_subdirfile.gif").touch() p1.joinpath("$Recycle.Bin", "subdar").mkdir() p1.joinpath("$Recycle.Bin", "subdar", "somesubdarfile.jpeg").touch() p1.joinpath("$Recycle.Bin", "subdar", "unwanted_subdarfile.png").touch() self.d.add_path(p1.joinpath("$Recycle.Bin")) # Filter should set the default state to Excluded eq_(self.d.get_state(p1.joinpath("$Recycle.Bin")), DirectoryState.EXCLUDED) # The subdir should inherit its parent state eq_(self.d.get_state(p1.joinpath("$Recycle.Bin", "subdir")), DirectoryState.EXCLUDED) eq_(self.d.get_state(p1.joinpath("$Recycle.Bin", "subdar")), DirectoryState.EXCLUDED) # Override a child path's state self.d.set_state(p1.joinpath("$Recycle.Bin", "subdir"), DirectoryState.NORMAL) eq_(self.d.get_state(p1.joinpath("$Recycle.Bin", "subdir")), DirectoryState.NORMAL) # Parent should keep its default state, and the other child too eq_(self.d.get_state(p1.joinpath("$Recycle.Bin")), DirectoryState.EXCLUDED) eq_(self.d.get_state(p1.joinpath("$Recycle.Bin", "subdar")), DirectoryState.EXCLUDED) # print(f"get_folders(): {[x for x in self.d.get_folders()]}") # only the 2 files directly under the Normal directory files = self.get_files_and_expect_num_result(2) assert "somefile.png" not in files assert "some_unwanted_file.jpg" not in files assert "somesubdarfile.jpeg" not in files assert "unwanted_subdarfile.png" not in files assert "somesubdirfile.png" in files assert "unwanted_subdirfile.gif" in files # Overriding the parent should enable all children self.d.set_state(p1.joinpath("$Recycle.Bin"), DirectoryState.NORMAL) eq_(self.d.get_state(p1.joinpath("$Recycle.Bin", "subdar")), DirectoryState.NORMAL) # all files there files = self.get_files_and_expect_num_result(6) assert "somefile.png" in files assert "some_unwanted_file.jpg" in files # This should still filter out files under directory, despite the Normal state regex2 = r".*unwanted.*" self.d._exclude_list.add(regex2) self.d._exclude_list.mark(regex2) files = self.get_files_and_expect_num_result(3) assert "somefile.png" in files assert "some_unwanted_file.jpg" not in files assert "unwanted_subdirfile.gif" not in files assert "unwanted_subdarfile.png" not in files if ISWINDOWS: regex3 = r".*Recycle\.Bin\\.*unwanted.*subdirfile.*" else: regex3 = r".*Recycle\.Bin\/.*unwanted.*subdirfile.*" self.d._exclude_list.rename(regex2, regex3) assert self.d._exclude_list.error(regex3) is None # print(f"get_folders(): {[x for x in self.d.get_folders()]}") # Directory shouldn't change its state here, unless explicitely done by user eq_(self.d.get_state(p1.joinpath("$Recycle.Bin", "subdir")), DirectoryState.NORMAL) files = self.get_files_and_expect_num_result(5) assert "unwanted_subdirfile.gif" not in files assert "unwanted_subdarfile.png" in files # using end of line character should only filter the directory, or file ending with subdir regex4 = r".*subdir$" self.d._exclude_list.rename(regex3, regex4) assert self.d._exclude_list.error(regex4) is None p1.joinpath("$Recycle.Bin", "subdar", "file_ending_with_subdir").touch() eq_(self.d.get_state(p1.joinpath("$Recycle.Bin", "subdir")), DirectoryState.EXCLUDED) files = self.get_files_and_expect_num_result(4) assert "file_ending_with_subdir" not in files assert "somesubdarfile.jpeg" in files assert "somesubdirfile.png" not in files assert "unwanted_subdirfile.gif" not in files self.d.set_state(p1.joinpath("$Recycle.Bin", "subdir"), DirectoryState.NORMAL) eq_(self.d.get_state(p1.joinpath("$Recycle.Bin", "subdir")), DirectoryState.NORMAL) # print(f"get_folders(): {[x for x in self.d.get_folders()]}") files = self.get_files_and_expect_num_result(6) assert "file_ending_with_subdir" not in files assert "somesubdirfile.png" in files assert "unwanted_subdirfile.gif" in files regex5 = r".*subdir.*" self.d._exclude_list.rename(regex4, regex5) # Files containing substring should be filtered eq_(self.d.get_state(p1.joinpath("$Recycle.Bin", "subdir")), DirectoryState.NORMAL) # The path should not match, only the filename, the "subdir" in the directory name shouldn't matter p1.joinpath("$Recycle.Bin", "subdir", "file_which_shouldnt_match").touch() files = self.get_files_and_expect_num_result(5) assert "somesubdirfile.png" not in files assert "unwanted_subdirfile.gif" not in files assert "file_ending_with_subdir" not in files assert "file_which_shouldnt_match" in files # This should match the directory only regex6 = r".*/.*subdir.*/.*" if ISWINDOWS: regex6 = r".*\\.*subdir.*\\.*" assert os.sep in regex6 self.d._exclude_list.rename(regex5, regex6) self.d._exclude_list.remove(regex1) eq_(len(self.d._exclude_list.compiled), 1) assert regex1 not in self.d._exclude_list assert regex5 not in self.d._exclude_list assert self.d._exclude_list.error(regex6) is None assert regex6 in self.d._exclude_list # This still should not be affected eq_(self.d.get_state(p1.joinpath("$Recycle.Bin", "subdir")), DirectoryState.NORMAL) files = self.get_files_and_expect_num_result(5) # These files are under the "/subdir" directory assert "somesubdirfile.png" not in files assert "unwanted_subdirfile.gif" not in files # This file under "subdar" directory should not be filtered out assert "file_ending_with_subdir" in files # This file is in a directory that should be filtered out assert "file_which_shouldnt_match" not in files def test_japanese_unicode(self, tmpdir): p1 = Path(str(tmpdir)) p1.joinpath("$Recycle.Bin").mkdir() p1.joinpath("$Recycle.Bin", "somerecycledfile.png").touch() p1.joinpath("$Recycle.Bin", "some_unwanted_file.jpg").touch() p1.joinpath("$Recycle.Bin", "subdir").mkdir() p1.joinpath("$Recycle.Bin", "subdir", "過去白濁物語~]_カラー.jpg").touch() p1.joinpath("$Recycle.Bin", "思叫物語").mkdir() p1.joinpath("$Recycle.Bin", "思叫物語", "なししろ会う前").touch() p1.joinpath("$Recycle.Bin", "思叫物語", "堂~ロ").touch() self.d.add_path(p1.joinpath("$Recycle.Bin")) regex3 = r".*物語.*" self.d._exclude_list.add(regex3) self.d._exclude_list.mark(regex3) # print(f"get_folders(): {[x for x in self.d.get_folders()]}") eq_(self.d.get_state(p1.joinpath("$Recycle.Bin", "思叫物語")), DirectoryState.EXCLUDED) files = self.get_files_and_expect_num_result(2) assert "過去白濁物語~]_カラー.jpg" not in files assert "なししろ会う前" not in files assert "堂~ロ" not in files # using end of line character should only filter that directory, not affecting its files regex4 = r".*物語$" self.d._exclude_list.rename(regex3, regex4) assert self.d._exclude_list.error(regex4) is None self.d.set_state(p1.joinpath("$Recycle.Bin", "思叫物語"), DirectoryState.NORMAL) files = self.get_files_and_expect_num_result(5) assert "過去白濁物語~]_カラー.jpg" in files assert "なししろ会う前" in files assert "堂~ロ" in files def test_get_state_returns_excluded_for_hidden_directories_and_files(self, tmpdir): # This regex only work for files, not paths regex = r"^\..*$" self.d._exclude_list.add(regex) self.d._exclude_list.mark(regex) p1 = Path(str(tmpdir)) p1.joinpath("foobar").mkdir() p1.joinpath("foobar", ".hidden_file.txt").touch() p1.joinpath("foobar", ".hidden_dir").mkdir() p1.joinpath("foobar", ".hidden_dir", "foobar.jpg").touch() p1.joinpath("foobar", ".hidden_dir", ".hidden_subfile.png").touch() self.d.add_path(p1.joinpath("foobar")) # It should not inherit its parent's state originally eq_(self.d.get_state(p1.joinpath("foobar", ".hidden_dir")), DirectoryState.EXCLUDED) self.d.set_state(p1.joinpath("foobar", ".hidden_dir"), DirectoryState.NORMAL) # The files should still be filtered files = self.get_files_and_expect_num_result(1) eq_(len(self.d._exclude_list.compiled_paths), 0) eq_(len(self.d._exclude_list.compiled_files), 1) assert ".hidden_file.txt" not in files assert ".hidden_subfile.png" not in files assert "foobar.jpg" in files class TestExcludeDict(TestExcludeList): def setup_method(self, method): self.d = Directories(exclude_list=ExcludeDict(union_regex=False)) class TestExcludeListunion(TestExcludeList): def setup_method(self, method): self.d = Directories(exclude_list=ExcludeList(union_regex=True)) class TestExcludeDictunion(TestExcludeList): def setup_method(self, method): self.d = Directories(exclude_list=ExcludeDict(union_regex=True)) dupeguru-4.3.1/core/tests/engine_test.py000066400000000000000000000730311426171743600203460ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import sys from hscommon.jobprogress import job from hscommon.util import first from hscommon.testutil import eq_, log_calls from core.tests.base import NamedObject from core import engine from core.engine import ( get_match, getwords, Group, getfields, unpack_fields, compare_fields, compare, WEIGHT_WORDS, MATCH_SIMILAR_WORDS, NO_FIELD_ORDER, build_word_dict, get_groups, getmatches, Match, getmatches_by_contents, merge_similar_words, reduce_common_words, ) no = NamedObject def get_match_triangle(): o1 = NamedObject(with_words=True) o2 = NamedObject(with_words=True) o3 = NamedObject(with_words=True) m1 = get_match(o1, o2) m2 = get_match(o1, o3) m3 = get_match(o2, o3) return [m1, m2, m3] def get_test_group(): m1, m2, m3 = get_match_triangle() result = Group() result.add_match(m1) result.add_match(m2) result.add_match(m3) return result def assert_match(m, name1, name2): # When testing matches, whether objects are in first or second position very often doesn't # matter. This function makes this test more convenient. if m.first.name == name1: eq_(m.second.name, name2) else: eq_(m.first.name, name2) eq_(m.second.name, name1) class TestCasegetwords: def test_spaces(self): eq_(["a", "b", "c", "d"], getwords("a b c d")) eq_(["a", "b", "c", "d"], getwords(" a b c d ")) def test_unicode(self): eq_(["e", "c", "0", "a", "o", "u", "e", "u"], getwords("é ç 0 à ö û è ¤ ù")) eq_(["02", "君のこころは輝いてるかい?", "国木田花丸", "solo", "ver"], getwords("02 君のこころは輝いてるかい? 国木田花丸 Solo Ver")) def test_splitter_chars(self): eq_( [chr(i) for i in range(ord("a"), ord("z") + 1)], getwords("a-b_c&d+e(f)g;h\\i[j]k{l}m:n.o,pr/s?t~u!v@w#x$y*z"), ) def test_joiner_chars(self): eq_(["aec"], getwords("a'e\u0301c")) def test_empty(self): eq_([], getwords("")) def test_returns_lowercase(self): eq_(["foo", "bar"], getwords("FOO BAR")) def test_decompose_unicode(self): eq_(["fooebar"], getwords("foo\xe9bar")) class TestCasegetfields: def test_simple(self): eq_([["a", "b"], ["c", "d", "e"]], getfields("a b - c d e")) def test_empty(self): eq_([], getfields("")) def test_cleans_empty_fields(self): expected = [["a", "bc", "def"]] actual = getfields(" - a bc def") eq_(expected, actual) class TestCaseUnpackFields: def test_with_fields(self): expected = ["a", "b", "c", "d", "e", "f"] actual = unpack_fields([["a"], ["b", "c"], ["d", "e", "f"]]) eq_(expected, actual) def test_without_fields(self): expected = ["a", "b", "c", "d", "e", "f"] actual = unpack_fields(["a", "b", "c", "d", "e", "f"]) eq_(expected, actual) def test_empty(self): eq_([], unpack_fields([])) class TestCaseWordCompare: def test_list(self): eq_(100, compare(["a", "b", "c", "d"], ["a", "b", "c", "d"])) eq_(86, compare(["a", "b", "c", "d"], ["a", "b", "c"])) def test_unordered(self): # Sometimes, users don't want fuzzy matching too much When they set the slider # to 100, they don't expect a filename with the same words, but not the same order, to match. # Thus, we want to return 99 in that case. eq_(99, compare(["a", "b", "c", "d"], ["d", "b", "c", "a"])) def test_word_occurs_twice(self): # if a word occurs twice in first, but once in second, we want the word to be only counted once eq_(89, compare(["a", "b", "c", "d", "a"], ["d", "b", "c", "a"])) def test_uses_copy_of_lists(self): first = ["foo", "bar"] second = ["bar", "bleh"] compare(first, second) eq_(["foo", "bar"], first) eq_(["bar", "bleh"], second) def test_word_weight(self): eq_( int((6.0 / 13.0) * 100), compare(["foo", "bar"], ["bar", "bleh"], (WEIGHT_WORDS,)), ) def test_similar_words(self): eq_( 100, compare( ["the", "white", "stripes"], ["the", "whites", "stripe"], (MATCH_SIMILAR_WORDS,), ), ) def test_empty(self): eq_(0, compare([], [])) def test_with_fields(self): eq_(67, compare([["a", "b"], ["c", "d", "e"]], [["a", "b"], ["c", "d", "f"]])) def test_propagate_flags_with_fields(self, monkeypatch): def mock_compare(first, second, flags): eq_((0, 1, 2, 3, 5), flags) monkeypatch.setattr(engine, "compare_fields", mock_compare) compare([["a"]], [["a"]], (0, 1, 2, 3, 5)) class TestCaseWordCompareWithFields: def test_simple(self): eq_( 67, compare_fields([["a", "b"], ["c", "d", "e"]], [["a", "b"], ["c", "d", "f"]]), ) def test_empty(self): eq_(0, compare_fields([], [])) def test_different_length(self): eq_(0, compare_fields([["a"], ["b"]], [["a"], ["b"], ["c"]])) def test_propagates_flags(self, monkeypatch): def mock_compare(first, second, flags): eq_((0, 1, 2, 3, 5), flags) monkeypatch.setattr(engine, "compare_fields", mock_compare) compare_fields([["a"]], [["a"]], (0, 1, 2, 3, 5)) def test_order(self): first = [["a", "b"], ["c", "d", "e"]] second = [["c", "d", "f"], ["a", "b"]] eq_(0, compare_fields(first, second)) def test_no_order(self): first = [["a", "b"], ["c", "d", "e"]] second = [["c", "d", "f"], ["a", "b"]] eq_(67, compare_fields(first, second, (NO_FIELD_ORDER,))) first = [["a", "b"], ["a", "b"]] # a field can only be matched once. second = [["c", "d", "f"], ["a", "b"]] eq_(0, compare_fields(first, second, (NO_FIELD_ORDER,))) first = [["a", "b"], ["a", "b", "c"]] second = [["c", "d", "f"], ["a", "b"]] eq_(33, compare_fields(first, second, (NO_FIELD_ORDER,))) def test_compare_fields_without_order_doesnt_alter_fields(self): # The NO_ORDER comp type altered the fields! first = [["a", "b"], ["c", "d", "e"]] second = [["c", "d", "f"], ["a", "b"]] eq_(67, compare_fields(first, second, (NO_FIELD_ORDER,))) eq_([["a", "b"], ["c", "d", "e"]], first) eq_([["c", "d", "f"], ["a", "b"]], second) class TestCaseBuildWordDict: def test_with_standard_words(self): item_list = [NamedObject("foo bar", True)] item_list.append(NamedObject("bar baz", True)) item_list.append(NamedObject("baz bleh foo", True)) d = build_word_dict(item_list) eq_(4, len(d)) eq_(2, len(d["foo"])) assert item_list[0] in d["foo"] assert item_list[2] in d["foo"] eq_(2, len(d["bar"])) assert item_list[0] in d["bar"] assert item_list[1] in d["bar"] eq_(2, len(d["baz"])) assert item_list[1] in d["baz"] assert item_list[2] in d["baz"] eq_(1, len(d["bleh"])) assert item_list[2] in d["bleh"] def test_unpack_fields(self): o = NamedObject("") o.words = [["foo", "bar"], ["baz"]] d = build_word_dict([o]) eq_(3, len(d)) eq_(1, len(d["foo"])) def test_words_are_unaltered(self): o = NamedObject("") o.words = [["foo", "bar"], ["baz"]] build_word_dict([o]) eq_([["foo", "bar"], ["baz"]], o.words) def test_object_instances_can_only_be_once_in_words_object_list(self): o = NamedObject("foo foo", True) d = build_word_dict([o]) eq_(1, len(d["foo"])) def test_job(self): def do_progress(p, d=""): self.log.append(p) return True j = job.Job(1, do_progress) self.log = [] s = "foo bar" build_word_dict([NamedObject(s, True), NamedObject(s, True), NamedObject(s, True)], j) # We don't have intermediate log because iter_with_progress is called with every > 1 eq_(0, self.log[0]) eq_(100, self.log[1]) class TestCaseMergeSimilarWords: def test_some_similar_words(self): d = { "foobar": {1}, "foobar1": {2}, "foobar2": {3}, } merge_similar_words(d) eq_(1, len(d)) eq_(3, len(d["foobar"])) class TestCaseReduceCommonWords: def test_typical(self): d = { "foo": {NamedObject("foo bar", True) for _ in range(50)}, "bar": {NamedObject("foo bar", True) for _ in range(49)}, } reduce_common_words(d, 50) assert "foo" not in d eq_(49, len(d["bar"])) def test_dont_remove_objects_with_only_common_words(self): d = { "common": set([NamedObject("common uncommon", True) for _ in range(50)] + [NamedObject("common", True)]), "uncommon": {NamedObject("common uncommon", True)}, } reduce_common_words(d, 50) eq_(1, len(d["common"])) eq_(1, len(d["uncommon"])) def test_values_still_are_set_instances(self): d = { "common": set([NamedObject("common uncommon", True) for _ in range(50)] + [NamedObject("common", True)]), "uncommon": {NamedObject("common uncommon", True)}, } reduce_common_words(d, 50) assert isinstance(d["common"], set) assert isinstance(d["uncommon"], set) def test_dont_raise_keyerror_when_a_word_has_been_removed(self): # If a word has been removed by the reduce, an object in a subsequent common word that # contains the word that has been removed would cause a KeyError. d = { "foo": {NamedObject("foo bar baz", True) for _ in range(50)}, "bar": {NamedObject("foo bar baz", True) for _ in range(50)}, "baz": {NamedObject("foo bar baz", True) for _ in range(49)}, } try: reduce_common_words(d, 50) except KeyError: self.fail() def test_unpack_fields(self): # object.words may be fields. def create_it(): o = NamedObject("") o.words = [["foo", "bar"], ["baz"]] return o d = {"foo": {create_it() for _ in range(50)}} try: reduce_common_words(d, 50) except TypeError: self.fail("must support fields.") def test_consider_a_reduced_common_word_common_even_after_reduction(self): # There was a bug in the code that causeda word that has already been reduced not to # be counted as a common word for subsequent words. For example, if 'foo' is processed # as a common word, keeping a "foo bar" file in it, and the 'bar' is processed, "foo bar" # would not stay in 'bar' because 'foo' is not a common word anymore. only_common = NamedObject("foo bar", True) d = { "foo": set([NamedObject("foo bar baz", True) for _ in range(49)] + [only_common]), "bar": set([NamedObject("foo bar baz", True) for _ in range(49)] + [only_common]), "baz": {NamedObject("foo bar baz", True) for _ in range(49)}, } reduce_common_words(d, 50) eq_(1, len(d["foo"])) eq_(1, len(d["bar"])) eq_(49, len(d["baz"])) class TestCaseGetMatch: def test_simple(self): o1 = NamedObject("foo bar", True) o2 = NamedObject("bar bleh", True) m = get_match(o1, o2) eq_(50, m.percentage) eq_(["foo", "bar"], m.first.words) eq_(["bar", "bleh"], m.second.words) assert m.first is o1 assert m.second is o2 def test_in(self): o1 = NamedObject("foo", True) o2 = NamedObject("bar", True) m = get_match(o1, o2) assert o1 in m assert o2 in m assert object() not in m def test_word_weight(self): m = get_match(NamedObject("foo bar", True), NamedObject("bar bleh", True), (WEIGHT_WORDS,)) eq_(m.percentage, int((6.0 / 13.0) * 100)) class TestCaseGetMatches: def test_empty(self): eq_(getmatches([]), []) def test_simple(self): item_list = [ NamedObject("foo bar"), NamedObject("bar bleh"), NamedObject("a b c foo"), ] r = getmatches(item_list) eq_(2, len(r)) m = first(m for m in r if m.percentage == 50) # "foo bar" and "bar bleh" assert_match(m, "foo bar", "bar bleh") m = first(m for m in r if m.percentage == 33) # "foo bar" and "a b c foo" assert_match(m, "foo bar", "a b c foo") def test_null_and_unrelated_objects(self): item_list = [ NamedObject("foo bar"), NamedObject("bar bleh"), NamedObject(""), NamedObject("unrelated object"), ] r = getmatches(item_list) eq_(len(r), 1) m = r[0] eq_(m.percentage, 50) assert_match(m, "foo bar", "bar bleh") def test_twice_the_same_word(self): item_list = [NamedObject("foo foo bar"), NamedObject("bar bleh")] r = getmatches(item_list) eq_(1, len(r)) def test_twice_the_same_word_when_preworded(self): item_list = [NamedObject("foo foo bar", True), NamedObject("bar bleh", True)] r = getmatches(item_list) eq_(1, len(r)) def test_two_words_match(self): item_list = [NamedObject("foo bar"), NamedObject("foo bar bleh")] r = getmatches(item_list) eq_(1, len(r)) def test_match_files_with_only_common_words(self): # If a word occurs more than 50 times, it is excluded from the matching process # The problem with the common_word_threshold is that the files containing only common # words will never be matched together. We *should* match them. # This test assumes that the common word threshold const is 50 item_list = [NamedObject("foo") for _ in range(50)] r = getmatches(item_list) eq_(1225, len(r)) def test_use_words_already_there_if_there(self): o1 = NamedObject("foo") o2 = NamedObject("bar") o2.words = ["foo"] eq_(1, len(getmatches([o1, o2]))) def test_job(self): def do_progress(p, d=""): self.log.append(p) return True j = job.Job(1, do_progress) self.log = [] s = "foo bar" getmatches([NamedObject(s), NamedObject(s), NamedObject(s)], j=j) assert len(self.log) > 2 eq_(0, self.log[0]) eq_(100, self.log[-1]) def test_weight_words(self): item_list = [NamedObject("foo bar"), NamedObject("bar bleh")] m = getmatches(item_list, weight_words=True)[0] eq_(int((6.0 / 13.0) * 100), m.percentage) def test_similar_word(self): item_list = [NamedObject("foobar"), NamedObject("foobars")] eq_(len(getmatches(item_list, match_similar_words=True)), 1) eq_(getmatches(item_list, match_similar_words=True)[0].percentage, 100) item_list = [NamedObject("foobar"), NamedObject("foo")] eq_(len(getmatches(item_list, match_similar_words=True)), 0) # too far item_list = [NamedObject("bizkit"), NamedObject("bizket")] eq_(len(getmatches(item_list, match_similar_words=True)), 1) item_list = [NamedObject("foobar"), NamedObject("foosbar")] eq_(len(getmatches(item_list, match_similar_words=True)), 1) def test_single_object_with_similar_words(self): item_list = [NamedObject("foo foos")] eq_(len(getmatches(item_list, match_similar_words=True)), 0) def test_double_words_get_counted_only_once(self): item_list = [NamedObject("foo bar foo bleh"), NamedObject("foo bar bleh bar")] m = getmatches(item_list)[0] eq_(75, m.percentage) def test_with_fields(self): o1 = NamedObject("foo bar - foo bleh") o2 = NamedObject("foo bar - bleh bar") o1.words = getfields(o1.name) o2.words = getfields(o2.name) m = getmatches([o1, o2])[0] eq_(50, m.percentage) def test_with_fields_no_order(self): o1 = NamedObject("foo bar - foo bleh") o2 = NamedObject("bleh bang - foo bar") o1.words = getfields(o1.name) o2.words = getfields(o2.name) m = getmatches([o1, o2], no_field_order=True)[0] eq_(m.percentage, 50) def test_only_match_similar_when_the_option_is_set(self): item_list = [NamedObject("foobar"), NamedObject("foobars")] eq_(len(getmatches(item_list, match_similar_words=False)), 0) def test_dont_recurse_do_match(self): # with nosetests, the stack is increased. The number has to be high enough not to be failing falsely sys.setrecursionlimit(200) files = [NamedObject("foo bar") for _ in range(201)] try: getmatches(files) except RuntimeError: self.fail() finally: sys.setrecursionlimit(1000) def test_min_match_percentage(self): item_list = [ NamedObject("foo bar"), NamedObject("bar bleh"), NamedObject("a b c foo"), ] r = getmatches(item_list, min_match_percentage=50) eq_(1, len(r)) # Only "foo bar" / "bar bleh" should match def test_memory_error(self, monkeypatch): @log_calls def mocked_match(first, second, flags): if len(mocked_match.calls) > 42: raise MemoryError() return Match(first, second, 0) objects = [NamedObject() for _ in range(10)] # results in 45 matches monkeypatch.setattr(engine, "get_match", mocked_match) try: r = getmatches(objects) except MemoryError: self.fail("MemoryError must be handled") eq_(42, len(r)) class TestCaseGetMatchesByContents: def test_big_file_partial_hashing(self): smallsize = 1 bigsize = 100 * 1024 * 1024 # 100MB f = [ no("bigfoo", size=bigsize), no("bigbar", size=bigsize), no("smallfoo", size=smallsize), no("smallbar", size=smallsize), ] f[0].digest = f[0].digest_partial = f[0].digest_samples = "foobar" f[1].digest = f[1].digest_partial = f[1].digest_samples = "foobar" f[2].digest = f[2].digest_partial = "bleh" f[3].digest = f[3].digest_partial = "bleh" r = getmatches_by_contents(f, bigsize=bigsize) eq_(len(r), 2) # User disabled optimization for big files, compute digests as usual r = getmatches_by_contents(f, bigsize=0) eq_(len(r), 2) # Other file is now slightly different, digest_partial is still the same f[1].digest = f[1].digest_samples = "foobardiff" r = getmatches_by_contents(f, bigsize=bigsize) # Successfully filter it out eq_(len(r), 1) r = getmatches_by_contents(f, bigsize=0) eq_(len(r), 1) class TestCaseGroup: def test_empty(self): g = Group() eq_(None, g.ref) eq_([], g.dupes) eq_(0, len(g.matches)) def test_add_match(self): g = Group() m = get_match(NamedObject("foo", True), NamedObject("bar", True)) g.add_match(m) assert g.ref is m.first eq_([m.second], g.dupes) eq_(1, len(g.matches)) assert m in g.matches def test_multiple_add_match(self): g = Group() o1 = NamedObject("a", True) o2 = NamedObject("b", True) o3 = NamedObject("c", True) o4 = NamedObject("d", True) g.add_match(get_match(o1, o2)) assert g.ref is o1 eq_([o2], g.dupes) eq_(1, len(g.matches)) g.add_match(get_match(o1, o3)) eq_([o2], g.dupes) eq_(2, len(g.matches)) g.add_match(get_match(o2, o3)) eq_([o2, o3], g.dupes) eq_(3, len(g.matches)) g.add_match(get_match(o1, o4)) eq_([o2, o3], g.dupes) eq_(4, len(g.matches)) g.add_match(get_match(o2, o4)) eq_([o2, o3], g.dupes) eq_(5, len(g.matches)) g.add_match(get_match(o3, o4)) eq_([o2, o3, o4], g.dupes) eq_(6, len(g.matches)) def test_len(self): g = Group() eq_(0, len(g)) g.add_match(get_match(NamedObject("foo", True), NamedObject("bar", True))) eq_(2, len(g)) def test_add_same_match_twice(self): g = Group() m = get_match(NamedObject("foo", True), NamedObject("foo", True)) g.add_match(m) eq_(2, len(g)) eq_(1, len(g.matches)) g.add_match(m) eq_(2, len(g)) eq_(1, len(g.matches)) def test_in(self): g = Group() o1 = NamedObject("foo", True) o2 = NamedObject("bar", True) assert o1 not in g g.add_match(get_match(o1, o2)) assert o1 in g assert o2 in g def test_remove(self): g = Group() o1 = NamedObject("foo", True) o2 = NamedObject("bar", True) o3 = NamedObject("bleh", True) g.add_match(get_match(o1, o2)) g.add_match(get_match(o1, o3)) g.add_match(get_match(o2, o3)) eq_(3, len(g.matches)) eq_(3, len(g)) g.remove_dupe(o3) eq_(1, len(g.matches)) eq_(2, len(g)) g.remove_dupe(o1) eq_(0, len(g.matches)) eq_(0, len(g)) def test_remove_with_ref_dupes(self): g = Group() o1 = NamedObject("foo", True) o2 = NamedObject("bar", True) o3 = NamedObject("bleh", True) g.add_match(get_match(o1, o2)) g.add_match(get_match(o1, o3)) g.add_match(get_match(o2, o3)) o1.is_ref = True o2.is_ref = True g.remove_dupe(o3) eq_(0, len(g)) def test_switch_ref(self): o1 = NamedObject(with_words=True) o2 = NamedObject(with_words=True) g = Group() g.add_match(get_match(o1, o2)) assert o1 is g.ref g.switch_ref(o2) assert o2 is g.ref eq_([o1], g.dupes) g.switch_ref(o2) assert o2 is g.ref g.switch_ref(NamedObject("", True)) assert o2 is g.ref def test_switch_ref_from_ref_dir(self): # When the ref dupe is from a ref dir, switch_ref() does nothing o1 = no(with_words=True) o2 = no(with_words=True) o1.is_ref = True g = Group() g.add_match(get_match(o1, o2)) g.switch_ref(o2) assert o1 is g.ref def test_get_match_of(self): g = Group() for m in get_match_triangle(): g.add_match(m) o = g.dupes[0] m = g.get_match_of(o) assert g.ref in m assert o in m assert g.get_match_of(NamedObject("", True)) is None assert g.get_match_of(g.ref) is None def test_percentage(self): # percentage should return the avg percentage in relation to the ref m1, m2, m3 = get_match_triangle() m1 = Match(m1[0], m1[1], 100) m2 = Match(m2[0], m2[1], 50) m3 = Match(m3[0], m3[1], 33) g = Group() g.add_match(m1) g.add_match(m2) g.add_match(m3) eq_(75, g.percentage) g.switch_ref(g.dupes[0]) eq_(66, g.percentage) g.remove_dupe(g.dupes[0]) eq_(33, g.percentage) g.add_match(m1) g.add_match(m2) eq_(66, g.percentage) def test_percentage_on_empty_group(self): g = Group() eq_(0, g.percentage) def test_prioritize(self): m1, m2, m3 = get_match_triangle() o1 = m1.first o2 = m1.second o3 = m2.second o1.name = "c" o2.name = "b" o3.name = "a" g = Group() g.add_match(m1) g.add_match(m2) g.add_match(m3) assert o1 is g.ref assert g.prioritize(lambda x: x.name) assert o3 is g.ref def test_prioritize_with_tie_breaker(self): # if the ref has the same key as one or more of the dupe, run the tie_breaker func among them g = get_test_group() o1, o2, o3 = g.ordered g.prioritize(lambda x: 0, lambda ref, dupe: dupe is o3) assert g.ref is o3 def test_prioritize_with_tie_breaker_runs_on_all_dupes(self): # Even if a dupe is chosen to switch with ref with a tie breaker, we still run the tie breaker # with other dupes and the newly chosen ref g = get_test_group() o1, o2, o3 = g.ordered o1.foo = 1 o2.foo = 2 o3.foo = 3 g.prioritize(lambda x: 0, lambda ref, dupe: dupe.foo > ref.foo) assert g.ref is o3 def test_prioritize_with_tie_breaker_runs_only_on_tie_dupes(self): # The tie breaker only runs on dupes that had the same value for the key_func g = get_test_group() o1, o2, o3 = g.ordered o1.foo = 2 o2.foo = 2 o3.foo = 1 o1.bar = 1 o2.bar = 2 o3.bar = 3 g.prioritize(lambda x: -x.foo, lambda ref, dupe: dupe.bar > ref.bar) assert g.ref is o2 def test_prioritize_with_ref_dupe(self): # when the ref dupe of a group is from a ref dir, make it stay on top. g = get_test_group() o1, o2, o3 = g o1.is_ref = True o2.size = 2 g.prioritize(lambda x: -x.size) assert g.ref is o1 def test_prioritize_nothing_changes(self): # prioritize() returns False when nothing changes in the group. g = get_test_group() g[0].name = "a" g[1].name = "b" g[2].name = "c" assert not g.prioritize(lambda x: x.name) def test_list_like(self): g = Group() o1, o2 = (NamedObject("foo", True), NamedObject("bar", True)) g.add_match(get_match(o1, o2)) assert g[0] is o1 assert g[1] is o2 def test_discard_matches(self): g = Group() o1, o2, o3 = ( NamedObject("foo", True), NamedObject("bar", True), NamedObject("baz", True), ) g.add_match(get_match(o1, o2)) g.add_match(get_match(o1, o3)) g.discard_matches() eq_(1, len(g.matches)) eq_(0, len(g.candidates)) class TestCaseGetGroups: def test_empty(self): r = get_groups([]) eq_([], r) def test_simple(self): item_list = [NamedObject("foo bar"), NamedObject("bar bleh")] matches = getmatches(item_list) m = matches[0] r = get_groups(matches) eq_(1, len(r)) g = r[0] assert g.ref is m.first eq_([m.second], g.dupes) def test_group_with_multiple_matches(self): # This results in 3 matches item_list = [NamedObject("foo"), NamedObject("foo"), NamedObject("foo")] matches = getmatches(item_list) r = get_groups(matches) eq_(1, len(r)) g = r[0] eq_(3, len(g)) def test_must_choose_a_group(self): item_list = [ NamedObject("a b"), NamedObject("a b"), NamedObject("b c"), NamedObject("c d"), NamedObject("c d"), ] # There will be 2 groups here: group "a b" and group "c d" # "b c" can go either of them, but not both. matches = getmatches(item_list) r = get_groups(matches) eq_(2, len(r)) eq_(5, len(r[0]) + len(r[1])) def test_should_all_go_in_the_same_group(self): item_list = [ NamedObject("a b"), NamedObject("a b"), NamedObject("a b"), NamedObject("a b"), ] # There will be 2 groups here: group "a b" and group "c d" # "b c" can fit in both, but it must be in only one of them matches = getmatches(item_list) r = get_groups(matches) eq_(1, len(r)) def test_give_priority_to_matches_with_higher_percentage(self): o1 = NamedObject(with_words=True) o2 = NamedObject(with_words=True) o3 = NamedObject(with_words=True) m1 = Match(o1, o2, 1) m2 = Match(o2, o3, 2) r = get_groups([m1, m2]) eq_(1, len(r)) g = r[0] eq_(2, len(g)) assert o1 not in g assert o2 in g assert o3 in g def test_four_sized_group(self): item_list = [NamedObject("foobar") for _ in range(4)] m = getmatches(item_list) r = get_groups(m) eq_(1, len(r)) eq_(4, len(r[0])) def test_referenced_by_ref2(self): o1 = NamedObject(with_words=True) o2 = NamedObject(with_words=True) o3 = NamedObject(with_words=True) m1 = get_match(o1, o2) m2 = get_match(o3, o1) m3 = get_match(o3, o2) r = get_groups([m1, m2, m3]) eq_(3, len(r[0])) def test_group_admissible_discarded_dupes(self): # If, with a (A, B, C, D) set, all match with A, but C and D don't match with B and that the # (A, B) match is the highest (thus resulting in an (A, B) group), still match C and D # in a separate group instead of discarding them. A, B, C, D = (NamedObject() for _ in range(4)) m1 = Match(A, B, 90) # This is the strongest "A" match m2 = Match(A, C, 80) # Because C doesn't match with B, it won't be in the group m3 = Match(A, D, 80) # Same thing for D m4 = Match(C, D, 70) # However, because C and D match, they should have their own group. groups = get_groups([m1, m2, m3, m4]) eq_(len(groups), 2) g1, g2 = groups assert A in g1 assert B in g1 assert C in g2 assert D in g2 dupeguru-4.3.1/core/tests/exclude_test.py000066400000000000000000000425131426171743600205330ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import io from xml.etree import ElementTree as ET from hscommon.testutil import eq_ from hscommon.plat import ISWINDOWS from core.tests.base import DupeGuru from core.exclude import ExcludeList, ExcludeDict, default_regexes, AlreadyThereException from re import error # Two slightly different implementations here, one around a list of lists, # and another around a dictionary. class TestCaseListXMLLoading: def setup_method(self, method): self.exclude_list = ExcludeList() def test_load_non_existant_file(self): # Loads the pre-defined regexes self.exclude_list.load_from_xml("non_existant.xml") eq_(len(default_regexes), len(self.exclude_list)) # they should also be marked by default eq_(len(default_regexes), self.exclude_list.marked_count) def test_save_to_xml(self): f = io.BytesIO() self.exclude_list.save_to_xml(f) f.seek(0) doc = ET.parse(f) root = doc.getroot() eq_("exclude_list", root.tag) def test_save_and_load(self, tmpdir): e1 = ExcludeList() e2 = ExcludeList() eq_(len(e1), 0) e1.add(r"one") e1.mark(r"one") e1.add(r"two") tmpxml = str(tmpdir.join("exclude_testunit.xml")) e1.save_to_xml(tmpxml) e2.load_from_xml(tmpxml) # We should have the default regexes assert r"one" in e2 assert r"two" in e2 eq_(len(e2), 2) eq_(e2.marked_count, 1) def test_load_xml_with_garbage_and_missing_elements(self): root = ET.Element("foobar") # The root element shouldn't matter exclude_node = ET.SubElement(root, "bogus") exclude_node.set("regex", "None") exclude_node.set("marked", "y") exclude_node = ET.SubElement(root, "exclude") exclude_node.set("regex", "one") # marked field invalid exclude_node.set("markedddd", "y") exclude_node = ET.SubElement(root, "exclude") exclude_node.set("regex", "two") # missing marked field exclude_node = ET.SubElement(root, "exclude") exclude_node.set("regex", "three") exclude_node.set("markedddd", "pazjbjepo") f = io.BytesIO() tree = ET.ElementTree(root) tree.write(f, encoding="utf-8") f.seek(0) self.exclude_list.load_from_xml(f) print(f"{[x for x in self.exclude_list]}") # only the two "exclude" nodes should be added, eq_(3, len(self.exclude_list)) # None should be marked eq_(0, self.exclude_list.marked_count) class TestCaseDictXMLLoading(TestCaseListXMLLoading): def setup_method(self, method): self.exclude_list = ExcludeDict() class TestCaseListEmpty: def setup_method(self, method): self.app = DupeGuru() self.app.exclude_list = ExcludeList(union_regex=False) self.exclude_list = self.app.exclude_list def test_add_mark_and_remove_regex(self): regex1 = r"one" regex2 = r"two" self.exclude_list.add(regex1) assert regex1 in self.exclude_list self.exclude_list.add(regex2) self.exclude_list.mark(regex1) self.exclude_list.mark(regex2) eq_(len(self.exclude_list), 2) eq_(len(self.exclude_list.compiled), 2) compiled_files = [x for x in self.exclude_list.compiled_files] eq_(len(compiled_files), 2) self.exclude_list.remove(regex2) assert regex2 not in self.exclude_list eq_(len(self.exclude_list), 1) def test_add_duplicate(self): self.exclude_list.add(r"one") eq_(1, len(self.exclude_list)) try: self.exclude_list.add(r"one") except Exception: pass eq_(1, len(self.exclude_list)) def test_add_not_compilable(self): # Trying to add a non-valid regex should not work and raise exception regex = r"one))" try: self.exclude_list.add(regex) except Exception as e: # Make sure we raise a re.error so that the interface can process it eq_(type(e), error) added = self.exclude_list.mark(regex) eq_(added, False) eq_(len(self.exclude_list), 0) eq_(len(self.exclude_list.compiled), 0) compiled_files = [x for x in self.exclude_list.compiled_files] eq_(len(compiled_files), 0) def test_force_add_not_compilable(self): """Used when loading from XML for example""" regex = r"one))" self.exclude_list.add(regex, forced=True) marked = self.exclude_list.mark(regex) eq_(marked, False) # can't be marked since not compilable eq_(len(self.exclude_list), 1) eq_(len(self.exclude_list.compiled), 0) compiled_files = [x for x in self.exclude_list.compiled_files] eq_(len(compiled_files), 0) # adding a duplicate regex = r"one))" try: self.exclude_list.add(regex, forced=True) except Exception as e: # we should have this exception, and it shouldn't be added assert type(e) is AlreadyThereException eq_(len(self.exclude_list), 1) eq_(len(self.exclude_list.compiled), 0) def test_rename_regex(self): regex = r"one" self.exclude_list.add(regex) self.exclude_list.mark(regex) regex_renamed = r"one))" # Not compilable, can't be marked self.exclude_list.rename(regex, regex_renamed) assert regex not in self.exclude_list assert regex_renamed in self.exclude_list eq_(self.exclude_list.is_marked(regex_renamed), False) self.exclude_list.mark(regex_renamed) eq_(self.exclude_list.is_marked(regex_renamed), False) regex_renamed_compilable = r"two" self.exclude_list.rename(regex_renamed, regex_renamed_compilable) assert regex_renamed_compilable in self.exclude_list eq_(self.exclude_list.is_marked(regex_renamed), False) self.exclude_list.mark(regex_renamed_compilable) eq_(self.exclude_list.is_marked(regex_renamed_compilable), True) eq_(len(self.exclude_list), 1) # Should still be marked after rename regex_compilable = r"three" self.exclude_list.rename(regex_renamed_compilable, regex_compilable) eq_(self.exclude_list.is_marked(regex_compilable), True) def test_rename_regex_file_to_path(self): regex = r".*/one.*" if ISWINDOWS: regex = r".*\\one.*" regex2 = r".*one.*" self.exclude_list.add(regex) self.exclude_list.mark(regex) compiled_re = [x.pattern for x in self.exclude_list._excluded_compiled] files_re = [x.pattern for x in self.exclude_list.compiled_files] paths_re = [x.pattern for x in self.exclude_list.compiled_paths] assert regex in compiled_re assert regex not in files_re assert regex in paths_re self.exclude_list.rename(regex, regex2) compiled_re = [x.pattern for x in self.exclude_list._excluded_compiled] files_re = [x.pattern for x in self.exclude_list.compiled_files] paths_re = [x.pattern for x in self.exclude_list.compiled_paths] assert regex not in compiled_re assert regex2 in compiled_re assert regex2 in files_re assert regex2 not in paths_re def test_restore_default(self): """Only unmark previously added regexes and mark the pre-defined ones""" regex = r"one" self.exclude_list.add(regex) self.exclude_list.mark(regex) self.exclude_list.restore_defaults() eq_(len(default_regexes), self.exclude_list.marked_count) # added regex shouldn't be marked eq_(self.exclude_list.is_marked(regex), False) # added regex shouldn't be in compiled list either compiled = [x for x in self.exclude_list.compiled] assert regex not in compiled # Only default regexes marked and in compiled list for re in default_regexes: assert self.exclude_list.is_marked(re) found = False for compiled_re in compiled: if compiled_re.pattern == re: found = True if not found: raise (Exception(f"Default RE {re} not found in compiled list.")) eq_(len(default_regexes), len(self.exclude_list.compiled)) class TestCaseListEmptyUnion(TestCaseListEmpty): """Same but with union regex""" def setup_method(self, method): self.app = DupeGuru() self.app.exclude_list = ExcludeList(union_regex=True) self.exclude_list = self.app.exclude_list def test_add_mark_and_remove_regex(self): regex1 = r"one" regex2 = r"two" self.exclude_list.add(regex1) assert regex1 in self.exclude_list self.exclude_list.add(regex2) self.exclude_list.mark(regex1) self.exclude_list.mark(regex2) eq_(len(self.exclude_list), 2) eq_(len(self.exclude_list.compiled), 1) compiled_files = [x for x in self.exclude_list.compiled_files] eq_(len(compiled_files), 1) # Two patterns joined together into one assert "|" in compiled_files[0].pattern self.exclude_list.remove(regex2) assert regex2 not in self.exclude_list eq_(len(self.exclude_list), 1) def test_rename_regex_file_to_path(self): regex = r".*/one.*" if ISWINDOWS: regex = r".*\\one.*" regex2 = r".*one.*" self.exclude_list.add(regex) self.exclude_list.mark(regex) eq_(len([x for x in self.exclude_list]), 1) compiled_re = [x.pattern for x in self.exclude_list.compiled] files_re = [x.pattern for x in self.exclude_list.compiled_files] paths_re = [x.pattern for x in self.exclude_list.compiled_paths] assert regex in compiled_re assert regex not in files_re assert regex in paths_re self.exclude_list.rename(regex, regex2) eq_(len([x for x in self.exclude_list]), 1) compiled_re = [x.pattern for x in self.exclude_list.compiled] files_re = [x.pattern for x in self.exclude_list.compiled_files] paths_re = [x.pattern for x in self.exclude_list.compiled_paths] assert regex not in compiled_re assert regex2 in compiled_re assert regex2 in files_re assert regex2 not in paths_re def test_restore_default(self): """Only unmark previously added regexes and mark the pre-defined ones""" regex = r"one" self.exclude_list.add(regex) self.exclude_list.mark(regex) self.exclude_list.restore_defaults() eq_(len(default_regexes), self.exclude_list.marked_count) # added regex shouldn't be marked eq_(self.exclude_list.is_marked(regex), False) # added regex shouldn't be in compiled list either compiled = [x for x in self.exclude_list.compiled] assert regex not in compiled # Need to escape both to get the same strings after compilation compiled_escaped = {x.encode("unicode-escape").decode() for x in compiled[0].pattern.split("|")} default_escaped = {x.encode("unicode-escape").decode() for x in default_regexes} assert compiled_escaped == default_escaped eq_(len(default_regexes), len(compiled[0].pattern.split("|"))) class TestCaseDictEmpty(TestCaseListEmpty): """Same, but with dictionary implementation""" def setup_method(self, method): self.app = DupeGuru() self.app.exclude_list = ExcludeDict(union_regex=False) self.exclude_list = self.app.exclude_list class TestCaseDictEmptyUnion(TestCaseDictEmpty): """Same, but with union regex""" def setup_method(self, method): self.app = DupeGuru() self.app.exclude_list = ExcludeDict(union_regex=True) self.exclude_list = self.app.exclude_list def test_add_mark_and_remove_regex(self): regex1 = r"one" regex2 = r"two" self.exclude_list.add(regex1) assert regex1 in self.exclude_list self.exclude_list.add(regex2) self.exclude_list.mark(regex1) self.exclude_list.mark(regex2) eq_(len(self.exclude_list), 2) eq_(len(self.exclude_list.compiled), 1) compiled_files = [x for x in self.exclude_list.compiled_files] # two patterns joined into one eq_(len(compiled_files), 1) self.exclude_list.remove(regex2) assert regex2 not in self.exclude_list eq_(len(self.exclude_list), 1) def test_rename_regex_file_to_path(self): regex = r".*/one.*" if ISWINDOWS: regex = r".*\\one.*" regex2 = r".*one.*" self.exclude_list.add(regex) self.exclude_list.mark(regex) marked_re = [x for marked, x in self.exclude_list if marked] eq_(len(marked_re), 1) compiled_re = [x.pattern for x in self.exclude_list.compiled] files_re = [x.pattern for x in self.exclude_list.compiled_files] paths_re = [x.pattern for x in self.exclude_list.compiled_paths] assert regex in compiled_re assert regex not in files_re assert regex in paths_re self.exclude_list.rename(regex, regex2) compiled_re = [x.pattern for x in self.exclude_list.compiled] files_re = [x.pattern for x in self.exclude_list.compiled_files] paths_re = [x.pattern for x in self.exclude_list.compiled_paths] assert regex not in compiled_re assert regex2 in compiled_re assert regex2 in files_re assert regex2 not in paths_re def test_restore_default(self): """Only unmark previously added regexes and mark the pre-defined ones""" regex = r"one" self.exclude_list.add(regex) self.exclude_list.mark(regex) self.exclude_list.restore_defaults() eq_(len(default_regexes), self.exclude_list.marked_count) # added regex shouldn't be marked eq_(self.exclude_list.is_marked(regex), False) # added regex shouldn't be in compiled list either compiled = [x for x in self.exclude_list.compiled] assert regex not in compiled # Need to escape both to get the same strings after compilation compiled_escaped = {x.encode("unicode-escape").decode() for x in compiled[0].pattern.split("|")} default_escaped = {x.encode("unicode-escape").decode() for x in default_regexes} assert compiled_escaped == default_escaped eq_(len(default_regexes), len(compiled[0].pattern.split("|"))) def split_union(pattern_object): """Returns list of strings for each union pattern""" return [x for x in pattern_object.pattern.split("|")] class TestCaseCompiledList: """Test consistency between union or and separate versions.""" def setup_method(self, method): self.e_separate = ExcludeList(union_regex=False) self.e_separate.restore_defaults() self.e_union = ExcludeList(union_regex=True) self.e_union.restore_defaults() def test_same_number_of_expressions(self): # We only get one union Pattern item in a tuple, which is made of however many parts eq_(len(split_union(self.e_union.compiled[0])), len(default_regexes)) # We get as many as there are marked items eq_(len(self.e_separate.compiled), len(default_regexes)) exprs = split_union(self.e_union.compiled[0]) # We should have the same number and the same expressions eq_(len(exprs), len(self.e_separate.compiled)) for expr in self.e_separate.compiled: assert expr.pattern in exprs def test_compiled_files(self): # is path separator checked properly to yield the output if ISWINDOWS: regex1 = r"test\\one\\sub" else: regex1 = r"test/one/sub" self.e_separate.add(regex1) self.e_separate.mark(regex1) self.e_union.add(regex1) self.e_union.mark(regex1) separate_compiled_dirs = self.e_separate.compiled separate_compiled_files = [x for x in self.e_separate.compiled_files] # HACK we need to call compiled property FIRST to generate the cache union_compiled_dirs = self.e_union.compiled # print(f"type: {type(self.e_union.compiled_files[0])}") # A generator returning only one item... ugh union_compiled_files = [x for x in self.e_union.compiled_files][0] print(f"compiled files: {union_compiled_files}") # Separate should give several plus the one added eq_(len(separate_compiled_dirs), len(default_regexes) + 1) # regex1 shouldn't be in the "files" version eq_(len(separate_compiled_files), len(default_regexes)) # Only one Pattern returned, which when split should be however many + 1 eq_(len(split_union(union_compiled_dirs[0])), len(default_regexes) + 1) # regex1 shouldn't be here either eq_(len(split_union(union_compiled_files)), len(default_regexes)) class TestCaseCompiledDict(TestCaseCompiledList): """Test the dictionary version""" def setup_method(self, method): self.e_separate = ExcludeDict(union_regex=False) self.e_separate.restore_defaults() self.e_union = ExcludeDict(union_regex=True) self.e_union.restore_defaults() dupeguru-4.3.1/core/tests/fs_test.py000066400000000000000000000107741426171743600175160ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2009-10-23 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import typing from os import urandom from pathlib import Path from hscommon.testutil import eq_ from core.tests.directories_test import create_fake_fs from core import fs hasher: typing.Callable try: import xxhash hasher = xxhash.xxh128 except ImportError: import hashlib hasher = hashlib.md5 def create_fake_fs_with_random_data(rootpath): rootpath = rootpath.joinpath("fs") rootpath.mkdir() rootpath.joinpath("dir1").mkdir() rootpath.joinpath("dir2").mkdir() rootpath.joinpath("dir3").mkdir() data1 = urandom(200 * 1024) # 200KiB data2 = urandom(1024 * 1024) # 1MiB data3 = urandom(10 * 1024 * 1024) # 10MiB with rootpath.joinpath("file1.test").open("wb") as fp: fp.write(data1) with rootpath.joinpath("file2.test").open("wb") as fp: fp.write(data2) with rootpath.joinpath("file3.test").open("wb") as fp: fp.write(data3) with rootpath.joinpath("dir1", "file1.test").open("wb") as fp: fp.write(data1) with rootpath.joinpath("dir2", "file2.test").open("wb") as fp: fp.write(data2) with rootpath.joinpath("dir3", "file3.test").open("wb") as fp: fp.write(data3) return rootpath def test_size_aggregates_subfiles(tmpdir): p = create_fake_fs(Path(str(tmpdir))) b = fs.Folder(p) eq_(b.size, 12) def test_digest_aggregate_subfiles_sorted(tmpdir): # dir.allfiles can return child in any order. Thus, bundle.digest must aggregate # all files' digests it contains, but it must make sure that it does so in the # same order everytime. p = create_fake_fs_with_random_data(Path(str(tmpdir))) b = fs.Folder(p) digest1 = fs.File(p.joinpath("dir1", "file1.test")).digest digest2 = fs.File(p.joinpath("dir2", "file2.test")).digest digest3 = fs.File(p.joinpath("dir3", "file3.test")).digest digest4 = fs.File(p.joinpath("file1.test")).digest digest5 = fs.File(p.joinpath("file2.test")).digest digest6 = fs.File(p.joinpath("file3.test")).digest # The expected digest is the hash of digests for folders and the direct digest for files folder_digest1 = hasher(digest1).digest() folder_digest2 = hasher(digest2).digest() folder_digest3 = hasher(digest3).digest() digest = hasher(folder_digest1 + folder_digest2 + folder_digest3 + digest4 + digest5 + digest6).digest() eq_(b.digest, digest) def test_partial_digest_aggregate_subfile_sorted(tmpdir): p = create_fake_fs_with_random_data(Path(str(tmpdir))) b = fs.Folder(p) digest1 = fs.File(p.joinpath("dir1", "file1.test")).digest_partial digest2 = fs.File(p.joinpath("dir2", "file2.test")).digest_partial digest3 = fs.File(p.joinpath("dir3", "file3.test")).digest_partial digest4 = fs.File(p.joinpath("file1.test")).digest_partial digest5 = fs.File(p.joinpath("file2.test")).digest_partial digest6 = fs.File(p.joinpath("file3.test")).digest_partial # The expected digest is the hash of digests for folders and the direct digest for files folder_digest1 = hasher(digest1).digest() folder_digest2 = hasher(digest2).digest() folder_digest3 = hasher(digest3).digest() digest = hasher(folder_digest1 + folder_digest2 + folder_digest3 + digest4 + digest5 + digest6).digest() eq_(b.digest_partial, digest) digest1 = fs.File(p.joinpath("dir1", "file1.test")).digest_samples digest2 = fs.File(p.joinpath("dir2", "file2.test")).digest_samples digest3 = fs.File(p.joinpath("dir3", "file3.test")).digest_samples digest4 = fs.File(p.joinpath("file1.test")).digest_samples digest5 = fs.File(p.joinpath("file2.test")).digest_samples digest6 = fs.File(p.joinpath("file3.test")).digest_samples # The expected digest is the digest of digests for folders and the direct digest for files folder_digest1 = hasher(digest1).digest() folder_digest2 = hasher(digest2).digest() folder_digest3 = hasher(digest3).digest() digest = hasher(folder_digest1 + folder_digest2 + folder_digest3 + digest4 + digest5 + digest6).digest() eq_(b.digest_samples, digest) def test_has_file_attrs(tmpdir): # a Folder must behave like a file, so it must have mtime attributes b = fs.Folder(Path(str(tmpdir))) assert b.mtime > 0 eq_(b.extension, "") dupeguru-4.3.1/core/tests/ignore_test.py000066400000000000000000000103771426171743600203700ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import io from xml.etree import ElementTree as ET from pytest import raises from hscommon.testutil import eq_ from core.ignore import IgnoreList def test_empty(): il = IgnoreList() eq_(0, len(il)) assert not il.are_ignored("foo", "bar") def test_simple(): il = IgnoreList() il.ignore("foo", "bar") assert il.are_ignored("foo", "bar") assert il.are_ignored("bar", "foo") assert not il.are_ignored("foo", "bleh") assert not il.are_ignored("bleh", "bar") eq_(1, len(il)) def test_multiple(): il = IgnoreList() il.ignore("foo", "bar") il.ignore("foo", "bleh") il.ignore("bleh", "bar") il.ignore("aybabtu", "bleh") assert il.are_ignored("foo", "bar") assert il.are_ignored("bar", "foo") assert il.are_ignored("foo", "bleh") assert il.are_ignored("bleh", "bar") assert not il.are_ignored("aybabtu", "bar") eq_(4, len(il)) def test_clear(): il = IgnoreList() il.ignore("foo", "bar") il.clear() assert not il.are_ignored("foo", "bar") assert not il.are_ignored("bar", "foo") eq_(0, len(il)) def test_add_same_twice(): il = IgnoreList() il.ignore("foo", "bar") il.ignore("bar", "foo") eq_(1, len(il)) def test_save_to_xml(): il = IgnoreList() il.ignore("foo", "bar") il.ignore("foo", "bleh") il.ignore("bleh", "bar") f = io.BytesIO() il.save_to_xml(f) f.seek(0) doc = ET.parse(f) root = doc.getroot() eq_(root.tag, "ignore_list") eq_(len(root), 2) eq_(len([c for c in root if c.tag == "file"]), 2) f1, f2 = root[:] subchildren = [c for c in f1 if c.tag == "file"] + [c for c in f2 if c.tag == "file"] eq_(len(subchildren), 3) def test_save_then_load(): il = IgnoreList() il.ignore("foo", "bar") il.ignore("foo", "bleh") il.ignore("bleh", "bar") il.ignore("\u00e9", "bar") f = io.BytesIO() il.save_to_xml(f) f.seek(0) il = IgnoreList() il.load_from_xml(f) eq_(4, len(il)) assert il.are_ignored("\u00e9", "bar") def test_load_xml_with_empty_file_tags(): f = io.BytesIO() f.write(b'') f.seek(0) il = IgnoreList() il.load_from_xml(f) eq_(0, len(il)) def test_are_ignore_works_when_a_child_is_a_key_somewhere_else(): il = IgnoreList() il.ignore("foo", "bar") il.ignore("bar", "baz") assert il.are_ignored("bar", "foo") def test_no_dupes_when_a_child_is_a_key_somewhere_else(): il = IgnoreList() il.ignore("foo", "bar") il.ignore("bar", "baz") il.ignore("bar", "foo") eq_(2, len(il)) def test_iterate(): # It must be possible to iterate through ignore list il = IgnoreList() expected = [("foo", "bar"), ("bar", "baz"), ("foo", "baz")] for i in expected: il.ignore(i[0], i[1]) for i in il: expected.remove(i) # No exception should be raised assert not expected # expected should be empty def test_filter(): il = IgnoreList() il.ignore("foo", "bar") il.ignore("bar", "baz") il.ignore("foo", "baz") il.filter(lambda f, s: f == "bar") eq_(1, len(il)) assert not il.are_ignored("foo", "bar") assert il.are_ignored("bar", "baz") def test_save_with_non_ascii_items(): il = IgnoreList() il.ignore("\xac", "\xbf") f = io.BytesIO() try: il.save_to_xml(f) except Exception as e: raise AssertionError(str(e)) def test_len(): il = IgnoreList() eq_(0, len(il)) il.ignore("foo", "bar") eq_(1, len(il)) def test_nonzero(): il = IgnoreList() assert not il il.ignore("foo", "bar") assert il def test_remove(): il = IgnoreList() il.ignore("foo", "bar") il.ignore("foo", "baz") il.remove("bar", "foo") eq_(len(il), 1) assert not il.are_ignored("foo", "bar") def test_remove_non_existant(): il = IgnoreList() il.ignore("foo", "bar") il.ignore("foo", "baz") with raises(ValueError): il.remove("foo", "bleh") dupeguru-4.3.1/core/tests/markable_test.py000066400000000000000000000071511426171743600206570ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.testutil import eq_ from core.markable import MarkableList, Markable def gen(): ml = MarkableList() ml.extend(list(range(10))) return ml def test_unmarked(): ml = gen() for i in ml: assert not ml.is_marked(i) def test_mark(): ml = gen() assert ml.mark(3) assert ml.is_marked(3) assert not ml.is_marked(2) def test_unmark(): ml = gen() ml.mark(4) assert ml.unmark(4) assert not ml.is_marked(4) def test_unmark_unmarked(): ml = gen() assert not ml.unmark(4) assert not ml.is_marked(4) def test_mark_twice_and_unmark(): ml = gen() assert ml.mark(5) assert not ml.mark(5) ml.unmark(5) assert not ml.is_marked(5) def test_mark_toggle(): ml = gen() ml.mark_toggle(6) assert ml.is_marked(6) ml.mark_toggle(6) assert not ml.is_marked(6) ml.mark_toggle(6) assert ml.is_marked(6) def test_is_markable(): class Foobar(Markable): def _is_markable(self, o): return o == "foobar" f = Foobar() assert not f.is_marked("foobar") assert not f.mark("foo") assert not f.is_marked("foo") f.mark_toggle("foo") assert not f.is_marked("foo") f.mark("foobar") assert f.is_marked("foobar") ml = gen() ml.mark(11) assert not ml.is_marked(11) def test_change_notifications(): class Foobar(Markable): def _did_mark(self, o): self.log.append((True, o)) def _did_unmark(self, o): self.log.append((False, o)) f = Foobar() f.log = [] f.mark("foo") f.mark("foo") f.mark_toggle("bar") f.unmark("foo") f.unmark("foo") f.mark_toggle("bar") eq_([(True, "foo"), (True, "bar"), (False, "foo"), (False, "bar")], f.log) def test_mark_count(): ml = gen() eq_(0, ml.mark_count) ml.mark(7) eq_(1, ml.mark_count) ml.mark(11) eq_(1, ml.mark_count) def test_mark_none(): log = [] ml = gen() ml._did_unmark = lambda o: log.append(o) ml.mark(1) ml.mark(2) eq_(2, ml.mark_count) ml.mark_none() eq_(0, ml.mark_count) eq_([1, 2], log) def test_mark_all(): ml = gen() eq_(0, ml.mark_count) ml.mark_all() eq_(10, ml.mark_count) assert ml.is_marked(1) def test_mark_invert(): ml = gen() ml.mark(1) ml.mark_invert() assert not ml.is_marked(1) assert ml.is_marked(2) def test_mark_while_inverted(): log = [] ml = gen() ml._did_unmark = lambda o: log.append((False, o)) ml._did_mark = lambda o: log.append((True, o)) ml.mark(1) ml.mark_invert() assert ml.mark_inverted assert ml.mark(1) assert ml.unmark(2) assert ml.unmark(1) ml.mark_toggle(3) assert not ml.is_marked(3) eq_(7, ml.mark_count) eq_([(True, 1), (False, 1), (True, 2), (True, 1), (True, 3)], log) def test_remove_mark_flag(): ml = gen() ml.mark(1) ml._remove_mark_flag(1) assert not ml.is_marked(1) ml.mark(1) ml.mark_invert() assert not ml.is_marked(1) ml._remove_mark_flag(1) assert ml.is_marked(1) def test_is_marked_returns_false_if_object_not_markable(): class MyMarkableList(MarkableList): def _is_markable(self, o): return o != 4 ml = MyMarkableList() ml.extend(list(range(10))) ml.mark_invert() assert ml.is_marked(1) assert not ml.is_marked(4) dupeguru-4.3.1/core/tests/prioritize_test.py000066400000000000000000000142461426171743600213040ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2011/09/07 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import os.path as op from itertools import combinations from core.tests.base import TestApp, NamedObject, with_app, eq_ from core.engine import Group, Match no = NamedObject def app_with_dupes(dupes): # Creates an app with specified dupes. dupes is a list of lists, each list in the list being # a dupe group. We cheat a little bit by creating dupe groups manually instead of running a # dupe scan, but it simplifies the test code quite a bit app = TestApp() groups = [] for dupelist in dupes: g = Group() for dupe1, dupe2 in combinations(dupelist, 2): g.add_match(Match(dupe1, dupe2, 100)) groups.append(g) app.app.results.groups = groups app.app._results_changed() return app # --- def app_normal_results(): # Just some results, with different extensions and size, for good measure. dupes = [ [ no("foo1.ext1", size=1, folder="folder1"), no("foo2.ext2", size=2, folder="folder2"), ], ] return app_with_dupes(dupes) @with_app(app_normal_results) def test_kind_subcrit(app): # The subcriteria of the "Kind" criteria is a list of extensions contained in the dupes. app.select_pri_criterion("Kind") eq_(app.pdialog.criteria_list[:], ["ext1", "ext2"]) @with_app(app_normal_results) def test_kind_reprioritization(app): # Just a simple test of the system as a whole. # select a criterion, and perform re-prioritization and see if it worked. app.select_pri_criterion("Kind") app.pdialog.criteria_list.select([1]) # ext2 app.pdialog.add_selected() app.pdialog.perform_reprioritization() eq_(app.rtable[0].data["name"], "foo2.ext2") @with_app(app_normal_results) def test_folder_subcrit(app): app.select_pri_criterion("Folder") eq_(app.pdialog.criteria_list[:], ["folder1", "folder2"]) @with_app(app_normal_results) def test_folder_reprioritization(app): app.select_pri_criterion("Folder") app.pdialog.criteria_list.select([1]) # folder2 app.pdialog.add_selected() app.pdialog.perform_reprioritization() eq_(app.rtable[0].data["name"], "foo2.ext2") @with_app(app_normal_results) def test_prilist_display(app): # The prioritization list displays selected criteria correctly. app.select_pri_criterion("Kind") app.pdialog.criteria_list.select([1]) # ext2 app.pdialog.add_selected() app.select_pri_criterion("Folder") app.pdialog.criteria_list.select([1]) # folder2 app.pdialog.add_selected() app.select_pri_criterion("Size") app.pdialog.criteria_list.select([1]) # Lowest app.pdialog.add_selected() expected = [ "Kind (ext2)", "Folder (folder2)", "Size (Lowest)", ] eq_(app.pdialog.prioritization_list[:], expected) @with_app(app_normal_results) def test_size_subcrit(app): app.select_pri_criterion("Size") eq_(app.pdialog.criteria_list[:], ["Highest", "Lowest"]) @with_app(app_normal_results) def test_size_reprioritization(app): app.select_pri_criterion("Size") app.pdialog.criteria_list.select([0]) # highest app.pdialog.add_selected() app.pdialog.perform_reprioritization() eq_(app.rtable[0].data["name"], "foo2.ext2") @with_app(app_normal_results) def test_reorder_prioritizations(app): app.add_pri_criterion("Kind", 0) # ext1 app.add_pri_criterion("Kind", 1) # ext2 app.pdialog.prioritization_list.move_indexes([1], 0) expected = [ "Kind (ext2)", "Kind (ext1)", ] eq_(app.pdialog.prioritization_list[:], expected) @with_app(app_normal_results) def test_remove_crit_from_list(app): app.add_pri_criterion("Kind", 0) app.add_pri_criterion("Kind", 1) app.pdialog.prioritization_list.select(0) app.pdialog.remove_selected() expected = [ "Kind (ext2)", ] eq_(app.pdialog.prioritization_list[:], expected) @with_app(app_normal_results) def test_add_crit_without_selection(app): # Adding a criterion without having made a selection doesn't cause a crash. app.pdialog.add_selected() # no crash # --- def app_one_name_ends_with_number(): dupes = [ [no("foo.ext"), no("foo1.ext")], ] return app_with_dupes(dupes) @with_app(app_one_name_ends_with_number) def test_filename_reprioritization(app): app.add_pri_criterion("Filename", 0) # Ends with a number app.pdialog.perform_reprioritization() eq_(app.rtable[0].data["name"], "foo1.ext") # --- def app_with_subfolders(): dupes = [ [no("foo1", folder="baz"), no("foo2", folder="foo/bar")], [no("foo3", folder="baz"), no("foo4", folder="foo")], ] return app_with_dupes(dupes) @with_app(app_with_subfolders) def test_folder_crit_is_sorted(app): # Folder subcriteria are sorted. app.select_pri_criterion("Folder") eq_(app.pdialog.criteria_list[:], ["baz", "foo", op.join("foo", "bar")]) @with_app(app_with_subfolders) def test_folder_crit_includes_subfolders(app): # When selecting a folder crit, dupes in a subfolder are also considered as affected by that # crit. app.add_pri_criterion("Folder", 1) # foo app.pdialog.perform_reprioritization() # Both foo and foo/bar dupes will be prioritized eq_(app.rtable[0].data["name"], "foo2") eq_(app.rtable[2].data["name"], "foo4") @with_app(app_with_subfolders) def test_display_something_on_empty_extensions(app): # When there's no extension, display "None" instead of nothing at all. app.select_pri_criterion("Kind") eq_(app.pdialog.criteria_list[:], ["None"]) # --- def app_one_name_longer_than_the_other(): dupes = [ [no("shortest.ext"), no("loooongest.ext")], ] return app_with_dupes(dupes) @with_app(app_one_name_longer_than_the_other) def test_longest_filename_prioritization(app): app.add_pri_criterion("Filename", 2) # Longest app.pdialog.perform_reprioritization() eq_(app.rtable[0].data["name"], "loooongest.ext") dupeguru-4.3.1/core/tests/result_table_test.py000066400000000000000000000042651426171743600215710ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2013-07-28 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from core.tests.base import TestApp, GetTestGroups def app_with_results(): app = TestApp() objects, matches, groups = GetTestGroups() app.app.results.groups = groups app.rtable.refresh() return app def test_delta_flags_delta_mode_off(): app = app_with_results() # When the delta mode is off, we never have delta values flags app.rtable.delta_values = False # Ref file, always false anyway assert not app.rtable[0].is_cell_delta("size") # False because delta mode is off assert not app.rtable[1].is_cell_delta("size") def test_delta_flags_delta_mode_on_delta_columns(): # When the delta mode is on, delta columns always have a delta flag, except for ref rows app = app_with_results() app.rtable.delta_values = True # Ref file, always false anyway assert not app.rtable[0].is_cell_delta("size") # But for a dupe, the flag is on assert app.rtable[1].is_cell_delta("size") def test_delta_flags_delta_mode_on_non_delta_columns(): # When the delta mode is on, non-delta columns have a delta flag if their value differs from # their ref. app = app_with_results() app.rtable.delta_values = True # "bar bleh" != "foo bar", flag on assert app.rtable[1].is_cell_delta("name") # "ibabtu" row, but it's a ref, flag off assert not app.rtable[3].is_cell_delta("name") # "ibabtu" == "ibabtu", flag off assert not app.rtable[4].is_cell_delta("name") def test_delta_flags_delta_mode_on_non_delta_columns_case_insensitive(): # Comparison that occurs for non-numeric columns to check whether they're delta is case # insensitive app = app_with_results() app.app.results.groups[1].ref.name = "ibAbtu" app.app.results.groups[1].dupes[0].name = "IBaBTU" app.rtable.delta_values = True # "ibAbtu" == "IBaBTU", flag off assert not app.rtable[4].is_cell_delta("name") dupeguru-4.3.1/core/tests/results_test.py000066400000000000000000000756771426171743600206240ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import io import os.path as op from xml.etree import ElementTree as ET from pytest import raises from hscommon.testutil import eq_ from hscommon.util import first from core import engine from core.tests.base import NamedObject, GetTestGroups, DupeGuru from core.results import Results class TestCaseResultsEmpty: def setup_method(self, method): self.app = DupeGuru() self.results = self.app.results def test_apply_invalid_filter(self): # If the applied filter is an invalid regexp, just ignore the filter. self.results.apply_filter("[") # invalid self.test_stat_line() # make sure that the stats line isn't saying we applied a '[' filter def test_stat_line(self): eq_("0 / 0 (0.00 B / 0.00 B) duplicates marked.", self.results.stat_line) def test_groups(self): eq_(0, len(self.results.groups)) def test_get_group_of_duplicate(self): assert self.results.get_group_of_duplicate("foo") is None def test_save_to_xml(self): f = io.BytesIO() self.results.save_to_xml(f) f.seek(0) doc = ET.parse(f) root = doc.getroot() eq_("results", root.tag) def test_is_modified(self): assert not self.results.is_modified def test_is_modified_after_setting_empty_group(self): # Don't consider results as modified if they're empty self.results.groups = [] assert not self.results.is_modified def test_save_to_same_name_as_folder(self, tmpdir): # Issue #149 # When saving to a filename that already exists, the file is overwritten. However, when # the name exists but that it's a folder, then there used to be a crash. The proper fix # would have been some kind of feedback to the user, but the work involved for something # that simply never happens (I never received a report of this crash, I experienced it # while fooling around) is too much. Instead, use standard name conflict resolution. folderpath = tmpdir.join("foo") folderpath.mkdir() self.results.save_to_xml(str(folderpath)) # no crash assert tmpdir.join("[000] foo").check() class TestCaseResultsWithSomeGroups: def setup_method(self, method): self.app = DupeGuru() self.results = self.app.results self.objects, self.matches, self.groups = GetTestGroups() self.results.groups = self.groups def test_stat_line(self): eq_("0 / 3 (0.00 B / 1.01 KB) duplicates marked.", self.results.stat_line) def test_groups(self): eq_(2, len(self.results.groups)) def test_get_group_of_duplicate(self): for o in self.objects: g = self.results.get_group_of_duplicate(o) assert isinstance(g, engine.Group) assert o in g assert self.results.get_group_of_duplicate(self.groups[0]) is None def test_remove_duplicates(self): g1, g2 = self.results.groups self.results.remove_duplicates([g1.dupes[0]]) eq_(2, len(g1)) assert g1 in self.results.groups self.results.remove_duplicates([g1.ref]) eq_(2, len(g1)) assert g1 in self.results.groups self.results.remove_duplicates([g1.dupes[0]]) eq_(0, len(g1)) assert g1 not in self.results.groups self.results.remove_duplicates([g2.dupes[0]]) eq_(0, len(g2)) assert g2 not in self.results.groups eq_(0, len(self.results.groups)) def test_remove_duplicates_with_ref_files(self): g1, g2 = self.results.groups self.objects[0].is_ref = True self.objects[1].is_ref = True self.results.remove_duplicates([self.objects[2]]) eq_(0, len(g1)) assert g1 not in self.results.groups def test_make_ref(self): g = self.results.groups[0] d = g.dupes[0] self.results.make_ref(d) assert d is g.ref def test_sort_groups(self): self.results.make_ref(self.objects[1]) # We want to make the 1024 sized object to go ref. g1, g2 = self.groups self.results.sort_groups("size") assert self.results.groups[0] is g2 assert self.results.groups[1] is g1 self.results.sort_groups("size", False) assert self.results.groups[0] is g1 assert self.results.groups[1] is g2 def test_set_groups_when_sorted(self): self.results.make_ref(self.objects[1]) # We want to make the 1024 sized object to go ref. self.results.sort_groups("size") objects, matches, groups = GetTestGroups() g1, g2 = groups g1.switch_ref(objects[1]) self.results.groups = groups assert self.results.groups[0] is g2 assert self.results.groups[1] is g1 def test_get_dupe_list(self): eq_([self.objects[1], self.objects[2], self.objects[4]], self.results.dupes) def test_dupe_list_is_cached(self): assert self.results.dupes is self.results.dupes def test_dupe_list_cache_is_invalidated_when_needed(self): o1, o2, o3, o4, o5 = self.objects eq_([o2, o3, o5], self.results.dupes) self.results.make_ref(o2) eq_([o1, o3, o5], self.results.dupes) objects, matches, groups = GetTestGroups() o1, o2, o3, o4, o5 = objects self.results.groups = groups eq_([o2, o3, o5], self.results.dupes) def test_dupe_list_sort(self): o1, o2, o3, o4, o5 = self.objects o1.size = 5 o2.size = 4 o3.size = 3 o4.size = 2 o5.size = 1 self.results.sort_dupes("size") eq_([o5, o3, o2], self.results.dupes) self.results.sort_dupes("size", False) eq_([o2, o3, o5], self.results.dupes) def test_dupe_list_remember_sort(self): o1, o2, o3, o4, o5 = self.objects o1.size = 5 o2.size = 4 o3.size = 3 o4.size = 2 o5.size = 1 self.results.sort_dupes("size") self.results.make_ref(o2) eq_([o5, o3, o1], self.results.dupes) def test_dupe_list_sort_delta_values(self): o1, o2, o3, o4, o5 = self.objects o1.size = 10 o2.size = 2 # -8 o3.size = 3 # -7 o4.size = 20 o5.size = 1 # -19 self.results.sort_dupes("size", delta=True) eq_([o5, o2, o3], self.results.dupes) def test_sort_empty_list(self): # There was an infinite loop when sorting an empty list. app = DupeGuru() r = app.results r.sort_dupes("name") eq_([], r.dupes) def test_dupe_list_update_on_remove_duplicates(self): o1, o2, o3, o4, o5 = self.objects eq_(3, len(self.results.dupes)) self.results.remove_duplicates([o2]) eq_(2, len(self.results.dupes)) def test_is_modified(self): # Changing the groups sets the modified flag assert self.results.is_modified def test_is_modified_after_save_and_load(self): # Saving/Loading a file sets the modified flag back to False def get_file(path): return [f for f in self.objects if str(f.path) == path][0] f = io.BytesIO() self.results.save_to_xml(f) assert not self.results.is_modified self.results.groups = self.groups # sets the flag back f.seek(0) self.results.load_from_xml(f, get_file) assert not self.results.is_modified def test_is_modified_after_removing_all_results(self): # Removing all results sets the is_modified flag to false. self.results.mark_all() self.results.perform_on_marked(lambda x: None, True) assert not self.results.is_modified def test_group_of_duplicate_after_removal(self): # removing a duplicate also removes it from the dupe:group map. dupe = self.results.groups[1].dupes[0] ref = self.results.groups[1].ref self.results.remove_duplicates([dupe]) assert self.results.get_group_of_duplicate(dupe) is None # also remove group ref assert self.results.get_group_of_duplicate(ref) is None def test_dupe_list_sort_delta_values_nonnumeric(self): # When sorting dupes in delta mode on a non-numeric column, our first sort criteria is if # the string is the same as its ref. g1r, g1d1, g1d2, g2r, g2d1 = self.objects # "aaa" makes our dupe go first in alphabetical order, but since we have the same value as # ref, we're going last. g2r.name = g2d1.name = "aaa" self.results.sort_dupes("name", delta=True) eq_("aaa", self.results.dupes[2].name) def test_dupe_list_sort_delta_values_nonnumeric_case_insensitive(self): # Non-numeric delta sorting comparison is case insensitive g1r, g1d1, g1d2, g2r, g2d1 = self.objects g2r.name = "AaA" g2d1.name = "aAa" self.results.sort_dupes("name", delta=True) eq_("aAa", self.results.dupes[2].name) class TestCaseResultsWithSavedResults: def setup_method(self, method): self.app = DupeGuru() self.results = self.app.results self.objects, self.matches, self.groups = GetTestGroups() self.results.groups = self.groups self.f = io.BytesIO() self.results.save_to_xml(self.f) self.f.seek(0) def test_is_modified(self): # Saving a file sets the modified flag back to False assert not self.results.is_modified def test_is_modified_after_load(self): # Loading a file sets the modified flag back to False def get_file(path): return [f for f in self.objects if str(f.path) == path][0] self.results.groups = self.groups # sets the flag back self.results.load_from_xml(self.f, get_file) assert not self.results.is_modified def test_is_modified_after_remove(self): # Removing dupes sets the modified flag self.results.remove_duplicates([self.results.groups[0].dupes[0]]) assert self.results.is_modified def test_is_modified_after_make_ref(self): # Making a dupe ref sets the modified flag self.results.make_ref(self.results.groups[0].dupes[0]) assert self.results.is_modified class TestCaseResultsMarkings: def setup_method(self, method): self.app = DupeGuru() self.results = self.app.results self.objects, self.matches, self.groups = GetTestGroups() self.results.groups = self.groups def test_stat_line(self): eq_("0 / 3 (0.00 B / 1.01 KB) duplicates marked.", self.results.stat_line) self.results.mark(self.objects[1]) eq_("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.", self.results.stat_line) self.results.mark_invert() eq_("2 / 3 (2.00 B / 1.01 KB) duplicates marked.", self.results.stat_line) self.results.mark_invert() self.results.unmark(self.objects[1]) self.results.mark(self.objects[2]) self.results.mark(self.objects[4]) eq_("2 / 3 (2.00 B / 1.01 KB) duplicates marked.", self.results.stat_line) self.results.mark(self.objects[0]) # this is a ref, it can't be counted eq_("2 / 3 (2.00 B / 1.01 KB) duplicates marked.", self.results.stat_line) self.results.groups = self.groups eq_("0 / 3 (0.00 B / 1.01 KB) duplicates marked.", self.results.stat_line) def test_with_ref_duplicate(self): self.objects[1].is_ref = True self.results.groups = self.groups assert not self.results.mark(self.objects[1]) self.results.mark(self.objects[2]) eq_("1 / 2 (1.00 B / 2.00 B) duplicates marked.", self.results.stat_line) def test_perform_on_marked(self): def log_object(o): log.append(o) return True log = [] self.results.mark_all() self.results.perform_on_marked(log_object, False) assert self.objects[1] in log assert self.objects[2] in log assert self.objects[4] in log eq_(3, len(log)) log = [] self.results.mark_none() self.results.mark(self.objects[4]) self.results.perform_on_marked(log_object, True) eq_(1, len(log)) assert self.objects[4] in log eq_(1, len(self.results.groups)) def test_perform_on_marked_with_problems(self): def log_object(o): log.append(o) if o is self.objects[1]: raise OSError("foobar") log = [] self.results.mark_all() assert self.results.is_marked(self.objects[1]) self.results.perform_on_marked(log_object, True) eq_(len(log), 3) eq_(len(self.results.groups), 1) eq_(len(self.results.groups[0]), 2) assert self.objects[1] in self.results.groups[0] assert not self.results.is_marked(self.objects[2]) assert self.results.is_marked(self.objects[1]) eq_(len(self.results.problems), 1) dupe, msg = self.results.problems[0] assert dupe is self.objects[1] eq_(msg, "foobar") def test_perform_on_marked_with_ref(self): def log_object(o): log.append(o) return True log = [] self.objects[0].is_ref = True self.objects[1].is_ref = True self.results.mark_all() self.results.perform_on_marked(log_object, True) assert self.objects[1] not in log assert self.objects[2] in log assert self.objects[4] in log eq_(2, len(log)) eq_(0, len(self.results.groups)) def test_perform_on_marked_remove_objects_only_at_the_end(self): def check_groups(o): eq_(3, len(g1)) eq_(2, len(g2)) return True g1, g2 = self.results.groups self.results.mark_all() self.results.perform_on_marked(check_groups, True) eq_(0, len(g1)) eq_(0, len(g2)) eq_(0, len(self.results.groups)) def test_remove_duplicates(self): g1 = self.results.groups[0] self.results.mark(g1.dupes[0]) eq_("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.", self.results.stat_line) self.results.remove_duplicates([g1.dupes[1]]) eq_("1 / 2 (1.00 KB / 1.01 KB) duplicates marked.", self.results.stat_line) self.results.remove_duplicates([g1.dupes[0]]) eq_("0 / 1 (0.00 B / 1.00 B) duplicates marked.", self.results.stat_line) def test_make_ref(self): g = self.results.groups[0] d = g.dupes[0] self.results.mark(d) eq_("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.", self.results.stat_line) self.results.make_ref(d) eq_("0 / 3 (0.00 B / 3.00 B) duplicates marked.", self.results.stat_line) self.results.make_ref(d) eq_("0 / 3 (0.00 B / 3.00 B) duplicates marked.", self.results.stat_line) def test_save_xml(self): self.results.mark(self.objects[1]) self.results.mark_invert() f = io.BytesIO() self.results.save_to_xml(f) f.seek(0) doc = ET.parse(f) root = doc.getroot() g1, g2 = root.iter("group") d1, d2, d3 = g1.iter("file") eq_("n", d1.get("marked")) eq_("n", d2.get("marked")) eq_("y", d3.get("marked")) d1, d2 = g2.iter("file") eq_("n", d1.get("marked")) eq_("y", d2.get("marked")) def test_load_xml(self): def get_file(path): return [f for f in self.objects if str(f.path) == path][0] self.objects[4].name = "ibabtu 2" # we can't have 2 files with the same path self.results.mark(self.objects[1]) self.results.mark_invert() f = io.BytesIO() self.results.save_to_xml(f) f.seek(0) app = DupeGuru() r = Results(app) r.load_from_xml(f, get_file) assert not r.is_marked(self.objects[0]) assert not r.is_marked(self.objects[1]) assert r.is_marked(self.objects[2]) assert not r.is_marked(self.objects[3]) assert r.is_marked(self.objects[4]) class TestCaseResultsXML: def setup_method(self, method): self.app = DupeGuru() self.results = self.app.results self.objects, self.matches, self.groups = GetTestGroups() self.results.groups = self.groups def get_file(self, path): # use this as a callback for load_from_xml return [o for o in self.objects if str(o.path) == path][0] def test_save_to_xml(self): self.objects[0].is_ref = True self.objects[0].words = [["foo", "bar"]] f = io.BytesIO() self.results.save_to_xml(f) f.seek(0) doc = ET.parse(f) root = doc.getroot() eq_("results", root.tag) eq_(2, len(root)) eq_(2, len([c for c in root if c.tag == "group"])) g1, g2 = root eq_(6, len(g1)) eq_(3, len([c for c in g1 if c.tag == "file"])) eq_(3, len([c for c in g1 if c.tag == "match"])) d1, d2, d3 = (c for c in g1 if c.tag == "file") eq_(op.join("basepath", "foo bar"), d1.get("path")) eq_(op.join("basepath", "bar bleh"), d2.get("path")) eq_(op.join("basepath", "foo bleh"), d3.get("path")) eq_("y", d1.get("is_ref")) eq_("n", d2.get("is_ref")) eq_("n", d3.get("is_ref")) eq_("foo,bar", d1.get("words")) eq_("bar,bleh", d2.get("words")) eq_("foo,bleh", d3.get("words")) eq_(3, len(g2)) eq_(2, len([c for c in g2 if c.tag == "file"])) eq_(1, len([c for c in g2 if c.tag == "match"])) d1, d2 = (c for c in g2 if c.tag == "file") eq_(op.join("basepath", "ibabtu"), d1.get("path")) eq_(op.join("basepath", "ibabtu"), d2.get("path")) eq_("n", d1.get("is_ref")) eq_("n", d2.get("is_ref")) eq_("ibabtu", d1.get("words")) eq_("ibabtu", d2.get("words")) def test_load_xml(self): def get_file(path): return [f for f in self.objects if str(f.path) == path][0] self.objects[0].is_ref = True self.objects[4].name = "ibabtu 2" # we can't have 2 files with the same path f = io.BytesIO() self.results.save_to_xml(f) f.seek(0) app = DupeGuru() r = Results(app) r.load_from_xml(f, get_file) eq_(2, len(r.groups)) g1, g2 = r.groups eq_(3, len(g1)) assert g1[0].is_ref assert not g1[1].is_ref assert not g1[2].is_ref assert g1[0] is self.objects[0] assert g1[1] is self.objects[1] assert g1[2] is self.objects[2] eq_(["foo", "bar"], g1[0].words) eq_(["bar", "bleh"], g1[1].words) eq_(["foo", "bleh"], g1[2].words) eq_(2, len(g2)) assert not g2[0].is_ref assert not g2[1].is_ref assert g2[0] is self.objects[3] assert g2[1] is self.objects[4] eq_(["ibabtu"], g2[0].words) eq_(["ibabtu"], g2[1].words) def test_load_xml_with_filename(self, tmpdir): def get_file(path): return [f for f in self.objects if str(f.path) == path][0] filename = str(tmpdir.join("dupeguru_results.xml")) self.objects[4].name = "ibabtu 2" # we can't have 2 files with the same path self.results.save_to_xml(filename) app = DupeGuru() r = Results(app) r.load_from_xml(filename, get_file) eq_(2, len(r.groups)) def test_load_xml_with_some_files_that_dont_exist_anymore(self): def get_file(path): if path.endswith("ibabtu 2"): return None return [f for f in self.objects if str(f.path) == path][0] self.objects[4].name = "ibabtu 2" # we can't have 2 files with the same path f = io.BytesIO() self.results.save_to_xml(f) f.seek(0) app = DupeGuru() r = Results(app) r.load_from_xml(f, get_file) eq_(1, len(r.groups)) eq_(3, len(r.groups[0])) def test_load_xml_missing_attributes_and_bogus_elements(self): def get_file(path): return [f for f in self.objects if str(f.path) == path][0] root = ET.Element("foobar") # The root element shouldn't matter, really. group_node = ET.SubElement(root, "group") dupe_node = ET.SubElement(group_node, "file") # Perfectly correct file dupe_node.set("path", op.join("basepath", "foo bar")) dupe_node.set("is_ref", "y") dupe_node.set("words", "foo, bar") dupe_node = ET.SubElement(group_node, "file") # is_ref missing, default to 'n' dupe_node.set("path", op.join("basepath", "foo bleh")) dupe_node.set("words", "foo, bleh") dupe_node = ET.SubElement(group_node, "file") # words are missing, valid. dupe_node.set("path", op.join("basepath", "bar bleh")) dupe_node = ET.SubElement(group_node, "file") # path is missing, invalid. dupe_node.set("words", "foo, bleh") dupe_node = ET.SubElement(group_node, "foobar") # Invalid element name dupe_node.set("path", op.join("basepath", "bar bleh")) dupe_node.set("is_ref", "y") dupe_node.set("words", "bar, bleh") match_node = ET.SubElement(group_node, "match") # match pointing to a bad index match_node.set("first", "42") match_node.set("second", "45") match_node = ET.SubElement(group_node, "match") # match with missing attrs match_node = ET.SubElement(group_node, "match") # match with non-int values match_node.set("first", "foo") match_node.set("second", "bar") match_node.set("percentage", "baz") group_node = ET.SubElement(root, "foobar") # invalid group group_node = ET.SubElement(root, "group") # empty group f = io.BytesIO() tree = ET.ElementTree(root) tree.write(f, encoding="utf-8") f.seek(0) app = DupeGuru() r = Results(app) r.load_from_xml(f, get_file) eq_(1, len(r.groups)) eq_(3, len(r.groups[0])) def test_xml_non_ascii(self): def get_file(path): if path == op.join("basepath", "\xe9foo bar"): return objects[0] if path == op.join("basepath", "bar bleh"): return objects[1] objects = [NamedObject("\xe9foo bar", True), NamedObject("bar bleh", True)] matches = engine.getmatches(objects) # we should have 5 matches groups = engine.get_groups(matches) # We should have 2 groups for g in groups: g.prioritize(lambda x: objects.index(x)) # We want the dupes to be in the same order as the list is app = DupeGuru() results = Results(app) results.groups = groups f = io.BytesIO() results.save_to_xml(f) f.seek(0) app = DupeGuru() r = Results(app) r.load_from_xml(f, get_file) g = r.groups[0] eq_("\xe9foo bar", g[0].name) eq_(["efoo", "bar"], g[0].words) def test_load_invalid_xml(self): f = io.BytesIO() f.write(b"".format(self.name, self.path) no = NamedObject @pytest.fixture def fake_fileexists(request): # This is a hack to avoid invalidating all previous tests since the scanner started to test # for file existence before doing the match grouping. monkeypatch = request.getfixturevalue("monkeypatch") monkeypatch.setattr(Path, "exists", lambda _: True) def test_empty(fake_fileexists): s = Scanner() r = s.get_dupe_groups([]) eq_(r, []) def test_default_settings(fake_fileexists): s = Scanner() eq_(s.min_match_percentage, 80) eq_(s.scan_type, ScanType.FILENAME) eq_(s.mix_file_kind, True) eq_(s.word_weighting, False) eq_(s.match_similar_words, False) eq_(s.size_threshold, 0) eq_(s.large_size_threshold, 0) eq_(s.big_file_size_threshold, 0) def test_simple_with_default_settings(fake_fileexists): s = Scanner() f = [no("foo bar", path="p1"), no("foo bar", path="p2"), no("foo bleh")] r = s.get_dupe_groups(f) eq_(len(r), 1) g = r[0] # 'foo bleh' cannot be in the group because the default min match % is 80 eq_(len(g), 2) assert g.ref in f[:2] assert g.dupes[0] in f[:2] def test_simple_with_lower_min_match(fake_fileexists): s = Scanner() s.min_match_percentage = 50 f = [no("foo bar", path="p1"), no("foo bar", path="p2"), no("foo bleh")] r = s.get_dupe_groups(f) eq_(len(r), 1) g = r[0] eq_(len(g), 3) def test_trim_all_ref_groups(fake_fileexists): # When all files of a group are ref, don't include that group in the results, but also don't # count the files from that group as discarded. s = Scanner() f = [ no("foo", path="p1"), no("foo", path="p2"), no("bar", path="p1"), no("bar", path="p2"), ] f[2].is_ref = True f[3].is_ref = True r = s.get_dupe_groups(f) eq_(len(r), 1) eq_(s.discarded_file_count, 0) def test_prioritize(fake_fileexists): s = Scanner() f = [ no("foo", path="p1"), no("foo", path="p2"), no("bar", path="p1"), no("bar", path="p2"), ] f[1].size = 2 f[2].size = 3 f[3].is_ref = True r = s.get_dupe_groups(f) g1, g2 = r assert f[1] in (g1.ref, g2.ref) assert f[0] in (g1.dupes[0], g2.dupes[0]) assert f[3] in (g1.ref, g2.ref) assert f[2] in (g1.dupes[0], g2.dupes[0]) def test_content_scan(fake_fileexists): s = Scanner() s.scan_type = ScanType.CONTENTS f = [no("foo"), no("bar"), no("bleh")] f[0].digest = f[0].digest_partial = f[0].digest_samples = "foobar" f[1].digest = f[1].digest_partial = f[1].digest_samples = "foobar" f[2].digest = f[2].digest_partial = f[1].digest_samples = "bleh" r = s.get_dupe_groups(f) eq_(len(r), 1) eq_(len(r[0]), 2) eq_(s.discarded_file_count, 0) # don't count the different digest as discarded! def test_content_scan_compare_sizes_first(fake_fileexists): class MyFile(no): @property def digest(self): raise AssertionError() s = Scanner() s.scan_type = ScanType.CONTENTS f = [MyFile("foo", 1), MyFile("bar", 2)] eq_(len(s.get_dupe_groups(f)), 0) def test_ignore_file_size(fake_fileexists): s = Scanner() s.scan_type = ScanType.CONTENTS small_size = 10 # 10KB s.size_threshold = 0 large_size = 100 * 1024 * 1024 # 100MB s.large_size_threshold = 0 f = [ no("smallignore1", small_size - 1), no("smallignore2", small_size - 1), no("small1", small_size), no("small2", small_size), no("large1", large_size), no("large2", large_size), no("largeignore1", large_size + 1), no("largeignore2", large_size + 1), ] f[0].digest = f[0].digest_partial = f[0].digest_samples = "smallignore" f[1].digest = f[1].digest_partial = f[1].digest_samples = "smallignore" f[2].digest = f[2].digest_partial = f[2].digest_samples = "small" f[3].digest = f[3].digest_partial = f[3].digest_samples = "small" f[4].digest = f[4].digest_partial = f[4].digest_samples = "large" f[5].digest = f[5].digest_partial = f[5].digest_samples = "large" f[6].digest = f[6].digest_partial = f[6].digest_samples = "largeignore" f[7].digest = f[7].digest_partial = f[7].digest_samples = "largeignore" r = s.get_dupe_groups(f) # No ignores eq_(len(r), 4) # Ignore smaller s.size_threshold = small_size r = s.get_dupe_groups(f) eq_(len(r), 3) # Ignore larger s.size_threshold = 0 s.large_size_threshold = large_size r = s.get_dupe_groups(f) eq_(len(r), 3) # Ignore both s.size_threshold = small_size r = s.get_dupe_groups(f) eq_(len(r), 2) def test_big_file_partial_hashes(fake_fileexists): s = Scanner() s.scan_type = ScanType.CONTENTS smallsize = 1 bigsize = 100 * 1024 * 1024 # 100MB s.big_file_size_threshold = bigsize f = [no("bigfoo", bigsize), no("bigbar", bigsize), no("smallfoo", smallsize), no("smallbar", smallsize)] f[0].digest = f[0].digest_partial = f[0].digest_samples = "foobar" f[1].digest = f[1].digest_partial = f[1].digest_samples = "foobar" f[2].digest = f[2].digest_partial = "bleh" f[3].digest = f[3].digest_partial = "bleh" r = s.get_dupe_groups(f) eq_(len(r), 2) # digest_partial is still the same, but the file is actually different f[1].digest = f[1].digest_samples = "difffoobar" # here we compare the full digests, as the user disabled the optimization s.big_file_size_threshold = 0 r = s.get_dupe_groups(f) eq_(len(r), 1) # here we should compare the digest_samples, and see they are different s.big_file_size_threshold = bigsize r = s.get_dupe_groups(f) eq_(len(r), 1) def test_min_match_perc_doesnt_matter_for_content_scan(fake_fileexists): s = Scanner() s.scan_type = ScanType.CONTENTS f = [no("foo"), no("bar"), no("bleh")] f[0].digest = f[0].digest_partial = f[0].digest_samples = "foobar" f[1].digest = f[1].digest_partial = f[1].digest_samples = "foobar" f[2].digest = f[2].digest_partial = f[2].digest_samples = "bleh" s.min_match_percentage = 101 r = s.get_dupe_groups(f) eq_(len(r), 1) eq_(len(r[0]), 2) s.min_match_percentage = 0 r = s.get_dupe_groups(f) eq_(len(r), 1) eq_(len(r[0]), 2) def test_content_scan_doesnt_put_digest_in_words_at_the_end(fake_fileexists): s = Scanner() s.scan_type = ScanType.CONTENTS f = [no("foo"), no("bar")] f[0].digest = f[0].digest_partial = f[ 0 ].digest_samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" f[1].digest = f[1].digest_partial = f[ 1 ].digest_samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" r = s.get_dupe_groups(f) # FIXME looks like we are missing something here? r[0] def test_extension_is_not_counted_in_filename_scan(fake_fileexists): s = Scanner() s.min_match_percentage = 100 f = [no("foo.bar"), no("foo.bleh")] r = s.get_dupe_groups(f) eq_(len(r), 1) eq_(len(r[0]), 2) def test_job(fake_fileexists): def do_progress(progress, desc=""): log.append(progress) return True s = Scanner() log = [] f = [no("foo bar"), no("foo bar"), no("foo bleh")] s.get_dupe_groups(f, j=job.Job(1, do_progress)) eq_(log[0], 0) eq_(log[-1], 100) def test_mix_file_kind(fake_fileexists): s = Scanner() s.mix_file_kind = False f = [no("foo.1"), no("foo.2")] r = s.get_dupe_groups(f) eq_(len(r), 0) def test_word_weighting(fake_fileexists): s = Scanner() s.min_match_percentage = 75 s.word_weighting = True f = [no("foo bar"), no("foo bar bleh")] r = s.get_dupe_groups(f) eq_(len(r), 1) g = r[0] m = g.get_match_of(g.dupes[0]) eq_(m.percentage, 75) # 16 letters, 12 matching def test_similar_words(fake_fileexists): s = Scanner() s.match_similar_words = True f = [ no("The White Stripes"), no("The Whites Stripe"), no("Limp Bizkit"), no("Limp Bizkitt"), ] r = s.get_dupe_groups(f) eq_(len(r), 2) def test_fields(fake_fileexists): s = Scanner() s.scan_type = ScanType.FIELDS f = [no("The White Stripes - Little Ghost"), no("The White Stripes - Little Acorn")] r = s.get_dupe_groups(f) eq_(len(r), 0) def test_fields_no_order(fake_fileexists): s = Scanner() s.scan_type = ScanType.FIELDSNOORDER f = [no("The White Stripes - Little Ghost"), no("Little Ghost - The White Stripes")] r = s.get_dupe_groups(f) eq_(len(r), 1) def test_tag_scan(fake_fileexists): s = Scanner() s.scan_type = ScanType.TAG o1 = no("foo") o2 = no("bar") o1.artist = "The White Stripes" o1.title = "The Air Near My Fingers" o2.artist = "The White Stripes" o2.title = "The Air Near My Fingers" r = s.get_dupe_groups([o1, o2]) eq_(len(r), 1) def test_tag_with_album_scan(fake_fileexists): s = Scanner() s.scan_type = ScanType.TAG s.scanned_tags = {"artist", "album", "title"} o1 = no("foo") o2 = no("bar") o3 = no("bleh") o1.artist = "The White Stripes" o1.title = "The Air Near My Fingers" o1.album = "Elephant" o2.artist = "The White Stripes" o2.title = "The Air Near My Fingers" o2.album = "Elephant" o3.artist = "The White Stripes" o3.title = "The Air Near My Fingers" o3.album = "foobar" r = s.get_dupe_groups([o1, o2, o3]) eq_(len(r), 1) def test_that_dash_in_tags_dont_create_new_fields(fake_fileexists): s = Scanner() s.scan_type = ScanType.TAG s.scanned_tags = {"artist", "album", "title"} s.min_match_percentage = 50 o1 = no("foo") o2 = no("bar") o1.artist = "The White Stripes - a" o1.title = "The Air Near My Fingers - a" o1.album = "Elephant - a" o2.artist = "The White Stripes - b" o2.title = "The Air Near My Fingers - b" o2.album = "Elephant - b" r = s.get_dupe_groups([o1, o2]) eq_(len(r), 1) def test_tag_scan_with_different_scanned(fake_fileexists): s = Scanner() s.scan_type = ScanType.TAG s.scanned_tags = {"track", "year"} o1 = no("foo") o2 = no("bar") o1.artist = "The White Stripes" o1.title = "some title" o1.track = "foo" o1.year = "bar" o2.artist = "The White Stripes" o2.title = "another title" o2.track = "foo" o2.year = "bar" r = s.get_dupe_groups([o1, o2]) eq_(len(r), 1) def test_tag_scan_only_scans_existing_tags(fake_fileexists): s = Scanner() s.scan_type = ScanType.TAG s.scanned_tags = {"artist", "foo"} o1 = no("foo") o2 = no("bar") o1.artist = "The White Stripes" o1.foo = "foo" o2.artist = "The White Stripes" o2.foo = "bar" r = s.get_dupe_groups([o1, o2]) eq_(len(r), 1) # Because 'foo' is not scanned, they match def test_tag_scan_converts_to_str(fake_fileexists): s = Scanner() s.scan_type = ScanType.TAG s.scanned_tags = {"track"} o1 = no("foo") o2 = no("bar") o1.track = 42 o2.track = 42 try: r = s.get_dupe_groups([o1, o2]) except TypeError: raise AssertionError() eq_(len(r), 1) def test_tag_scan_non_ascii(fake_fileexists): s = Scanner() s.scan_type = ScanType.TAG s.scanned_tags = {"title"} o1 = no("foo") o2 = no("bar") o1.title = "foobar\u00e9" o2.title = "foobar\u00e9" try: r = s.get_dupe_groups([o1, o2]) except UnicodeEncodeError: raise AssertionError() eq_(len(r), 1) def test_ignore_list(fake_fileexists): s = Scanner() f1 = no("foobar") f2 = no("foobar") f3 = no("foobar") f1.path = Path("dir1/foobar") f2.path = Path("dir2/foobar") f3.path = Path("dir3/foobar") ignore_list = IgnoreList() ignore_list.ignore(str(f1.path), str(f2.path)) ignore_list.ignore(str(f1.path), str(f3.path)) r = s.get_dupe_groups([f1, f2, f3], ignore_list=ignore_list) eq_(len(r), 1) g = r[0] eq_(len(g.dupes), 1) assert f1 not in g assert f2 in g assert f3 in g # Ignored matches are not counted as discarded eq_(s.discarded_file_count, 0) def test_ignore_list_checks_for_unicode(fake_fileexists): # scanner was calling path_str for ignore list checks. Since the Path changes, it must # be unicode(path) s = Scanner() f1 = no("foobar") f2 = no("foobar") f3 = no("foobar") f1.path = Path("foo1\u00e9") f2.path = Path("foo2\u00e9") f3.path = Path("foo3\u00e9") ignore_list = IgnoreList() ignore_list.ignore(str(f1.path), str(f2.path)) ignore_list.ignore(str(f1.path), str(f3.path)) r = s.get_dupe_groups([f1, f2, f3], ignore_list=ignore_list) eq_(len(r), 1) g = r[0] eq_(len(g.dupes), 1) assert f1 not in g assert f2 in g assert f3 in g def test_file_evaluates_to_false(fake_fileexists): # A very wrong way to use any() was added at some point, causing resulting group list # to be empty. class FalseNamedObject(NamedObject): def __bool__(self): return False s = Scanner() f1 = FalseNamedObject("foobar", path="p1") f2 = FalseNamedObject("foobar", path="p2") r = s.get_dupe_groups([f1, f2]) eq_(len(r), 1) def test_size_threshold(fake_fileexists): # Only file equal or higher than the size_threshold in size are scanned s = Scanner() f1 = no("foo", 1, path="p1") f2 = no("foo", 2, path="p2") f3 = no("foo", 3, path="p3") s.size_threshold = 2 groups = s.get_dupe_groups([f1, f2, f3]) eq_(len(groups), 1) [group] = groups eq_(len(group), 2) assert f1 not in group assert f2 in group assert f3 in group def test_tie_breaker_path_deepness(fake_fileexists): # If there is a tie in prioritization, path deepness is used as a tie breaker s = Scanner() o1, o2 = no("foo"), no("foo") o1.path = Path("foo") o2.path = Path("foo/bar") [group] = s.get_dupe_groups([o1, o2]) assert group.ref is o2 def test_tie_breaker_copy(fake_fileexists): # if copy is in the words used (even if it has a deeper path), it becomes a dupe s = Scanner() o1, o2 = no("foo bar Copy"), no("foo bar") o1.path = Path("deeper/path") o2.path = Path("foo") [group] = s.get_dupe_groups([o1, o2]) assert group.ref is o2 def test_tie_breaker_same_name_plus_digit(fake_fileexists): # if ref has the same words as dupe, but has some just one extra word which is a digit, it # becomes a dupe s = Scanner() o1 = no("foo bar 42") o2 = no("foo bar [42]") o3 = no("foo bar (42)") o4 = no("foo bar {42}") o5 = no("foo bar") # all numbered names have deeper paths, so they'll end up ref if the digits aren't correctly # used as tie breakers o1.path = Path("deeper/path") o2.path = Path("deeper/path") o3.path = Path("deeper/path") o4.path = Path("deeper/path") o5.path = Path("foo") [group] = s.get_dupe_groups([o1, o2, o3, o4, o5]) assert group.ref is o5 def test_partial_group_match(fake_fileexists): # Count the number of discarded matches (when a file doesn't match all other dupes of the # group) in Scanner.discarded_file_count s = Scanner() o1, o2, o3 = no("a b"), no("a"), no("b") s.min_match_percentage = 50 [group] = s.get_dupe_groups([o1, o2, o3]) eq_(len(group), 2) assert o1 in group # The file that will actually be counted as a dupe is undefined. The only thing we want to test # is that we don't have both if o2 in group: assert o3 not in group else: assert o3 in group eq_(s.discarded_file_count, 1) def test_dont_group_files_that_dont_exist(tmpdir): # when creating groups, check that files exist first. It's possible that these files have # been moved during the scan by the user. # In this test, we have to delete one of the files between the get_matches() part and the # get_groups() part. s = Scanner() s.scan_type = ScanType.CONTENTS p = Path(str(tmpdir)) with p.joinpath("file1").open("w") as fp: fp.write("foo") with p.joinpath("file2").open("w") as fp: fp.write("foo") file1, file2 = fs.get_files(p) def getmatches(*args, **kw): file2.path.unlink() return [Match(file1, file2, 100)] s._getmatches = getmatches assert not s.get_dupe_groups([file1, file2]) def test_folder_scan_exclude_subfolder_matches(fake_fileexists): # when doing a Folders scan type, don't include matches for folders whose parent folder already # match. s = Scanner() s.scan_type = ScanType.FOLDERS topf1 = no("top folder 1", size=42) topf1.digest = topf1.digest_partial = topf1.digest_samples = b"some_digest__1" topf1.path = Path("/topf1") topf2 = no("top folder 2", size=42) topf2.digest = topf2.digest_partial = topf2.digest_samples = b"some_digest__1" topf2.path = Path("/topf2") subf1 = no("sub folder 1", size=41) subf1.digest = subf1.digest_partial = subf1.digest_samples = b"some_digest__2" subf1.path = Path("/topf1/sub") subf2 = no("sub folder 2", size=41) subf2.digest = subf2.digest_partial = subf2.digest_samples = b"some_digest__2" subf2.path = Path("/topf2/sub") eq_(len(s.get_dupe_groups([topf1, topf2, subf1, subf2])), 1) # only top folders # however, if another folder matches a subfolder, keep in in the matches otherf = no("other folder", size=41) otherf.digest = otherf.digest_partial = otherf.digest_samples = b"some_digest__2" otherf.path = Path("/otherfolder") eq_(len(s.get_dupe_groups([topf1, topf2, subf1, subf2, otherf])), 2) def test_ignore_files_with_same_path(fake_fileexists): # It's possible that the scanner is fed with two file instances pointing to the same path. One # of these files has to be ignored s = Scanner() f1 = no("foobar", path="path1/foobar") f2 = no("foobar", path="path1/foobar") eq_(s.get_dupe_groups([f1, f2]), []) def test_dont_count_ref_files_as_discarded(fake_fileexists): # To speed up the scan, we don't bother comparing contents of files that are both ref files. # However, this causes problems in "discarded" counting and we make sure here that we don't # report discarded matches in exact duplicate scans. s = Scanner() s.scan_type = ScanType.CONTENTS o1 = no("foo", path="p1") o2 = no("foo", path="p2") o3 = no("foo", path="p3") o1.digest = o1.digest_partial = o1.digest_samples = "foobar" o2.digest = o2.digest_partial = o2.digest_samples = "foobar" o3.digest = o3.digest_partial = o3.digest_samples = "foobar" o1.is_ref = True o2.is_ref = True eq_(len(s.get_dupe_groups([o1, o2, o3])), 1) eq_(s.discarded_file_count, 0) def test_prioritize_me(fake_fileexists): # in ScannerME, bitrate goes first (right after is_ref) in prioritization s = ScannerME() o1, o2 = no("foo", path="p1"), no("foo", path="p2") o1.bitrate = 1 o2.bitrate = 2 [group] = s.get_dupe_groups([o1, o2]) assert group.ref is o2 dupeguru-4.3.1/core/util.py000066400000000000000000000070761426171743600156630ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import time import sys import os import urllib.request import urllib.error import json import semantic_version import logging from typing import Union from hscommon.util import format_time_decimal def format_timestamp(t, delta): if delta: return format_time_decimal(t) else: if t > 0: return time.strftime("%Y/%m/%d %H:%M:%S", time.localtime(t)) else: return "---" def format_words(w): def do_format(w): if isinstance(w, list): return "(%s)" % ", ".join(do_format(item) for item in w) else: return w.replace("\n", " ") return ", ".join(do_format(item) for item in w) def format_perc(p): return "%0.0f" % p def format_dupe_count(c): return str(c) if c else "---" def cmp_value(dupe, attrname): value = getattr(dupe, attrname, "") return value.lower() if isinstance(value, str) else value def fix_surrogate_encoding(s, encoding="utf-8"): # ref #210. It's possible to end up with file paths that, while correct unicode strings, are # decoded with the 'surrogateescape' option, which make the string unencodable to utf-8. We fix # these strings here by trying to encode them and, if it fails, we do an encode/decode dance # to remove the problematic characters. This dance is *lossy* but there's not much we can do # because if we end up with this type of string, it means that we don't know the encoding of the # underlying filesystem that brought them. Don't use this for strings you're going to re-use in # fs-related functions because you're going to lose your path (it's going to change). Use this # if you need to export the path somewhere else, outside of the unicode realm. # See http://lucumr.pocoo.org/2013/7/2/the-updated-guide-to-unicode/ try: s.encode(encoding) except UnicodeEncodeError: return s.encode(encoding, "replace").decode(encoding) else: return s def executable_folder(): return os.path.dirname(os.path.abspath(sys.argv[0])) def check_for_update(current_version: str, include_prerelease: bool = False) -> Union[None, dict]: request = urllib.request.Request( "https://api.github.com/repos/arsenetar/dupeguru/releases", headers={"Accept": "application/vnd.github.v3+json"}, ) try: with urllib.request.urlopen(request) as response: if response.status != 200: logging.warn(f"Error retriving updates. Status: {response.status}") return None try: response_json = json.loads(response.read()) except json.JSONDecodeError as ex: logging.warn(f"Error parsing updates. {ex.msg}") return None except urllib.error.URLError as ex: logging.warn(f"Error retriving updates. {ex.reason}") return None new_version = semantic_version.Version(current_version) new_url = None for release in response_json: release_version = semantic_version.Version(release["name"]) if new_version < release_version and (include_prerelease or not release_version.prerelease): new_version = release_version new_url = release["html_url"] if new_url is not None: return {"version": new_version, "url": new_url} else: return None dupeguru-4.3.1/help/000077500000000000000000000000001426171743600143225ustar00rootroot00000000000000dupeguru-4.3.1/help/changelog000066400000000000000000000613411426171743600162010ustar00rootroot00000000000000=== 4.3.1 (2022-07-08) * Fix issue where cache db exceptions could prevent files being hashed (#1015) * Add extra guard for non-zero length files without digests to prevent false duplicates * Update Italian translations === 4.3.0 (2022-07-01) * Redirect stdout from custom command to the log files (#1008) * Update translations * Fix typo in debian control file (#989) * Add option to profile scans * Update fs.py to optimize stat() calls * Fix Error when delete after scan (#988) * Update directory scanning to use os.scandir() and DirEntry objects * Improve performance of Directories.get_state() * Migrate from hscommon.path to pathlib * Switch file hashing to xxhash with fallback to md5 * Add update check feature to about box === 4.2.1 (2022-03-25) * Default to English on unsupported system language (#976) * Fix image viewer zoom datatype issue (#978) * Fix errors from window change event (#937, #980) * Fix deprecation warning from SQLite * Enforce minimum Windows version in installer (#983) * Fix help path for local files * Drop python 3.6 support * VS Code project settings added, yaml validation for GitHub actions === 4.2.0 (2021-01-24) * Add Malay and Turkish * Add dark style for windows (#900) * Add caching md5 file hashes (#942) * Add feature to partially hash large files, with user adjustable preference (#908) * Add portable mode (store settings next to executable) * Add file association for .dupeguru files on windows * Add ability to pass .dupeguru file to load on startup (#902) * Add ability to reveal in explorer/finder (#895) * Switch audio tag processing from hsaudiotag to mutagen (#440) * Add ability to use Qt dialogs instead of native OS dialogs for some file selection operations * Add OS and Python details to error dialog to assist in troubleshooting * Add preference to ignore large files with threshold (#430) * Fix error on close from DetailsPanel (#857, #873) * Change reference background color (#894, #898) * Remove stripping of unicode characters when matching names (#879) * Fix exception when deleting in delta view (#863, #905) * Fix dupes only view not updating after re-prioritize results (#757, #910, #911) * Fix ability to drag'n'drop file/folder with certain characters in name (#897) * Fix window position opening partially offscreen (#653) * Fix TypeError is photo mode (#551) * Change message for when files are deleted directly (#904) * Add more feedback during scan (#700) * Add Python version check to build.py (#589) * General code cleanups * Improvements to using standardized build tooling * Moved CI/CD to github actions, added codeql, SonarCloud === 4.1.1 (2021-03-21) * Add Japanese * Update internationalization and translations to be up to date with current UI. * Minor translation and UI language updates * Fix language selection issues on Windows (#760) * Add some additional notes about builds on Linux based systems * Add import from transifex export to build.py === 4.1.0 (2020-12-29) * Use tabs instead of separate windows (#688) * Show the shortcut for "mark selected" in results dialog (#656, #641) * Add image comparison features to details dialog (#683) * Add the ability to use regex based exclusion filters (#705) * Change reference row background color, and allow user to adjust the color (#701) * Save / Load directories as XML (#706) * Workaround for EXIF IFD type mismatch in parsing function (#630, #698) * Progress dialog stuck at "Verified X/X matches" (#693, #694) * Fix word wrap in ignore list dialog (#687) * Fix issue with result window action on creation (#685) * Colorize details table differences, allow moving rows (#682) * Fix loading Result of 'Scan Type: Folders' shows only '---' in every table cell (#677, #676) * Fix issue with details and results dialog row trimming (#655, #654) * Add option to enable/disable bold font (#646, #314) * Use relative icon path for themes to override more easily (#746) * Fix issues with Python 3.8 compatibility (#665) * Fix flake8 issues (#672) * Update to use newer pytest and expand flake8 checking, cleanup various Deprecation Warnings * Add warnings to packaging script when files are not built (#691) * Use relative icon path for themes to override more easily (#746) * Update Packaging for Ubuntu (#593) * Minor Build Updates (#627, #575, #628, #614) * Update CI builds and add windows CI (#572, #669) === 4.0.4 (2019-05-13) * Update qt/platform.py to support other Unix style OSes (#444) * Fix font size scaling issue in properties dialog [qt] (#504) * Updates to support Python 3.7 * Fix issue with result window appearing partially off-screen [qt] (#521) * Fix translation error for Simplified Chinese * Updates to language files for German (#479) * Fix error with multiple close calls to the progress window [qt] (#460, #449) * Add Travis CI Builds * Un-recurse methods get_files() and get_state() to improve stability (#421) * Updates to language files for Italian (#445, #446, #447, #448) * Fix issue with cache_shelve (#402, #439) * Updated Windows packaging and builds (#438, #456, #461, #491, #474, #490, #565) * Handle OS termination signals (#425) * Make documentation installation optional * Move cocoa UI to dupeguru-cocoa [cocoa] === 4.0.3 (2016-11-24) * Add new picture cache backend: shelve * Make shelve picture cache backend the active one on MacOS to fix #394 more elegantly. [cocoa] * Remove Sparkle (auto-updates) due to technical limitations. [cocoa] === 4.0.2 (2016-10-09) * Fix systematic crash in Picture Mode under MacOS Sierra. (#394) * No change for Linux. Just keeping version in sync. === 4.0.1 (2016-08-24) * Add Greek localization, by Gabriel Koutilellis. (#382) * Fix localization base path. [qt] (#378) * Fix broken load results dialog. [qt] * Fix crash on load results. [cocoa] (#380) * Save preferences more predictably. [qt] (#379) * Fix picture mode's fuzzy block scanner threshold. (#387) === 4.0.0 (2016-07-01) * Merge Standard, Music and Picture editions in the same application! * Improve documentation. (#294) * Add Polish, Korean, Spanish and Dutch localizations. * qt: Fix wrong use_regexp option propagation to core. (#295) * qt: Fix progress window mistakenly showing up on startup. (#357) * Bump Python requirement to v3.4. * Bump OS X requirement to 10.8 * Drop Windows support, maybe temporarily. `Details `_ * cocoa: Drop iPhoto, Aperture and iTunes support. Was unmaintained and obsolete. * Drop "Audio Contents" scan type. Was confusing and seldom useful. * Change license to GPLv3 === 3.9.1 (2014-10-17) * Fixed ``AttributeError: 'ComboboxModel' object has no attribute 'reset'``. [Linux, Windows] (#254) * Fixed ``PermissionError`` on saving results. (#266) * Fixed a build problem introduced by Sphinx 1.2.3. * Updated German localisation, by Frank Weber. === 3.9.0 (2014-04-19) * This is mostly a dependencies upgrade. * Upgraded to Python 3.3. * Upgraded to Qt 5. * Minimum Windows version is now Windows 7 64bit. * Minimum Ubuntu version is now 14.04. * Minimum OS X version is now 10.7 (Lion). * ... But with a couple of little improvements. * Improved documentation. * Overwrite subfolders' state when setting states in folder dialog (#248) * The error report dialog now brings the user to Github issues. === 3.8.0 (2013-12-07) * Disable symlink/hardlink deletion option when not relevant. (#247) * Make Cmd+A select all folders in the Folder Selection dialog. [Mac] (#228) * Make non-numeric delta comparison case insensitive. (#239) * Fix surrogate-related UnicodeEncodeError on CSV export. (#210) * Fixed crash on Dupe Count sorting with Delta + Dupes Only. (#238) * Improved documentation. * Important internal refactorings. * Dropped Ubuntu 12.04 and 12.10 support. * Removed the fairware dialog ([More Info](http://www.hardcoded.net/articles/phasing-out-fairware)). === 3.7.1 (2013-08-19) * Fixed folder scan type, which was broken in v3.7.0. === 3.7.0 (2013-08-17) * Improved delta values to support non-numerical values. (#213) * Improved the Re-Prioritize dialog's UI. (#224) * Added hardlink/symlink support on Windows Vista+. (#220) * Dropped 32bit support on Mac OS X. * Added Vietnamese localization by Phan Anh. === 3.6.1 (2013-04-28) * Improved "Make Selection Reference" to make it clearer. (#222) * Improved "Open Selected" to allow opening more than one file at once. (#142) * Fixed a few typos here and there. (#216 #225) * Tweaked the fairware dialog ([More Info](http://www.hardcoded.net/articles/phasing-out-fairware)). * Added Arch Linux packaging * Added a 64-bit build for Windows. * Improved Russian localization by Kyrill Detinov. * Improved Brazilian localization by Victor Figueiredo. === 3.6.0 (2012-08-08) * Added "Export to CSV". (#189) * Added "Replace with symlinks" to complement "Replace with hardlinks". [Mac, Linux] (#194) * dupeGuru now tells how many duplicates were affected after each re-prioritization operation. (#204) * Added Longest/Shortest filename criteria in the re-prioritize dialog. (#198) * Fixed result table cells which mistakenly became writable in v3.5.0. [Mac] (#203) * Fixed "Rename Selected" which was broken since v3.5.0. [Mac] (#202) * Fixed a bug where "Reset to Defaults" in the Columns menu wouldn't refresh menu items' marked state. * Added Brazilian localization by Victor Figueiredo. === 3.5.0 (2012-06-01) * Added a Deletion Options panel. * Greatly improved memory usage for big scans. * Added a keybinding for the filter field. (#182) [Mac] * Upgraded minimum requirements for Ubuntu to 12.04. === 3.4.1 (2012-04-14) * Fixed the "Folders" scan type. [Mac] * Fixed localization issues. [Windows, Linux] === 3.4.0 (2012-03-29) * Improved results window UI. [Windows, Linux] * Added a dialog to edit the Ignore List. * Added the ability to sort results by "marked" status. * Fixed "Open with default application". (#190) * Fixed a bug where there would be a false reporting of discarded matches. (#195) * Fixed various localization glitches. * Fixed hard crashes on crash reporting. (#196) * Fixed bug where the details panel would show up at inconvenient places in the screen. [Windows, Linux] === 3.3.3 (2012-02-01) * Fixed crash on adding some folders. [Mac OS X] * Added Ukrainian localization by Yuri Petrashko. === 3.3.2 (2012-01-16) * Fixed random hard crashes (yeah, again). [Mac OS X] * Fixed crash on Export to HTML. [Windows, Linux] * Added Armenian localization by Hrant Ohanyan. * Added Russian localization by Igor Pavlov. === 3.3.1 (2011-12-02) * Fixed a couple of nasty crashes. === 3.3.0 (2011-11-30) * Added multiple-selection in folder selection dialog for a more efficient folder removal. (#179) * Fixed a crash in the prioritize dialog. (#178) * Fixed a bug where mass marking with a filter would mark more than filtered duplicates. (#181) * Fixed random hard crashes. [Mac OS X] (#183 #184) * Added Czech localization by Aleš Nehyba. * Added Italian localization by Paolo Rossi. === 3.2.1 (2011-10-02) * Fixed a couple of broken action bindings from v3.2.0. === 3.2.0 (2011-09-27) * Added duplicate re-prioritization dialog. (#138) * Added font size preference for duplicate table. (#82) * Added Quicklook support. [Mac OS X] (#21) * Improved behavior of Mark Selected. (#139) * Improved filename sorting. (#169) * Added Chinese (Simplified) localization by Eric Dee. * Tweaked the fairware system. * Upgraded minimum requirements to OS X 10.6 and Ubuntu 11.04. === 3.1.2 (2011-08-25) * Fixed a bug preventing the Folders scan from working. (#172) === 3.1.1 (2011-08-24) * Added German localization by Gregor Tätzner. * Improved OS X Lion compatibility. [Mac OS X] * Made the file collection phase cancellable. (#168) * Fixed glitch in folder window upon selecting a folder state. [Windows, Linux] (#165) * Fixed a text coloring glitch in the results. (#156) * Fixed glitch in the sorting feature of the Folder column. (#161) * Make sure that saved results have the ".dupeguru" extension. [Linux] (#157) === 3.1.0 (2011-04-16) * Added the "Folders" scan type. (#89) * Fixed a couple of crashes. (#140 #149) === 3.0.2 (2011-03-16) * Fixed crash after removing marked dupes. (#140) * Fixed crash on error handling. [Windows] (#144) * Fixed crash on copy/move. [Windows] (#148) * Fixed crash when launching dupeGuru from a very long folder name. [Mac OS X] (#119) * Fixed a refresh bug in directory panel. (#153) * Improved reliability of the "Send to Trash" operation. [Linux] * Tweaked Fairware reminders. === 3.0.1 (2011-01-27) * Restored the context menu which had been broken in 3.0.0. [Mac OS X] (#133) * Fixed a bug where an "unsaved results" warning would be issued on quit even with empty results. (#134) * Removed focus from the cancel button in the progress dialog to avoid accidental cancellations. [Mac OS X] (#135) * Folders added through drag and drop are added to the recent folders list. (#136) * Added a debugging mode. (#132) * Fixed french localization glitches. === 3.0.0 (2011-01-24) * Re-designed the UI. (#129) * Internationalized dupeGuru and localized it to french. (#32) * Changed the format of the help file. (#130) === 2.12.3 (2011-01-01) * Fixed bug causing results to be corrupted after a scan cancellation. (#120) * Fixed crash when fetching Fairware unpaid hours. (#121) * Fixed crash when replacing files with hardlinks. (#122) === 2.12.2 (2010-10-05) * Fixed delta column colors which were broken since 2.12.0. * Fixed column sorting crash. (#108) * Fixed occasional crash during scan. (#106) === 2.12.1 (2010-09-30) * Re-licensed dupeGuru to BSD and made it [Fairware](http://open.hardcoded.net/about/). === 2.12.0 (2010-09-26) * Improved UI with a little revamp. * Added the possibility to place hardlinks to references after having deleted duplicates. [Mac OS X, Linux] (#91) * Added an option to ignore duplicates hardlinking to the same file. [Mac OS X, Linux] (#92) * Added multiple selection in the "Add Directory" dialog. [Mac OS X] (#105) * Fixed a bug preventing drag & drop from working in the Directories panel. [Windows, Linux] === 2.11.1 (2010-08-26) * Fixed HTML exporting which was broken in 2.11.0. === 2.11.0 (2010-08-18) * Added the ability to save results (and reload them) at arbitrary locations. * Improved the way reference files in dupe groups are chosen. (#15) * Remember size/position of all windows between launches. (#102) * Fixed a bug sometimes preventing dupeGuru from reloading previous results. * Fixed a bug sometimes causing the progress dialog to be stuck there. [Mac OS X] (#103) * Removed the Creation Date column, which wasn't displaying the correct value anyway. (#101) === 2.10.1 (2010-07-15) * Fixed a couple of crashes. (#95, #97, #100) === 2.10.0 (2010-04-13) * Improved error messages when files can't be sent to trash, moved or copied. * Added a custom command invocation action. (#12) * Filters are now applied on whole paths. (#4) === 2.9.2 (2010-02-10) * dupeGuru is now 64-bit on Mac OS X! * Fixed a crash upon quitting when support folder is not present. (#83) * Fixed a crash during sorting. (#85) * Fixed selection glitches, especially while renaming. (#93) === 2.9.1 (2010-01-13) * Improved memory usage for Contents scans. (#75) * Improved scanning speed when ref directories are involved. (#77) * Show a message dialog at the end of the scan if no duplicates are found. (#81) * Fixed a bug sometimes causing the small files threshold pref to be ignored. [Mac OS X] (#75) === 2.9.0 (2009-11-03) * Significantly improved speed and memory usage of big contents-based scans. * Added drag & drop support in the Directories panel. (#9) * Fixed a bug causing dupeGuru to be confused if a scanned file was moved during the scan. (#72) * Dropped support for Mac OS X 10.4 (Tiger) === 2.8.2 (2009-10-14) * Improved directory selection in the Directories panel (Windows). (#56) * Fixed a bug preventing dupeGuru from starting on certain machines (Windows). (#68) * Fixed a crash during very big scans. (#70) === 2.8.1 (2009-10-02) * Fixed crash with filtering when regular expressions were enabled. (#60) * Fixed crash when setting directories' state. (Mac OS X) (#66) * Fixed crash with Make Reference when certain filters are applied. (Mac OS X) (#55) * Improved error handling during delete/move/copy actions. (#62 #65) === 2.8.0 (2009-09-07) * Added support for all kinds of bundle (not just applications) (Mac OS X) (#11) * Re-introduced the Export to XHTML feature to Windows. (#14) * Improved Export to XHTML speed. (#14) * Improved Contents scanning speed for large files. (#33) * Improved the grouping algorithm to reduce the number of discarded files in non-exact scans. (#51) * Stopped showing the same file on the 2 sides of the details panel when a ref file is selected. (#50) * Fixed crashes in the Directories panel. (#46) === 2.7.3 (2009-06-20) * Fixed bugs with selection being jumpy during "Make Reference" actions and Power Marker switches. (#3) * Fixed crash happening when a file with non-roman characters couldn't be analyzed. (#30) * Fixed crash sometimes happening during the file collection phase in scanning. (#38) * Restored double-click and right-click behavior lost in the PyQt move (Windows). (#34 #35) === 2.7.2 (2009-06-10) * Fixed an occasional crash on Copy/Move operations. (#16) * Added automatic exclusion for sensible folders (like system folders). (#20) * Fixed an occasional crash when application files were part of the results (Mac OS X). (#25) === 2.7.1 (2009-05-29) * Fixed a bug causing crashes when having application files in the results. * Fixed a bug causing a GUI freeze at the beginning of a scan with a lot of files. * Fixed a bug that sometimes caused a crash when an action was cancelled, and then started again. === 2.7.0 (2009-05-25) * Converted the Windows GUI to Qt. * Improved the reliability of the scanning process. === 2.6.1 (2009-03-27) * **Fixed** an occasional crash caused by permission issues. * **Fixed** a bug where the "X discarded" notice would show a too large number of discarded duplicates. === 2.6.0 (2008-09-10) * **Added** a small file threshold preference. * **Added** a notice in the status bar when matches were discarded during the scan. * **Improved** duplicate prioritization (smartly chooses which file you will keep). * **Improved** scan progress feedback. * **Improved** responsiveness of the user interface for certain actions. === 2.5.4 (2008-08-10) * **Improved** the speed of results loading and saving. * **Fixed** a crash sometimes occurring during duplicate deletion. === 2.5.3 (2008-07-08) * **Improved** unicode handling for filenames. dupeGuru will now find a lot more duplicates if your files have non-ascii characters in it. * **Fixed** "Clear Ignore List" crash in Windows. === 2.5.2 (2008-01-10) * **Improved** the handling of low memory situations. * **Improved** the directory panel. The "Remove" button changes to "Put Back" when an excluded directory is selected. * **Improved** scan, delete and move speed in situations where there were a lot of duplicates. * **Fixed** occasional crashes when moving bundles (such as .app files). * **Fixed** occasional crashes when moving a lot of files at once. === 2.5.1 (2007-11-22) * **Added** the "Remove empty folders" option. * **Fixed** results load/save issues. * **Fixed** occasional status bar inaccuracies when the results are filtered. === 2.5.0 (2007-09-15) * **Added** post scan filtering. * **Fixed** issues with the rename feature under Windows * **Fixed** some user interface annoyances under Windows === 2.4.8 (2007-04-14) * **Improved** UI responsiveness (using threads) under Mac OS X. * **Improved** result load/save speed and memory usage. === 2.4.7 (2007-03-10) * **Fixed** a "bad file descriptor" error occasionally popping up. * **Fixed** a bug with non-latin directory names. === 2.4.6 (2007-02-10) * **Added** Re-orderable columns. In fact, I re-added the feature which was lost in the C# conversion in 2.4.0 (Windows). * **Changed** the behavior of the scanning engine when setting the hardness to 100. It will now only match files that have their words in the same order. * **Fixed** a bug with all the Delete/Move/Copy actions with certain kinds of files. === 2.4.5 (2007-01-11) * **Fixed** a bug with the Move action. === 2.4.4 (2007-01-07) * **Fixed** a "ghosting" bug. Dupes deleted by dupeGuru would sometimes come back in subsequent scans (Windows). * **Fixed** bugs sometimes making dupeGuru crash when marking a dupe (Windows). * **Fixed** some minor visual glitches (Windows). === 2.4.3 (2006-12-08) * **Fixed** a mishandling of ".app" files (OS X). * **Fixed** a bug preventing files from "reference" directories to be displayed in blue in the results (Windows). * **Fixed** a bug preventing some files to be sent to the recycle bin (Windows). * **Fixed** a bug in the packaging preventing certain Windows configurations to start dupeGuru at all. === 2.4.2 (2006-11-18) * **Fixed** a bug with directory states. === 2.4.1 (2006-11-15) * **Fixed** a bug causing the ignore list not to be saved. * **Fixed** a bug sometimes making delete and move operations stall. === 2.4.0 (2006-11-10) * **Changed** the Windows interface. It is now .NET based. * **Added** an auto-update feature to the windows version. * **Changed** the way power marking works. It is now a mode instead of a separate window. * **Changed** the "Size (MB)" column for a "Size (KB)" column. The values are now "ceiled" instead of rounded. Therefore, a size "0" is now really 0 bytes, not just a value too small to be rounded up. It is also the case for delta values. * **Removed** the min word length/count options. These came from Mp3 Filter, and just aren't used anymore. Word weighting does pretty much the same job. === 2.3.4 (2006-11-07) * **Improved** speed and memory usage of the scanning engine, again. Does it mean there was a lot of improvements to be made? Nah... === 2.3.3 (2006-11-02) * **Improved** speed and memory usage of the scanning engine, especially when the scan results in a lot of duplicates. * Now I wonder if Sparkle is going to work well... === 2.3.2 (2006-10-16) * **Added** an auto-update feature in the Mac OS X version (with Sparkle). * **Fixed** a bug preventing some duplicate reports to be created correctly under Windows. === 2.3.1 (2006-10-02) * **Fixed** a bug preventing some duplicates to be found, especially when scanning lots of files. === 2.3.0 (2006-09-22) * **Added** XHTML export feature. === 2.2.10 (2006-08-31) * **Added** sticky columns. * **Fixed** an issue with file caching between scans. * **Fixed** an issue preventing some duplicates from being deleted/moved/copied. === 2.2.9 (2006-08-27) * **Fixed** an issue with ignore list and unicode. * **Fixed** an issue with file attribute fetching sometimes causing dupeGuru to crash. * **Fixed** an issue in the directories panel under Windows. === 2.2.8 (2006-08-17) * **Fixed** an issue in the duplicate seeking engine preventing some duplicates to be found. === 2.2.7 (2006-08-12) * **Improved** unicode support. * **Improved** the "Reveal in Finder" ("Open Containing Folder" in Windows) feature so it selects the file in the folder it opens. === 2.2.6 (2006-08-07) * **Improved** the ignore list system. * dupeGuru is now a Universal application on Mac OS X. === 2.2.5 (2006-07-26) * **Improved** application (.app) dupe detection on Mac OS X. * **Fixed** an issue that occasionally made dupeGuru crash on startup. === 2.2.4 (2006-06-27) * **Fixed** an issue with Move and Copy features. === 2.2.3 (2006-06-15) * **Improved** duplicate scanning speed. * **Added** a warning that a file couldn't be renamed if a file with the same name already exists. === 2.2.2 (2006-06-07) * **Added** "Rename Selected" feature. * **Fixed** some minor issues with "Reload Last Results" feature. * **Fixed** ignore list issues. === 2.2.1 (2006-05-22) * **Fixed** occasional progress bar woes under Windows. * **Fixed** a bug in the registration system under Windows. * Nothing has been changed in the Mac OS X version, but I want to keep version in sync. === 2.2.0 (2006-05-10) * **Added** destination path re-creation options. * **Added** an ignore list. * **Changed** the main icon. * **Improved** dramatically the delta values feature. === 2.1.2 (2006-04-18) * **Added** the "Match similar words" option. * **Fixed** Power marking issues under Mac. === 2.1.1 (2006-04-14) * **Added** the "Display delta values" option. * **Improved** Power marking sorting speed under Mac. * **Fixed** Power marking sorting issues. === 2.1.0 (2006-04-03) * **Added** the Power Marker feature. * **Fixed** a column sorting bug. The results would sometimes lose their sort order. * **Fixed** a bug with the Make Reference feature. The results sometimes wasn't correctly refreshed after the reference switch. === 2.0.1 (2006-03-23) * **Fixed** an issue occasionally occurring when trying to reload results from removable media that is no longer present. === 2.0.0 (2006-03-17) * Complete rewrite. * Now runs on Mac OS X. === 1.0.0 (2004-09-24) * Initial release. dupeguru-4.3.1/help/changelog.tmpl000066400000000000000000000007361426171743600171550ustar00rootroot00000000000000:tocdepth: 1 Changelog ========= **About the word "crash":** When reading this changelog, you might be alarmed at the number of fixes for "crashes". Be aware that when the word "crash" is used here, it refers to "soft crashes" which don't cause the application to quit. You simply get an error window that asks you if you want to send the crash report to Hardcoded Software. Crashes that cause the application to quit are called "hard crashes" in this changelog. {changelog} dupeguru-4.3.1/help/conf.tmpl000066400000000000000000000140261426171743600161500ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # dupeGuru documentation build configuration file, created by # sphinx-quickstart on Wed Jan 12 13:20:15 2011. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os import re # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # for autodocs sys.path.insert(0, os.path.abspath(os.path.join('..', '..'))) # -- Misc fixes for autodoc def fix_nulljob_in_sig(app, what, name, obj, options, signature, return_annotation): if signature: signature = re.sub(r"", "nulljob", signature) return signature, return_annotation def setup(app): app.connect('autodoc-process-signature', fix_nulljob_in_sig) autodoc_member_order = 'groupwise' # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.todo', 'sphinx.ext.autodoc', 'sphinx.ext.autosummary'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'dupeGuru' copyright = u'2016, Hardcoded Software' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '{version}' # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. language = '{language}' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'haiku' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = False # If false, no index is generated. # html_use_index = False # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'dupeGurudoc' todo_include_todos = True dupeguru-4.3.1/help/de/000077500000000000000000000000001426171743600147125ustar00rootroot00000000000000dupeguru-4.3.1/help/de/faq.rst000066400000000000000000000220351426171743600162150ustar00rootroot00000000000000Häufig gestellte Fragen ========================== .. topic:: What is dupeGuru? .. only:: edition_se DupeGuru ist ein Tool zum Auffinden von Duplikaten auf Ihrem Computer. Es kann entweder Dateinamen oder Inhalte scannen. Der Dateiname-Scan stellt einen lockeren Suchalgorithmus zur Verfügung, der sogar Duplikate findet, die nicht den exakten selben Namen haben. .. only:: edition_me dupeGuru Music Edition ist ein Tool zum Auffinden von Duplikaten in Ihrer Musiksammlung. Es kann seine Suche auf Dateinamen, Tags oder Inhalte basieren. Der Dateiname-Scan und Tag-Scan stellt einen lockeren Suchalgorithmus zur Verfügung, der sogar Dateinamen und Tags findet, die nicht den exakt selben Namen haben. .. only:: edition_pe dupeGuru Picture Edition (kurz PE) ist ein Tool zum Auffinden von doppelten Bildern auf Ihrem Computer. Es findet nicht nur exakte Übereinstimmungen, sondern auch Duplikate unterschiedlichen Dateityps (PNG, JPG, GIF etc..) und Qualität. .. topic:: Was macht es besser ala andere Duplikatscanner? Die Scan-Engine ist extrem flexibel. Sie können sie modifizieren, um die Art von Ergebnissen zu bekommen die Sie möchten. Sie können mehr über die dupeGuru Modifikationen finden auf der :doc:`Einstellungen Seite `. .. topic:: Wie sicher ist dupeGuru? Sehr sicher. DupeGuru wurde entwickelt, um sicherzustellen keine Dateien zu löschen, die nicht gelöscht werden sollen. Erstens, es existiert ein Referenzordnersystem welches Ordner definiert, die auf **keinen** Fall angefasst werden sollen. Dann gibt es noch das Referenzgruppensystem, das sicherstellt das **immer** ein Mitglied einer Duplikatgruppe behalten wird. .. topic:: Was sind die Demo-Einschränkungen von dupeGuru? Keine, dupeGuru ist `Fairware `_. .. topic:: Die Markierungsbox einer Datei, die ich löschen möchte, ist deaktiviert. Was muss ich tun? Sie können die Referenz nicht markieren (die erste Datei einer Duplikatgruppe). Wie auch immer, Sie können ein Duplikat zur Referenz befördnern. Wenn eine Datei, die Sie markieren möchten, eine Referenz ist, muss ein Duplikat der Gruppe zur Referenz gemacht werden, indem man es auswählt und auf **Aktionen-->Mache Ausgewählte zur Referenz** gehen. Befindet sich die Referenzdatei in einem Referenzordner (Dateiname in blauen Buchstaben), kann sie nicht aus der Referenzposition entfernt werden. .. topic:: ich habe einen Ordner aus dem ich wirklich nichts löschen möchte. Möchten Sie sicherstellen, das dupeGuru niemals Dateien aus einem bestimmten Ordner löscht, dann versetzen sie den Ordner in den **Referenzzustand**. Siehe :doc:`folders`. .. topic:: Was bedeutet diese '(X verworfen)' Nachricht in der Statusbar? In einigen Fällen werden manche Treffer aus Sicherheitsgründen nicht in den Ergebnissen angezeigt. Lassen Sie mich ein Beispiel konstruieren. Wir haben 3 Datein: A, B und C. Wir scannen sie mit einer niedrigen Filterempfindlichkeit. Der Scanner findet heraus das A mit B und C übereinstimmt, aber B **nicht** mit C übereinstimmt. Hier hat dupeGuru ein Problem. Es kann keine Duplikatgruppe erstellen mit A, B und C, weil nicht alle Dateien der Gruppe zusammenpassen. Es könnte 2 Gruppen erstellen: eine A-B Gruppe und eine A-C Gruppe, aber es dies aus Sicherheitsbedenken nicht tun. Denken wir darüber nach: Wenn B nicht zu C passt, heißt das, das entweder B oder C keine echten Duplikate sind. Wären es 2 Gruppen (A-B und A-C), würden Sie damit enden sowohl B als auch C zu löschen. Und ist keine der Beiden ein Duplikat, möchten Sie das ganz sicher nicht tun, richtig? Also verwirft dupeGuru in diesem Fall den A-C Treffer (und fügt eine Notiz in der Statusbar hinzu). Folglich, wenn Sie B löschen und den Scan erneut durchführen, haben Sie einen A-C Treffer nächstes Mal in den Ergebnissen. .. topic:: Ich möchte alle Dateien aus einem bestimmten Ordner markieren. Was kann ich tun? Aktiveren Sie den :doc:`Nur Duplikate ` Modus und klicken auf die Ordnerspalte, um die Duplikate nach Ordner zu sortieren. Es wird dann einfach sein, alle Duplikate aus dem selben Ordner auszuwählen und auf die Leertaste zu drücken, um sie alle zu markieren. .. only:: edition_se or edition_pe .. topic:: Ich möchte alle Dateien löschen, deren Größe sich um mehr als 300 KB von ihrer Referenz unterscheidet. Was kann ich tun? * Aktivieren Sie den :doc:`Nur Duplikate ` Modus. * Aktivieren Sie den **Deltawerte** Modus. * Gehen Sie auf die "Größe" Spalte, um die Ergebnisse nach Größe zu sortieren. * Alle Duplikate unter -300 auswählen. * Klicken Sie auf **Entferne Ausgewählte von den Ergebnissen**. * Alle Duplikate über 300 auswählen * Klicken Sie auf **Entferne Ausgewählte von den Ergebnissen**. .. topic:: Ich möchte meine zuletzt geänderten Dateien zur Referenz machen. Was kann ich tun? * Aktivieren Sie den :doc:`Nur Duplikate ` Modus. * Aktivieren Sie den **Deltawerte** Modus. * Gehen Sie auf die "Modifikation" Spalte, um die Ergebnisse nach Änderungsdatum zu sortieren. * Klicken Sie erneut auf die "Modifikation" Spalte, um die Reihenfolge umzukehren. * Wählen Sie alle Duplikate über 0. * Klicken Sie auf **Mache Ausgewählte zur Referenz**. .. topic:: Ich möchte alle Duplikate mit dem Wort copy markieren. Wie mache ich das? * **Windows**: Klicken Sie auf **Aktionen --> Filter anwenden**, tippen "copy" und klicken auf OK. * **Mac OS X**: Geben Sie "copy" in das "Filter" Feld in der Werkzeugleiste ein. * Klicken Sie **Markieren --> Alle Markieren**. .. only:: edition_me .. topic:: Ich möchte alle Stücke markieren, die mehr als 3 Sekunden von ihrer Referenz verschieden sind. Was kann ich tun? * Aktivieren Sie den :doc:`Nur Duplikate ` Modus. * Aktivieren Sie den **Deltawerte** Modus. * Klicken Sie auf die "Zeit" Spalte, um nach Zeit zu sortieren. * Wählen Sie alle Duplikate unter -00:03. * Klicken Sie auf **Entferne Ausgewählte von den Ergebnissen**. * Wählen Sie alle Duplikate über 00:03. * Klicken Sie auf **Entferne Ausgewählte von den Ergebnissen**. .. topic:: Ich möchte meine Stücke mit der höchsten Bitrate zur Referenz machen. Was kann ich tun? * Aktivieren Sie den :doc:`Nur Duplikate ` Modus. * Aktivieren Sie den **Deltawerte** Modus. * Klicken Sie auf die "Bitrate" Spalte, um nach Bitrate zu sortieren. * Klicken Sie erneut auf die "Bitrate" Spalte, um die Reihenfolge umzukehren. * Wählen Sie alle Duplikate über 0. * Klicken Sie auf **Mache Ausgewählte zur Referenz**. .. topic:: Ich möchte nicht das [live] und [remix] Versionen meiner Stücke als Duplikate erkannt werden. Was kann ich tun? Ist Ihre Vergleichsschwelle niedrig genug, werden möglicherweise die live und remix Versionen in der Ergebnisliste landen. Das kann nicht verhindert werden, aber es gibt die Möglichkeit die Ergebnisse nach dem Scan zu entfernen, mittels dem Filter. Möchten Sie jedes Stück mit irgendetwas in eckigen Klammern [] im Dateinamen entfernen, so: * **Windows**: Klicken Sie auf **Aktionen --> Filter anwenden**, geben "[*]" ein und klicken OK. * **Mac OS X**: Geben Sie "[*]" in das "Filter" Feld der Werkzeugleiste ein. * Klicken Sie auf **Markieren --> Alle Markieren**. * Klicken Sie auf **Entferne Ausgewählte von den Ergebnissen**. .. topic:: Ich habe versucht, meine Duplikate in den Mülleimer zu verschieben, aber dupeGuru sagt es ist nicht möglich. Warum? Was kann ich tun? Meistens kann dupeGuru aufgrund von Dateirechten keine Dateien in den Mülleimer schicken. Sie brauchen **Schreib** Rechte für Dateien, die in den Mülleimer sollen. Wenn Sie nicht vertraut mit Kommandozeilenwerkzeugen sind, können dafür auch Dienstprogramme wie `BatChmod `_ verwendet werden, um die Dateirechte zu reparieren. Wenn dupeGuru sich nach dem Reparieren der Recht immer noch verweigert, könnte es helfen die Funktion "Verschiebe Markierte nach..." als Workaround zu verwenden. Anstelle die Dateien in den Mülleimer zu schieben, senden SIe sie in einen temporären Ordner, den Sie dann manuell löschen können. .. only:: edition_pe Wenn Sie versuchen *iPhoto* Bilder zu löschen, dann ist der Grund des Versagens ein Anderer. Das Löschen schlägt fehl, weil dupeGuru nicht mit iPhoto kommunizieren kann. Achten Sie darauf nicht mit iPhoto herumzuspielen, während dupeGuru arbeitet, damit das Löschen funktioniert. Außerdem scheint das Applescript System manchmal zu vergessen wo sich iPhoto befindet, um es zu starten. Es hilft in diesen Fällen, wenn Sie iPhoto starten **bevor** Duplikate in den Mülleimer verschoben werden. Wenn dies alles fehlschlägt, kontaktieren Sie `HS support `_, wir werden das Problem lösen. .. todo:: This FAQ qestion is outdated, see english version. dupeguru-4.3.1/help/de/folders.rst000066400000000000000000000037531426171743600171120ustar00rootroot00000000000000Ordnerauswahl ================ Das erste Fenster das Sie sehen, wenn dupeGuru gestartet wird, ist das Ordnerauswahl Fenster. Dieses Fenster enthält die Liste der Ordner die durchsucht werden, wenn Sie **Scan** wählen. Das Fenster ist leicht zu bedienen. Wollen Sie einen Ordner hinzufügen, klicken Sie auf den **+** Knopf. Haben Sie bereits vorher Ordner hinzugefügt, erscheint ein Popup-Menü mit einer Liste der zuletzt hinzugefügten Ordner. Sie können einen davon auswählen, indem Sie darauf klicken. Wenn Sie auf den ersten Eintrag der Liste klicken, **Neuen Ordner hinzufügen...**, werden Sie nach einem Ordner zum Hinzufügen gefragt. Nutzen Sie dupeGuru zum ersten Mal, erscheint kein Menü und Sie werden direkt nach einem Ordner gefragt. Ein alternativer Weg zum Hinzufügen der Ordner ist, sie auf die Liste zu ziehen. Um einen Ordner zu entfernen, wählen Sie ihn aus und klicken auf **-**. Wenn Sie einen Unterordner auswählen, wird der ausgewählte Ordner in den **Ausgeschlossen** Zustand versetzt (siehe unten), anstatt entfernt zu werden. Ordnerzustände -------------- Jeder Ordner kann in einem von 3 Zuständen sein: * **Normal:** Duplikate in diesem Ordner können gelöscht werden. * **Referenz:** Duplikate in diesem Ordner können **nicht** gelöscht werden. Dateien dieses Ordners können sich nur in der **Referenz** Position einer Duplikatgruppe befinden. Ist mehr als eine Datei des Referenzordners in derselben Duplikatgruppe, so wird nur Eine behalten. Die Anderen werden aus der Gruppe entfernt. * **Ausgeschlossen:** Dateien in diesem Verzeichnis sind nicht im Scan eingeschlossen. Der Standardzustand eines Ordners ist natürlich **Normal**. Sie können den **Referenz** Zustand für Ordner nutzen, in denen auf keinen Fall eine Datei gelöscht werden soll. Wenn sie einen Zustand für ein Verzeichnis setzen, erben alle Unterordner automatisch diesen Zustand, es sei denn Sie ändern den Zustand der Unterordner explizit. .. todo:: Add iPhoto/Aperture/iTunes libraries notes dupeguru-4.3.1/help/de/index.rst000066400000000000000000000024351426171743600165570ustar00rootroot00000000000000dupeGuru Hilfe =============== .. only:: edition_se Dieses Dokument ist auch auf `Englisch `__ und `Französisch `__ verfügbar. .. only:: edition_se or edition_me dupeGuru ist ein Tool zum Auffinden von Duplikaten auf Ihrem Computer. Es kann entweder Dateinamen oder Inhalte scannen. Der Dateiname-Scan stellt einen lockeren Suchalgorithmus zur Verfügung, der sogar Duplikate findet, die nicht den exakten selben Namen haben. .. only:: edition_pe dupeGuru Picture Edition (kurz PE) ist ein Tool zum Auffinden von doppelten Bildern auf Ihrem Computer. Es findet nicht nur exakte Übereinstimmungen, sondern auch Duplikate unterschiedlichen Dateityps (PNG, JPG, GIF etc..) und Qualität. Obwohl dupeGuru auch leicht ohne Dokumentation genutzt werden kann, ist es sinnvoll die Hilfe zu lesen. Wenn Sie nach einer Führung für den ersten Duplikatscan suchen, werfen Sie einen Blick auf die :doc:`Schnellstart ` Sektion Es ist eine gute Idee dupeGuru aktuell zu halten. Sie können die neueste Version auf der http://dupeguru.voltaicideas.net finden. Inhalte: .. toctree:: :maxdepth: 2 quick_start folders preferences results reprioritize faq changelog dupeguru-4.3.1/help/de/preferences.rst000066400000000000000000000225041426171743600177500ustar00rootroot00000000000000Einstellungen ============= .. only:: edition_se **Scan Typ:** Diese Option bestimmt nach welcher Eigenschaft die Dateien in einem Duplikate Scan verglichen werden. Wenn Sie **Dateiname** auswählen, wird dupeGuru jeden Dateinamen Wort für Wort vergleichen und, abhängig von den unteren Einstellungen, feststellen ob genügend Wörter übereinstimmen, um 2 Dateien als Duplikate zu betrachten. Wenn Sie **Inhalt** wählen, werden nur Dateien mit dem exakt gleichen Inhalt zusammenpassen. Der **Ordner** Scan Typ ist etwas speziell. Wird er ausgewählt, scannt dupeGuru nach doppelten Ordnern anstelle von Dateien. Um festzustellen ob 2 Ordner identisch sind, werden alle Datein im Ordner gescannt und wenn die Inhalte aller Dateien der Ordner übereinstimmen, werden die Ordner als Duplikate erkannt. **Filterempfindlichkeit:** Wenn Sie den **Dateiname** Scan Typ wählen, bestimmt diese Option wie ähnlich 2 Dateinamen für dupeGuru sein müssen, um Duplikate zu sein. Ist die Empfindlichkeit zum Beispiel 80, müssen 80% der Worte der 2 Dateinamen übereinstimmen. Um den Übereinstimmungsanteil herauszufinden, zählt dupeGuru zuerst die Gesamtzahl der Wörter **beider** Dateinamen, dann werden die gleichen Wörter gezählt (jedes Wort zählt als 2) und durch die Gesamtzahl der Wörter dividiert. Ist das Resultat größer oder gleich der Filterempfindlichkeit, haben wir ein Duplikat. Zum Beispiel, "a b c d" und "c d e" haben einen Übereinstimmungsanteil von 57 (4 gleiche Wörter, insgesamt 7 Wörter). .. only:: edition_me **Scan Typ:** Diese Option bestimmt nach welcher Eigenschaft die Dateien in einem Duplikate Scan verglichen werden. Die Beschaffenheit des Duplikate Scans hängt hauptsächlich davon ab, was Sie für diese Option auswählen. * **Dateiname:** Der Dateiname jedes Stücks wird in einzelne Wörter zerlegt und verglichen, um den Übereinstimmungsanteil zu berechnen. Ist das Resultat größer oder gleich der **Filterempfindlichkeit** (siehe unten für mehr Details), wird dupeGuru die beiden Stücke als Duplikate erkennen. * **Dateiname - Felder:** Wie **Dateiname**, außer das, nachdem der Dateiname in Wörter geteilt wurde, diese Wörter in Felder gruppiert werden. Der Feldseparator ist " - ". Der endgültige Übereinstimmungsanteil ist der kleinste Übereinstimmungssatz zwischen den Feldern. Also, "Ein Künstler - Der Titel" und "Ein Künstler - Anderer Titel" hätte eine Übereinstimmung von 50 (Bei einem **Dateiname** Scan wäre es 75). * **Dateiname - Felder (keine Reihenfolge):** Wie **Dateiname - Felder**, außer das die Feldreihenfolge keine Rolle spielt. Also, "Ein Künstler - Der Titel" und "Der Titel - Ein Künstler" hätte eine Übereinstimmung von 100 anstelle von 0. * **Tags:** Diese Methode liest die Tags (Metadaten) jedes Stücks und vergleicht ihre Werte. Es wird, wie in **Dateiname - Felder**, die niedrigste Übereinstimmung als endgültiger Übereinstimmungsanteil betrachtet. * **Inhalt:** Diese Scanmethode nutzt den Inhalt des Stücks, um Duplikate zu erkennen. Damit 2 Stücke mit dieser Methode gleich sind, müssen sie **exakt den selben Inhalt** haben. * **Audioinhalt:** Das selbe wie Inhalt, aber nur der Audioinhalt wird verglichen (ohne Metadaten). **Filterempfindlichkeit:** Wenn Sie den **Dateiname** Scan Typ wählen, bestimmt diese Option wie ähnlich 2 Dateinamen für dupeGuru sein müssen, um Duplikate zu sein. Ist die Empfindlichkeit zum Beispiel 80, müssen 80% der Worte der 2 Dateinamen übereinstimmen. Um den Übereinstimmungsanteil herauszufinden, zählt dupeGuru zuerst die Gesamtzahl der Wörter **beider** Dateinamen, dann werden die gleichen Wörter gezählt (jedes Wort zählt als 2) und durch die Gesamtzahl der Wörter dividiert. Ist das Resultat größer oder gleich der Filterempfindlichkeit, haben wir ein Duplikat. Zum Beispiel, "a b c d" und "c d e" haben einen Übereinstimmungsanteil von 57% (4 gleiche Wörter, insgesamt 7 Wörter). **Tags zu scannen:** Bei der Nutzung des **Tags** Scan Typs, können Sie wählen welche Tags verglichen werden sollen. .. only:: edition_se or edition_me **Wortgewichtung:** Wenn Sie den **Dateiname** Scan Type nutzen, ändert diese Option leicht die Berechnung der Übereinstimmung. Mit Wortgewichtung hat jedes Wort nicht mehr den Wert 1 in der Duplikatezählung und der Gesamtwortzahl, sondern einen Wert der sich aus der Gesamtzahl der Buchstaben des Wortes ergibt. Mit Wortgewichtung hätte "ab cde fghi" und "ab cde fghij" eine Übereinstimmung von 53% (Gesamt 19 Buchstaben, 10 gleiche Buchstaben (4 für "ab" und 6 für "cde")). **Ähnliche Wörter gleich** Wird diese Option angeschaltet, zählen ähnliche Wörter als Übereinstimmung. Zum Beispiel hätte mit dieser Option "The White Stripes" und "The White Stripe" eine Übereinstimmung von 100 anstelle von 0. **Warnung:** Nutzen Sie diese Option mit Vorsicht. Es ist wahrscheinlich, das sie eine hohe Anzahl an Falschpositiven erhalten. Wie auch immer, Sie werden Duplikate finden, die Sie sonst nie gefunden hätten. Der Suchdurchlauf wird außerdem mit dieser Option etwas länger dauern. .. only:: edition_pe **Scan Typ:** Diese option bestimmt, welcher Scan Typ bei Ihren Bildern angewendet wird. Der **Inhalte** Scan Typ vergleicht den Inhalt der Bilder auf eine ungenaue Art und Weise (so werden nicht nur exakte Duplikate gefunden, sondern auch Ähnliche). Der **EXIF Zeitstempel** Scan Typ schaut auf die EXIF Metadaten der Bilder (wenn vorhanden) und erkennt Bilder die den Selben haben. Er ist viel schneller als der Inhalte Scan. **Warnung:** Veränderte Bilder behalten oft den selben EXIF Zeitstempel, also achten Sie auf Falschpositive bei der Nutzung dieses Scans. **Filterempfindlichkeit:** *Nur Inhalte Scan.* Je höher diese Einstellung, desto strenger ist der Filter (Mit anderen Worten, desto weniger Ergebnisse erhalten Sie). Die meisten Bilder der selben Qualität stimmen zu 100% überein, selbst wenn das Format anders ist (PNG und JPG zum Beispiel). Wie auch immer, wenn ein PNG mit einem JPG niederiger Qualität übereinstimmen soll, muss die Filterempfindlichkeit kleiner als 100 sein. Die Voreinstellung, 95, ist eine gute Wahl. **Bilder unterschiedlicher Abmessung gleich:** Wird diese Box gewählt, dürfen Bilder unterschiedlicher Abmessung in einer Duplikategruppe sein.. **Dateitypen dürfen gemischt werden:** Wird diese Box gewählt, dürfen Duplikategruppen Bilder mit unterschiedlichen Dateierweiterungen enthalten. **Ignoriere Duplikate die mit derselben Datei verlinkt sind:** Ist diese Option aktiviert, wird dupeGuru überprüfen ob Duplikate auf den selben `inode `_ verweisen. Wenn sie es tun, werden sie nicht als Duplikat erkannt. (Nur für OS X und Linux) **Nutze reguläre Ausdrücke beim Filtern:** Ist diese Option aktiviert, wird die Filterfunktion Ihre Filteranfrage als **regulären Ausdruck** interpretieren. Sie zu erklären ist außerhalb des Aufgabenbereiches dieser Dokumentation. Ein guter Platz zum Starten ist `regular-expressions.info `_. **Entferne leere Ordner nach dem Löschen oder Verschieben:** Ist diese Option aktiviert, werden Ordner gelöscht nachdem eine Datei gelöscht oder verschoben wurde und der Ordner leer ist. **Copy and Move:** Determines how the Copy and Move operations (in the Action menu) will behave. * **Zum Ziel:** Alle Dateien werden direkt in das ausgwählte Verzeichnis gesendet, ohne zu versuchen den Quellpfad wiederherzustellen * **Relativen Pfad neu erstellen:** Der Pfad der Quelldatei wird im Zielverzeichnis wiederhergestellt bis zur Wurzelauswahl im Verzeichnis Panel. Zum Beispiel, wenn Sie ``/Users/foobar/SomeFolder`` zu ihrem Verzeichnis Panel hinzufügen und ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` zu dem Ziel ``/Users/foobar/MyDestination`` verschieben, wird das endgültige Ziel der Datei ``/Users/foobar/MyDestination/SubFolder`` sein (``SomeFolder`` wurde vom Pfad der Quelldatei im endgültigen Ziel abgetrennt.). * **Absoluten Pfad neu erstellen:** Der Pfad der Quelldatei wird im Zielverzeichnis vollständig wiederhergestellt. Zum Beispiel, wenn Sie ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` zu dem Ziel ``/Users/foobar/MyDestination`` verschieben, wird das endgültige Ziel der Datei ``/Users/foobar/MyDestination/Users/foobar/SomeFolder/SubFolder`` sein. Auf jeden Fall behandelt dupeGuru Namenskonflikte indem es dem Ziel-Dateinamen eine Nummer voranstellt, wenn der Dateiname bereits im Zielverzeichnis existiert. **Eigener Befehl:** Diese Einstellung bestimmt den Befehl der durch "Führe eigenen Befehl aus" ausgeführt wird. Sie können jede externe Anwendung durch diese Aktion aufrufen. Dies ist zum Beispiel hilfreich, wenn Sie eine gute diff-Anwendung installiert haben. Das Format des Befehls ist das Selbe wie in einer Befehlszeile, außer das 2 Platzhalter vorhanden sind: **%d** und **%r**. Diese Platzhalter werden durch den Pfad des markierten Duplikates (%d) und dem Pfad der Duplikatereferenz ersetzt (%r). Wenn der Pfad Ihrer ausführbaren Datei Leerzeichen enthält, so schließen sie ihn bitte mit "" Zeichen ein. Sie sollten auch Platzhalter mit den Zitatzeichen einschließen, denn es ist möglich, das die Pfade der Duplikate und Referenzen ebenfalls Leerzeichen enthalten. Hier ist ein Beispiel eines eigenen Befehls:: "C:\Program Files\SuperDiffProg\SuperDiffProg.exe" "%d" "%r" dupeguru-4.3.1/help/de/quick_start.rst000066400000000000000000000020761426171743600200020ustar00rootroot00000000000000Schnellstart ============ Damit Sie sich schnell mit dupeGuru zurechtfinden, machen wir für den Anfang einen Standardscan mit den Voreinstellungen. * dupeGuru starten. * Zu scannende Ordner entweder mit drag & drop oder dem "+" Knopf auswählen. * Drücken Sie auf **Scan**. * Warten Sie bis der Scanvorgang fertig ist. * Betrachten Sie jedes Duplikat (die eingerückten Dateien) und überprüfen ob es wirklich ein Duplikat der Referenzdatei ist (die obere nicht eingerückte Datei ohne Markierungsfeld). * Wenn eine Datei kein Duplikat ist, wählen Sie es aus und drücken auf **Aktionen-->Entferne Ausgewählte aus den Ergebnissen**. * Erst wenn Sie sicher sind, das keine Falsch-Duplikate mehr in den Ergebnissen sind, drücken Sie auf **Bearbeiten-->Alle markieren**, und dann **Aktionen-->Verschiebe Markierte in den Mülleimer**. Das war nur ein einfacher Scan. Es gibt viele Optionen mit denen der Suchdurchlauf beeinflusst werden und einige Methoden zur Begutachtung und Veränderung der Ergebnisliste. Um mehr über sie zu erfahren, lesen Sie die restlichen Hilfedateien. dupeguru-4.3.1/help/de/reprioritize.rst000066400000000000000000000033411426171743600201740ustar00rootroot00000000000000Re-Prioritizing duplicates ========================== dupeGuru tries to automatically determine which duplicate should go in each group's reference position, but sometimes it gets it wrong. In many cases, clever dupe sorting with "Delta Values" and "Dupes Only" options in addition to the "Make Selected into Reference" action does the trick, but sometimes, a more powerful option is needed. This is where the Re-Prioritization dialog comes into play. You can summon it through the "Re-Prioritize Results" item in the "Actions" menu. This dialog allows you to select criteria according to which a reference dupe will be selected in each dupe group. The list of available criteria is on the left and the list of criteria you've selected is on the right. A criteria is a category followed by an argument. For example, "Size (Highest)" means that the dupe with the biggest size will win. "Folder (/foo/bar)" means that dupes in this folder will win. To add a criterion to the rightmost list, first select a category in the combobox, then select a subargument in the list below, and then click on the right pointing arrow button. The order of the list on the right is important (you can re-order items through drag & drop). When picking a dupe for reference position, the first criterion is used. If there's a tie, the second criterion is used and so on and so on. For example, if your arguments are "Size (Highest)" and then "Filename (Doesn't end with a number)", the reference file that will be picked in a group will be the biggest file, and if two or more files have the same size, the one that has a filename that doesn't end with a number will be used. When all criteria result in ties, the order in which dupes previously were in the group will be used.dupeguru-4.3.1/help/de/results.rst000066400000000000000000000255411426171743600171540ustar00rootroot00000000000000Ergebnisse ========== Sobald dupeGuru den Duplikatescan beendet hat, werden die Ergebnisse in Form einer Duplikate-Gruppenliste gezeigt. Über Duplikatgruppen -------------------- Eine Duplikatgruppe ist eine Gruppe von übereinstimmenden Dateien. Jede Gruppe hat eine **Referenzdatei** und ein oder mehrere **Duplikate**. Die Referenzdatei ist die 1. Datei der Gruppe. Die Auswahlbox ist deaktiviert. Darunter befinden sich die eingerückten Duplikate. Sie können Duplikate markieren, aber niemals die Referenzdatei der Gruppe. Das ist eine Sicherheitsmaßnahme, die dupeGuru davon abhält nicht nur die Duplikate zu löschen, sondern auch die Referenzdatei. Sie wollen sicher nicht das das passiert, oder? Welche Dateien Referenz oder Duplikate sind hängt zuerst von ihrem Ordnerzustand ab. Eine Datei von einem Referenzordner ist immer Referenz einer Duplikatgruppe. Sind alle Dateien aus normalen Ordnern, bestimmt die Größe welche Datei die Referenz einer Gruppe sein wird. DupeGuru nimmt an, das Sie immer die größte Datei behalten wollen. Also übernimmt die größte Datei die Referenzposition. Sie können die Referenzdatei manuell verändern. Um das zu tun, wählen Sie das Duplikat aus, das zur Referenz befördert werden soll und drücken auf **Aktionen-->Mache Ausgewählte zur Referenz**. Ergebnisse beurteilen --------------------- Obwohl Sie einfach auf **Markieren-->Alles markieren** gehen und dann **Aktionen-->Verschiebe Markierte in den Mülleimer** ausführen können, um schnell alle Duplikate zu löschen, ist es sinnvoll erst alle Duplikate zu betrachten, bevor man sie löscht. Um die Überprüfung zu erleichtern, können Sie das **Detail Panel** öffnen. Dieses Panel zeigt alle Details der gerade ausgewählten Datei sowie deren Referenz Details. Das ist sehr praktisch um schnell zu bestimmen, ob ein Duplikat wirklich ein Duplikat ist. Sie können außerdem auf die Datei doppelt klicken, um sie mit der verknüpften Anwendung zu öffnen. Wenn Sie mehr Falschpositive als echte Duplikate haben (die Filterempfindlichkeit sehr niedrig ist), ist es der beste Weg die echten Duplikate zu markieren und mit **Aktionen-->Verschiebe Markierte in den Mülleimer** zu entfernen. Haben Sie mehr echte Duplikate als Falschpositive, können Sie stattdessen alle unechten Duplikate markieren und **Entferne Markierte aus den Ergebnissen** nutzen. Markierung und Auswahl ---------------------- Ein **markiertes** Duplikat ist ein Duplikat, dessen kleine Box ein Häkchen hat. Ein **ausgewähltes** Duplikat ist hervorgehoben. Mehrfachauswahl wird in dupeGuru über den normalen Weg erreicht (Shift/Command/Steuerung Klick). Sie können die Markierung aller Duplikate umschalten, indem sie **Leertaste** drücken. .. todo:: Add "Non-numerical delta" information. Nur Duplikate anzeigen ---------------------- Wird dieser Modus aktiviert, so werden ausschließlich Duplikate ohne ihre respektive Referenzdatei gezeigt. Sie können diese Liste auswählen, markieren und sortieren, ganz wie im normalen Modus. Die dupeGuru Ergebnisse werden, im normalen Modus, nach der **Referenzdatei** der Duplikatgruppen sortiert. Das bedeutet zum Beispiel, um alle Duplikate mit der "exe" Erweiterung zu markieren, können Sie nicht einfach die Ergebnisse nach "Typ" ordnen um alle exe Duplikate zu erhalten, denn eine Gruppe kann aus mehreren Typen (Dateiarten) bestehen. Hier kommt der Nur-Duplikate Modus ins Spiel. Um alle "exe" Duplikate zu markieren, müssen Sie nur: * Nur Duplikate anzeigen aktivieren * Die "Typ" Spalte über das "Spalten" Menü hinzufügen * Auf "Typ" klicken, um die Liste zu sortieren * Das erste Duplikat mit dem "exe" Typ lokalisieren. * Es auswählen. * Die Liste herunterscrollen und das letzte Duplikat mit dem "exe" Typ finden. * Die Shift Taste halten und es auswählen. * Leertaste drücken, um alle ausgewählten Duplikate zu markieren. Deltawerte ---------- Wenn Sie diesen Schalter aktivieren, zeigen einige Spalten den Wert relativ zur Duplikate-Referenz anstelle des absoluten Wertes an. Diese Deltawerte werden zusätzlich in einer anderen Farbe dargestellt, um sie leichter zu entdecken. Zum Beispiel, ein Duplikat ist 1,2 MB groß und die Referenz 1,4 MB, dann zeigt die Größe-Spalte -0,2 MB. Nur Duplikate anzeigen und Deltawerte ------------------------------------- Der Nur-Duplikate Modus enthüllt seine wahre Macht nur, wenn der Deltawerte Schalter aktiviert wurde. Wenn Sie ihn anschalten, werden relative Werte anstelle Absoluter gezeigt. Wenn Sie also, zum Beispiel, alle Duplikate die mehr als 300 KB von der Referenz verschieden sind aus der Ergebnisliste entfernen möchten, so sortieren Sie die Duplikate nach der Größe, wählen alle Duplikate mit weniger als -300 in der Größe-Spalte, löschen sie und tun das selbe für Duplikate mit mehr als +300 auf der Unterseite der Liste. Sie können dies außerdem nutzen, um die Referenzpriorität der Duplikateliste zu ändern. Wenn sie einen neuen Scan durchführen ist die größte Datei jeder Gruppe die Referenzdatei, solange keine Referenzordner existieren. Wollen Sie beispielsweise die Referenz nach der letztes Änderungszeit bestimmen, können Sie das Nur-Duplikate Ergebnis nach Änderungszeit in **absteigender** Reihenfolge sortieren, alle Duplikate mit einem Änderungszeit-Deltawert größergleich 0 auswählen und auf **Mache Ausgewählte zur Referenz** klicken. Der Grund warum die Sortierung absteigend erfolgen muss ist, wenn 2 Dateien der selben Duplikatgruppe ausgewählt werden und Sie **Mache Ausgewählte zur Referenz** klicken, dann wird nur der Erste der Liste wirklich als Referenz gesetzt. Da Sie nur die zuletzt geänderte Datei als Referenz haben möchten, stellt die vorangegangene Sortierung sicher, das der erste Eintrag der Liste auch der zuletzt Geänderte ist. Filtern ------- DupeGuru unterstützt das Filtern nach dem Scandurchlauf. Damit können Sie ihre Ergebnisse einschränken und diverse Aktionen auf einer Teilmenge ausführen. Beispielsweise ist es möglich alle Duplikate, deren Dateiname "copy" enthält mithilfe dieser Filterfunktion zu markieren. .. todo:: Qt has a toolbar search field now, not a menu item. **Windows/Linux:** Um diese Filterfunktion zu nutzen, klicken Sie Aktionen --> Filter anwenden, geben den Filter ein und drücken OK. Um zurück zu den ungefilterten Ergebnissen zu gelangen, gehen Sie auf Aktionen --> Filter entfernen. **Mac OS X:** Um diese Filterfunktion zu nutzen, geben Sie ihren Filter im "Filter" Suchfeld in der Symbolleiste ein. Um zurück zu den ungefilterten Ergebnissen zu gelangen, leeren Sie das Feld oder drücken auf "X". Im Einfach-Modus (Voreinstellung) wird jede Zeichenkette die Sie eingeben auch zum Filtern genutzt, mit Ausnahme einer Wildcard: **\***. Wenn Sie "[*]" als Filter nutzen, wird alles gefunden was die eckigen Klammern [] enthält, was auch immer zwischen diesen Klammern stehen mag. Für fortgeschrittenes Filtern, können Sie "Nutze reguläre Ausdrücke beim Filtern" aktivieren. Diese Funktion erlaubt es Ihnen **reguläre Ausdrücke** zu verwenden. Ein regulärer Ausdruck ist ein Filterkriterium für Text. Das zu erklären sprengt den Rahmen dieses Dokuments. Ein guter Platz für eine Einführung ist `regular-expressions.info `_. Filter ignorieren, im Einfach- und RegExp-Modus, die Groß- und Kleinschreibung. Damit der Filter etwas findet, muss Ihr regulärer Ausdruck nicht auf den gesamten Dateinamen passen. Der Name muss nur eine Zeichenkette enthalten die auf den Ausdruck zutrifft. Sie bemerken vielleicht, das nicht alle Duplikate in Ihren gefilterten Ergebnissen auf den Filter passen. Das liegt daran, sobald ein Duplikat einer Gruppe vom Filter gefunden wird, bleiben die restlichen Duplikate der Gruppe mit in der Liste, damit Sie einen besseren Überblick über den Kontext der Duplikate erhalten. Nicht passende Duplikate bleiben allerdings im "Referenz-Modus". Dadurch können Sie sicher sein Aktionen wie "Alles Markieren" anzuwenden und nur gefilterte Duplikate zu markieren. Aktionen Menü ------------- * **Ignorier-Liste leeren:** Entfernt alle ignorierten Treffer die Sie hinzugefügt haben. Um wirksam zu sein, muss ein neuer Scan für die gerade gelöschte Ignorier-Liste gestartet werden. * **Exportiere als XHTML:** Nimmt die aktuellen Ergebnisse und erstellt aus ihnen eine XHTML Datei. Die Spalten die sichtbar werden, wenn sie auf diesen Knopf drücken, werden die Spalten in der XHTML Datei sein. Die Datei wird automatisch mit dem Standardbrowser geöffnet. * **Verschiebe Markierte in den Mülleimer:** Verschiebt alle markierten Duplikate in den Mülleimer. * **Lösche Markierte und ersetze mit Hardlinks:** Verschiebt alle Markierten in den Mülleimer. Danach werden die gelöschten Dateien jedoch mit Hardlinks zur Referenzdatei ersetzt `hard link `_ . (Nur OS X und Linux) * **Verschiebe Markierte nach...:** Fragt nach einem Ziel und verschiebt alle Markierten zum Ziel. Der Quelldateipfad wird vielleicht am Ziel neu erstellt, abhängig von der "Kopieren und Verschieben" Einstellung. * **Kopiere Markierte nach...:** Fragt nach einem Ziel und kopiert alle Markierten zum Ziel. Der Quelldateipfad wird vielleicht am Ziel neu erstellt, abhängig von der "Kopieren und Verschieben" Einstellung. * **Entferne Markierte aus den Ergebnissen:** Entfernt alle markierte Duplikate aus den Ergebnissen. Die wirklichen Dateien werden nicht angerührt und bleiben wo sie sind. * **Entferne Ausgewählte aus den Ergebnissen:** Entfernt alle ausgewählten Duplikate aus den Ergebnissen. Beachten Sie das ausgewählte Referenzen ignoriert werden, nur Duplikate können entfernt werden. * **Mache Ausgewählte zur Referenz:** Ernenne alle ausgewählten Duplikate zur Referenz. Ist ein Duplikat Teil einer Gruppe, die eine Referenzdatei aus einem Referenzordner hat (blaue Farbe), wird keine Aktion für dieses Duplikat durchgeführt. Ist mehr als ein Duplikat aus der selben Gruppe ausgewählt, wird nur das Erste jeder Gruppe befördert. * **Füge Ausgewählte der Ignorier-Liste hinzu:** Dies entfernt zuerst alle ausgewählten Duplikate aus den Ergebnissen und fügt danach das aktuelle Duplikat und die Referenz der Ignorier-Liste hinzu. Diese Treffer werden bei zukünftigen Scans nicht mehr angezeigt. Das Duplikat selbst kann wieder auftauchen, es wird dann jedoch zur einer anderen Referenz gehören. Die Ignorier-Liste kann mit dem Ignorier-Liste leeren Kommando gelöscht werden. * **Öffne Ausgewählte mit Standardanwendung:** Öffnet die Datei mit der Anwendung die mit dem Dateityp verknüpft ist. * **Zeige Ausgewählte:** Öffnet den Ordner der die ausgewählte Datei enthält. * **Eigenen Befehl ausführen:** Ruft die in den Einstellungen definierte externe Anwendung auf und nutzt die aktuelle Auswahl als Argumente für den Aufruf. * **Ausgewählte umbenennen:** Fragt nach einem neuen Namen und benennt die ausgewählte Datei um. .. todo:: Add Move and iPhoto/iTunes warning .. todo:: Add "Deletion Options" section.dupeguru-4.3.1/help/en/000077500000000000000000000000001426171743600147245ustar00rootroot00000000000000dupeguru-4.3.1/help/en/contribute.rst000066400000000000000000000120761426171743600176420ustar00rootroot00000000000000Contribute to dupeGuru ====================== dupeGuru was started as shareware (thus proprietary) so it doesn't have a legacy of community-building. It's `been open source`_ for a while now and, although I've ("I" being Virgil Dupras, author of the software) always wanted to have people other than me working on dupeGuru, I've failed at attracting them. Since the end of 2013, I've been putting a lot of efforts into dupeGuru's :doc:`developer documentation ` and I'm more serious about my commitment to create a community around this project. So, whatever your skills, if you're interested in contributing to dupeGuru, please do so. Normally, this documentation should be enough to get you started, but if it isn't, then **please**, open a discussion at https://github.com/arsenetar/dupeguru/discussions. If there's any situation where you'd wish to contribute but some doubt you're having prevent you from going forward, please contact me. I'd much prefer to spend the time figuring out with you whether (and how) you can contribute than taking the chance of missing that opportunity. Development process ------------------- * `Source code repository`_ * `Issue Tracker`_ * `Issue labels meaning`_ dupeGuru's source code is on Github and thus managed in a Git repository. At all times, you should be able to build from source a fresh checkout of the ``master`` branch using instructions from the ``README.md`` file at the root of this project. If you can't, it's a bug. Please report it. ``master`` is the main development branch, and thus represents what going to be included in the next feature release. When needed, we create maintenance branches for bugfixes of the current feature release. When implementing a big feature, it's possible that it gets its own branch until it's stable enough to merge into ``master``. Every release is tagged, the tag name containing the edition (for old versions) and its version. For example, release 6.6.0 of dupeGuru ME is tagged ``me6.6.0``. Newer releases are tagged only with the version number (because editions don't exist anymore), for example ``4.0.0``. Once you're past building the software, the :doc:`developer documentation ` should be enough to get you started with actual development. Then again, proper documentation is a very difficult task and, in the case of dupeGuru, this documentation was practically nonexistent until late in the project, so it's still lacking. However, I'm committed to fix this situation, so if you're in a situation where you lack proper documentation to figure something out about this code, please contact me. Tasks for non-developers ------------------------ **Create and comment issues**. The single most useful way for a user who is not a developer to contribute to a software project is by thoroughly documenting a bug or a feature request. Most of the time, what we get as developers are emails like "the app crashes" and we spend a lot of time trying to figure out the cause of that bug. By properly describing the nature and context of a crash (we learn to do that with experience as a user who reports bugs), you help developers so immensely, you have no idea. It's the same thing with feature requests. Description of a feature request, when thoughts have already been given to how such a feature would fit in the current design, are precious to developers and help them figure out a clear roadmap for the project. So, even if you're not a developer, you can always open a Github account and create/comment issues. Your contribution will be much appreciated. **Documentation**. This is a bit trickier because dupeGuru's documentation is written with a rather complex markup language, `Sphinx`_ (based on `reST`_). To properly work within the documentation, you have to know that language. I don't think that learning this language is outside the realm of possibility for a non-developer, but it might be a daunting task. That being said, if it's a minor modification to the documentation, nothing stops you from opening an issue (there's a label for documentation issues, so this kind of issue is relevant to the tracker) describing the change you propose to make and I'll be happy to make the change myself (if relevant, of course). Even if it's a bigger contribution to the documentation you want to make, I probably wouldn't mind doing the formatting myself. But in that case, it's better to contact me first to make sure that we agree on what should be added to the documentation. **Translation**. Creating or improving an existing translation is a very good way to contribute to dupeGuru. For more information about how to do that, you can refer to the `translator guide`_. .. _been open source: https://www.hardcoded.net/articles/free-as-in-speech-fair-as-in-trade .. _Source code repository: https://github.com/arsenetar/dupeguru .. _Issue Tracker: https://github.com/arsenetar/issues .. _Issue labels meaning: https://github.com/arsenetar/wiki/issue-labels .. _Sphinx: http://sphinx-doc.org/ .. _reST: http://en.wikipedia.org/wiki/ReStructuredText .. _translator guide: https://github.com/arsenetar/wiki/Translator-Guide dupeguru-4.3.1/help/en/developer/000077500000000000000000000000001426171743600167115ustar00rootroot00000000000000dupeguru-4.3.1/help/en/developer/core/000077500000000000000000000000001426171743600176415ustar00rootroot00000000000000dupeguru-4.3.1/help/en/developer/core/app.rst000066400000000000000000000000721426171743600211520ustar00rootroot00000000000000core.app ======== .. automodule:: core.app :members: dupeguru-4.3.1/help/en/developer/core/directories.rst000066400000000000000000000001221426171743600227020ustar00rootroot00000000000000core.directories ================ .. automodule:: core.directories :members: dupeguru-4.3.1/help/en/developer/core/engine.rst000066400000000000000000000024351426171743600216440ustar00rootroot00000000000000core.engine =========== .. automodule:: core.engine .. autoclass:: Match .. autoclass:: Group :members: .. autofunction:: build_word_dict .. autofunction:: compare .. autofunction:: compare_fields .. autofunction:: getmatches .. autofunction:: getmatches_by_contents .. autofunction:: get_groups .. autofunction:: merge_similar_words .. autofunction:: reduce_common_words .. _fields: Fields ------ Fields are groups of words which each represent a significant part of the whole name. This concept is sifnificant in music file names, where we often have names like "My Artist - a very long title with many many words". This title has 10 words. If you run as scan with a bit of tolerance, let's say 90%, you'll be able to find a dupe that has only one "many" in the song title. However, you would also get false duplicates from a title like "My Giraffe - a very long title with many many words", which is of course a very different song and it doesn't make sense to match them. When matching by fields, each field (separated by "-") is considered as a separate string to match independently. After all fields are matched, the lowest result is kept. In the "Giraffe" example we gave, the result would be 50% instead of 90% in normal mode. dupeguru-4.3.1/help/en/developer/core/fs.rst000066400000000000000000000000671426171743600210060ustar00rootroot00000000000000core.fs ======= .. automodule:: core.fs :members: dupeguru-4.3.1/help/en/developer/core/gui/000077500000000000000000000000001426171743600204255ustar00rootroot00000000000000dupeguru-4.3.1/help/en/developer/core/gui/deletion_options.rst000066400000000000000000000001551426171743600245360ustar00rootroot00000000000000core.gui.deletion_options ========================= .. automodule:: core.gui.deletion_options :members: dupeguru-4.3.1/help/en/developer/core/gui/index.rst000066400000000000000000000001631426171743600222660ustar00rootroot00000000000000core.gui ======== .. automodule:: core.gui :members: .. toctree:: :maxdepth: 2 deletion_options dupeguru-4.3.1/help/en/developer/core/index.rst000066400000000000000000000001621426171743600215010ustar00rootroot00000000000000core ==== .. toctree:: :maxdepth: 2 app fs engine directories results gui/index dupeguru-4.3.1/help/en/developer/core/results.rst000066400000000000000000000001061426171743600220710ustar00rootroot00000000000000core.results ============ .. automodule:: core.results :members: dupeguru-4.3.1/help/en/developer/hscommon/000077500000000000000000000000001426171743600205345ustar00rootroot00000000000000dupeguru-4.3.1/help/en/developer/hscommon/build.rst000066400000000000000000000001141426171743600223610ustar00rootroot00000000000000hscommon.build ============== .. automodule:: hscommon.build :members: dupeguru-4.3.1/help/en/developer/hscommon/conflict.rst000066400000000000000000000001251426171743600230650ustar00rootroot00000000000000hscommon.conflict ================= .. automodule:: hscommon.conflict :members: dupeguru-4.3.1/help/en/developer/hscommon/desktop.rst000066400000000000000000000001221426171743600227320ustar00rootroot00000000000000hscommon.desktop ================ .. automodule:: hscommon.desktop :members: dupeguru-4.3.1/help/en/developer/hscommon/gui/000077500000000000000000000000001426171743600213205ustar00rootroot00000000000000dupeguru-4.3.1/help/en/developer/hscommon/gui/base.rst000066400000000000000000000003061426171743600227630ustar00rootroot00000000000000hscommon.gui.base ================= .. automodule:: hscommon.gui.base .. autosummary:: GUIObject .. autoclass:: GUIObject :members: :private-members: dupeguru-4.3.1/help/en/developer/hscommon/gui/column.rst000066400000000000000000000007171426171743600233540ustar00rootroot00000000000000hscommon.gui.column ============================ .. automodule:: hscommon.gui.column .. autosummary:: Columns Column ColumnsView PrefAccessInterface .. autoclass:: Columns :members: :private-members: .. autoclass:: Column :members: :private-members: .. autoclass:: ColumnsView :members: .. autoclass:: PrefAccessInterface :members: dupeguru-4.3.1/help/en/developer/hscommon/gui/progress_window.rst000066400000000000000000000005501426171743600253050ustar00rootroot00000000000000hscommon.gui.progress_window ============================ .. automodule:: hscommon.gui.progress_window .. autosummary:: ProgressWindow ProgressWindowView .. autoclass:: ProgressWindow :members: :private-members: .. autoclass:: ProgressWindowView :members: :private-members: dupeguru-4.3.1/help/en/developer/hscommon/gui/selectable_list.rst000066400000000000000000000010411426171743600252040ustar00rootroot00000000000000hscommon.gui.selectable_list ============================ .. automodule:: hscommon.gui.selectable_list .. autosummary:: Selectable SelectableList GUISelectableList GUISelectableListView .. autoclass:: Selectable :members: :private-members: .. autoclass:: SelectableList :members: :private-members: .. autoclass:: GUISelectableList :members: :private-members: .. autoclass:: GUISelectableListView :members: dupeguru-4.3.1/help/en/developer/hscommon/gui/table.rst000066400000000000000000000006771426171743600231530ustar00rootroot00000000000000hscommon.gui.table ================== .. automodule:: hscommon.gui.table .. autosummary:: Table Row GUITable GUITableView .. autoclass:: Table :members: :private-members: .. autoclass:: Row :members: :private-members: .. autoclass:: GUITable :members: :private-members: .. autoclass:: GUITableView :members: dupeguru-4.3.1/help/en/developer/hscommon/gui/text_field.rst000066400000000000000000000004421426171743600242010ustar00rootroot00000000000000hscommon.gui.text_field ======================= .. automodule:: hscommon.gui.text_field .. autosummary:: TextField TextFieldView .. autoclass:: TextField :members: :private-members: .. autoclass:: TextFieldView :members: dupeguru-4.3.1/help/en/developer/hscommon/gui/tree.rst000066400000000000000000000004271426171743600230140ustar00rootroot00000000000000hscommon.gui.tree ================= .. automodule:: hscommon.gui.tree .. autosummary:: Tree Node .. autoclass:: Tree :members: :private-members: .. autoclass:: Node :members: :private-members: dupeguru-4.3.1/help/en/developer/hscommon/index.rst000066400000000000000000000002361426171743600223760ustar00rootroot00000000000000hscommon ======== .. toctree:: :maxdepth: 2 :glob: build conflict desktop notify path util jobprogress/* gui/* dupeguru-4.3.1/help/en/developer/hscommon/jobprogress/000077500000000000000000000000001426171743600230735ustar00rootroot00000000000000dupeguru-4.3.1/help/en/developer/hscommon/jobprogress/job.rst000066400000000000000000000004161426171743600244000ustar00rootroot00000000000000hscommon.jobprogress.job ======================== .. automodule:: hscommon.jobprogress.job .. autosummary:: Job NullJob .. autoclass:: Job :members: :private-members: .. autoclass:: NullJob :members: dupeguru-4.3.1/help/en/developer/hscommon/jobprogress/performer.rst000066400000000000000000000003521426171743600256260ustar00rootroot00000000000000hscommon.jobprogress.performer ============================== .. automodule:: hscommon.jobprogress.performer .. autosummary:: ThreadedJobPerformer .. autoclass:: ThreadedJobPerformer :members: dupeguru-4.3.1/help/en/developer/hscommon/notify.rst000066400000000000000000000001171426171743600225750ustar00rootroot00000000000000hscommon.notify =============== .. automodule:: hscommon.notify :members: dupeguru-4.3.1/help/en/developer/hscommon/path.rst000066400000000000000000000001111426171743600222130ustar00rootroot00000000000000hscommon.path ============= .. automodule:: hscommon.path :members: dupeguru-4.3.1/help/en/developer/hscommon/util.rst000066400000000000000000000001111426171743600222340ustar00rootroot00000000000000hscommon.util ============= .. automodule:: hscommon.util :members: dupeguru-4.3.1/help/en/developer/index.rst000066400000000000000000000063271426171743600205620ustar00rootroot00000000000000Developer Guide =============== When looking at a non-trivial codebase for the first time, it's very difficult to understand anything of it until you get the "Big Picture". This page is meant to, hopefully, make you get dupeGuru's big picture. Branches and tags ----------------- The git repo has one main branch, ``master``. It represents the latest "stable development commit", that is, the latest commit that doesn't include in-progress features. This branch should always be buildable, ``tox`` should always run without errors on it. When a feature/bugfix has an atomicity of a single commit, it's alright to commit right into ``master``. However, if a feature/bugfix needs more than a commit, it should live in a separate topic branch until it's ready. Every release is tagged with the version number. For example, there's a ``2.8.2`` tag for the v2.8.2 release. Model/View/Controller... nope! ------------------------------ dupeGuru's codebase has quite a few design flaws. The Model, View and Controller roles are filled by different classes, scattered around. If you're aware of that, it might help you to understand what the heck is going on. The central piece of dupeGuru is :class:`core.app.DupeGuru`. It's the only interface to the python's code for the GUI code. A duplicate scan is started with :meth:`core.app.DupeGuru.start_scanning()`, directories are added through :meth:`core.app.DupeGuru.add_directory()`, etc.. A lot of functionalities of the App are implemented in the platform-specific subclasses of :class:`core.app.DupeGuru`, like ``DupeGuru`` in ``cocoa/inter/app.py``, or the ``DupeGuru`` class in ``qt/base/app.py``. For example, when performing "Remove Selected From Results", ``RemoveSelected()`` on the cocoa side, and ``remove_duplicates()`` on the PyQt side, are respectively called to perform the thing. .. _jobs: Jobs ---- A lot of operations in dupeGuru take a significant amount of time. This is why there's a generalized threaded job mechanism built-in :class:`~core.app.DupeGuru`. First, :class:`~core.app.DupeGuru` has a ``progress`` member which is an instance of :class:`~hscommon.jobprogress.performer.ThreadedJobPerformer`. It lets the GUI code know of the progress of the current threaded job. When :class:`~core.app.DupeGuru` needs to start a job, it calls ``_start_job()`` and the platform specific subclass deals with the details of starting the job. Core principles --------------- The core of the duplicate matching takes place (for SE and ME, not PE) in :mod:`core.engine`. There's :func:`core.engine.getmatches` which take a list of :class:`core.fs.File` instances and return a list of ``(firstfile, secondfile, match_percentage)`` matches. Then, there's :func:`core.engine.get_groups` which takes a list of matches and returns a list of :class:`.Group` instances (a :class:`.Group` is basically a list of :class:`.File` matching together). When a scan is over, the final result (the list of groups from :func:`.get_groups`) is placed into :attr:`core.app.DupeGuru.results`, which is a :class:`core.results.Results` instance. The :class:`~.Results` instance is where all the dupe marking, sorting, removing, power marking, etc. takes place. API --- .. toctree:: :maxdepth: 2 core/index hscommon/index dupeguru-4.3.1/help/en/faq.rst000066400000000000000000000216121426171743600162270ustar00rootroot00000000000000Frequently Asked Questions ========================== .. contents:: What is dupeGuru? ----------------- dupeGuru is a tool to find duplicate files on your computer. It has three operational modes: Standard, Music and Picture. Each mode has its own specialized preferences. Each mode has multiple scan types, such as filename, contents, tags. Some scan types feature advanced fuzzy matching algorithm, allowing you to find duplicates that other more rigid duplicate scanners can't. What makes it special? ---------------------- It's mostly about customizability. There's a lot of scanning options that allow you to get the type of results you're really looking for. How safe is it to use dupeGuru? ------------------------------- Very safe. dupeGuru has been designed to make sure you don't delete files you didn't mean to delete. First, there is the reference folder system that lets you define folders where you absolutely **don't** want dupeGuru to let you delete files there, and then there is the group reference system that makes sure that you will **always** keep at least one member of the duplicate group. How can I report a bug a suggest a feature? ------------------------------------------- dupeGuru is hosted on `Github`_ and it's also where issues are tracked. The best way to report a bug or suggest a feature is to sign up on Github and `open an issue`_. The mark box of a file I want to delete is disabled. What must I do? -------------------------------------------------------------------- You cannot mark the reference (The first file) of a duplicate group. However, what you can do is to promote a duplicate file to reference. Thus, if a file you want to mark is reference, select a duplicate file in the group that you want to promote to reference, and click on **Actions-->Make Selected into Reference**. If the reference file is from a reference folder (filename written in blue letters), you cannot remove it from the reference position. I have a folder from which I really don't want to delete files. --------------------------------------------------------------- If you want to be sure that dupeGuru will never delete file from a particular folder, make sure to set its state to **Reference** at :doc:`folders`. What is this '(X discarded)' notice in the status bar? ------------------------------------------------------ In some cases, some matches are not included in the final results for security reasons. Let me use an example. We have 3 file: A, B and C. We scan them using a low filter hardness. The scanner determines that A matches with B, A matches with C, but B does **not** match with C. Here, dupeGuru has kind of a problem. It cannot create a duplicate group with A, B and C in it because not all files in the group would match together. It could create 2 groups: one A-B group and then one A-C group, but it will not, for security reasons. Lets think about it: If B doesn't match with C, it probably means that either B, C or both are not actually duplicates. If there would be 2 groups (A-B and A-C), you would end up delete both B and C. And if one of them is not a duplicate, that is really not what you want to do, right? So what dupeGuru does in a case like this is to discard the A-C match (and adds a notice in the status bar). Thus, if you delete B and re-run a scan, you will have a A-C match in your next results. I want to mark all files from a specific folder. What can I do? --------------------------------------------------------------- Enable the :doc:`Dupes Only ` mode and click on the Folder column to sort your duplicates by folder. It will then be easy for you to select all duplicates from the same folder, and then press Space to mark all selected duplicates. I want to remove all files that are more than 300 KB away from their reference file. What can I do? --------------------------------------------------------------------------------------------------- * Enable the :doc:`Dupes Only ` mode. * Enable the **Delta Values** mode. * Click on the "Size" column to sort the results by size. * Select all duplicates below -300. * Click on **Remove Selected from Results**. * Select all duplicates over 300. * Click on **Remove Selected from Results**. I want to make my latest modified files reference files. What can I do? ----------------------------------------------------------------------- * Enable the :doc:`Dupes Only ` mode. * Enable the **Delta Values** mode. * Click on the "Modification" column to sort the results by modification date. * Click on the "Modification" column again to reverse the sort order. * Select all duplicates over 0. * Click on **Make Selected into Reference**. I want to mark all duplicates containing the word "copy". How do I do that? --------------------------------------------------------------------------- * Type "copy" in the "Filter" field in the top-right corner of the result window. * Click on **Mark --> Mark All**. I want to remove all songs that are more than 3 seconds away from their reference file. What can I do? ------------------------------------------------------------------------------------------------------ * Enable the :doc:`Dupes Only ` mode. * Enable the **Delta Values** mode. * Click on the "Time" column to sort the results by time. * Select all duplicates below -00:03. * Click on **Remove Selected from Results**. * Select all duplicates over 00:03. * Click on **Remove Selected from Results**. I want to make my highest bitrate songs reference files. What can I do? ----------------------------------------------------------------------- * Enable the :doc:`Dupes Only ` mode. * Enable the **Delta Values** mode. * Click on the "Bitrate" column to sort the results by bitrate. * Click on the "Bitrate" column again to reverse the sort order. * Select all duplicates over 0. * Click on **Make Selected into Reference**. I don't want [live] and [remix] versions of my songs counted as duplicates. How do I do that? --------------------------------------------------------------------------------------------- If your comparison threshold is low enough, you will probably end up with live and remix versions of your songs in your results. There's nothing you can do to prevent that, but there's something you can do to easily remove them from your results after the scan: post-scan filtering. If, for example, you want to remove every song with anything inside square brackets []: * Type "[*]" in the "Filter" field in the top-right corner of the result window. * Click on **Mark --> Mark All**. * Click on **Actions --> Remove Selected from Results**. The "Filter Hardness" slider in the preferences won't move! ----------------------------------------------------------- This slider is only relevant for scan types that support "fuzziness". Many scan types, such as the "Contents" type, only support exact matches. When these types are selected, the slider is disabled. On some OS, the fact that it's disabled is harder to see than on others, but if you can't move the slider, it means that this preference is irrelevant in your current scan type. I've tried to send my duplicates to Trash, but dupeGuru is telling me it can't do it. Why? What can I do? --------------------------------------------------------------------------------------------------------- Most of the time, the reason why dupeGuru can't send files to Trash is because of file permissions. You need *write* permissions on files you want to send to Trash. If dupeGuru still gives you troubles after fixing your permissions, try enabling the "Directly delete files" option that is offered to you when you activate Send to Trash. This will not send files to the Trash, but delete them immediately. In some cases, for example on network storage (NAS), this has been known to work when normal deletion didn't. Why is Picture mode's contents scan so slow? -------------------------------------------- This scanning method is very different from methods. It can detect duplicate photos even if they are not exactly the same. This very cool capability has a cost: time. Every picture has to be individually and fuzzily matched to all others, and this takes a lot of CPU power. If all you need to find is exact duplicates, just use the standard mode of dupeGuru with the Contents scan method. If your photos have EXIF tags, you can also try the "EXIF" scan method which is much faster. Where are user files located? ----------------------------- For some reason, you'd like to remove or edit dupeGuru's user files (debug logs, caches, etc.). Where they're located depends on your platform: * Linux: ``~/.local/share/data/Hardcoded Software/dupeGuru`` * Mac OS X: ``~/Library/Application Support/dupeGuru`` Preferences are stored elsewhere: * Linux: ``~/.config/Hardcoded Software/dupeGuru.conf`` * Mac OS X: In the built-in ``defaults`` system, as ``com.hardcoded-software.dupeguru`` .. _Github: https://github.com/arsenetar/dupeguru .. _open an issue: https://github.com/arsenetar/dupeguru/wiki/issue-labels dupeguru-4.3.1/help/en/folders.rst000066400000000000000000000055421426171743600171220ustar00rootroot00000000000000Folder Selection ================ The first window you see when you launch dupeGuru is the folder selection window. This windows contains the basic input dupeGuru needs to start a scan: * An Application Mode selection * A Scan Type selection * Folders to scan Application Mode ---------------- dupeGuru had three main modes: Standard, Music and Picture. Standard is for any type of files. This makes this mode the most polyvalent, but it lacks specialized features other modes have. Music mode scans only music files, but it supports tags comparison and its results window has many audio-related informational columns. Picture mode scans only pictures, but its contents scan type is a powerful fuzzy matcher that can find pictures that are similar without being exactly the same. Choosing an application mode not only changes available scan types in the selector below, but also changes available options in the preferences panel. Thus, if you want to fine tune your scan, be sure to open the preferences panel **after** you've selected the application mode. Scan Type --------- This selector determines the type of the scan we'll do. See :doc:`scan` for details about scan types. Folder List ----------- To add a folder, click on the **+** button. If you added folder before, a popup menu with a list of recent folders you added will pop. You can click on one of them to add it directly to your list. If you click on the first item of the popup menu, **Add New Folder...**, you will be prompted for a folder to add. If you never added a folder, no menu will pop and you will directly be prompted for a new folder to add. An alternate way to add folders to the list is to drag them in the list. To remove a folder, select the folder to remove and click on **-**. If a subfolder is selected when you click the button, the selected folder will be set to **excluded** state (see below) instead of being removed. Folder states ------------- Every folder can be in one of these 3 states: **Normal:** Duplicates found in this folder can be deleted. **Reference:** Duplicates found in this folder **cannot** be deleted. Files from this folder can only end up in **reference** position in the dupe group. If more than one file from reference folders end up in the same dupe group, only one will be kept. The others will be removed from the group. **Excluded:** Files in this directory will not be included in the scan. The default state of a folder is, of course, **Normal**. You can use **Reference** state for a folder if you want to be sure that you won't delete any file from it. When you set the state of a directory, all subfolders of this folder automatically inherit this state unless you explicitly set a subfolder's state. Scan ---- When you're ready, click on the **Scan** button to initiate the scanning process. When it's done, you'll be shown the :doc:`results`. dupeguru-4.3.1/help/en/index.rst000066400000000000000000000022541426171743600165700ustar00rootroot00000000000000dupeGuru help ============= This help document is also available in these languages: * `French `__ * `German `__ * `Armenian `__ * `Russian `__ * `Ukrainian `__ dupeGuru is a tool to find duplicate files on your computer. It has three modes, Standard, Music and Picture, with each mode having its own scan types and little features. Although dupeGuru can easily be used without documentation, reading this file will help you to master it. If you are looking for guidance for your first duplicate scan, you can take a look at the :doc:`Quick Start ` section. It is a good idea to keep dupeGuru updated. You can download the latest version on its `homepage`_. Contents: .. toctree:: :maxdepth: 2 contribute quick_start folders preferences scan results reprioritize faq developer/index changelog Indices and tables ================== * :ref:`genindex` * :ref:`search` .. _homepage: https://dupeguru.voltaicideas.net/ dupeguru-4.3.1/help/en/preferences.rst000066400000000000000000000074331426171743600177660ustar00rootroot00000000000000Preferences =========== **Tags to scan:** When using the **Tags** scan type, you can select the tags that will be used for comparison. **Word weighting:** See :ref:`word-weighting`. **Match similar words:** See :ref:`similarity-matching`. **Match pictures of different dimensions:** If you check this box, pictures of different dimensions will be allowed in the same duplicate group. .. _filter-hardness: **Filter Hardness:** The threshold needed for two files to be considered duplicates. A lower value means more duplicates. The meaning of the threshold depends on the scanning type (see :doc:`scan`). Only works for :ref:`worded ` and :ref:`picture blocks ` scans. **Can mix file kind:** If you check this box, duplicate groups are allowed to have files with different extensions. If you don't check it, well, they aren't! **Ignore duplicates hardlinking to the same file:** If this option is enabled, dupeGuru will verify duplicates to see if they refer to the same `inode`_. If they do, they will not be considered duplicates. (Only for OS X and Linux) **Use regular expressions when filtering:** If you check this box, the filtering feature will treat your filter query as a **regular expression**. Explaining them is beyond the scope of this document. A good place to start learning it is `regular-expressions.info`_. **Remove empty folders after delete or move:** When this option is enabled, folders are deleted after a file is deleted or moved and the folder is empty. **Copy and Move:** Determines how the Copy and Move operations (in the Action menu) will behave. * **Right in destination:** All files will be sent directly in the selected destination, without trying to recreate the source path at all. * **Recreate relative path:** The source file's path will be re-created in the destination folder up to the root selection in the Directories panel. For example, if you added ``/Users/foobar/SomeFolder`` to your Directories panel and you move ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` to the destination ``/Users/foobar/MyDestination``, the final destination for the file will be ``/Users/foobar/MyDestination/SubFolder`` (``SomeFolder`` has been trimmed from source's path in the final destination.). * **Recreate absolute path:** The source file's path will be re-created in the destination folder in its entirety. For example, if you move ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` to the destination ``/Users/foobar/MyDestination``, the final destination for the file will be ``/Users/foobar/MyDestination/Users/foobar/SomeFolder/SubFolder``. In all cases, dupeGuru nicely handles naming conflicts by prepending a number to the destination filename if the filename already exists in the destination. **Custom Command:** This preference determines the command that will be invoked by the "Invoke Custom Command" action. You can invoke any external application through this action. This can be useful if, for example, you have a nice diffing application installed. The format of the command is the same as what you would write in the command line, except that there are 2 placeholders: **%d** and **%r**. These placeholders will be replaced by the path of the selected dupe (%d) and the path of the selected dupe's reference file (%r). If the path to your executable contains space characters, you should enclose it in "" quotes. You should also enclose placeholders in quotes because it's very possible that paths to dupes and refs will contain spaces. Here's an example custom command:: "C:\Program Files\SuperDiffProg\SuperDiffProg.exe" "%d" "%r" .. _inode: http://en.wikipedia.org/wiki/Inode .. _regular-expressions.info: http://www.regular-expressions.info dupeguru-4.3.1/help/en/quick_start.rst000066400000000000000000000016521426171743600200130ustar00rootroot00000000000000Quick Start =========== To get you quickly started with dupeGuru, let's just make a standard scan using default preferences. * Launch dupeGuru. * Add folders to scan with either drag & drop or the "+" button. * Click on **Scan**. * Wait until the scan process is over. * Look at every duplicate (The files that are indented) and verify that it is indeed a duplicate to the group's reference (The file above the duplicate that is not indented and have a disabled mark box). * If a file is a false duplicate, select it and click on **Actions-->Remove Selected from Results**. * Once you are sure that there is no false duplicate in your results, click on **Edit-->Mark All**, and then **Actions-->Send Marked to Recycle bin**. That is only a basic scan. There are a lot of tweaking you can do to get different results and several methods of examining and modifying your results. To know about them, just read the rest of this help file. dupeguru-4.3.1/help/en/reprioritize.rst000066400000000000000000000033411426171743600202060ustar00rootroot00000000000000Re-Prioritizing duplicates ========================== dupeGuru tries to automatically determine which duplicate should go in each group's reference position, but sometimes it gets it wrong. In many cases, clever dupe sorting with "Delta Values" and "Dupes Only" options in addition to the "Make Selected into Reference" action does the trick, but sometimes, a more powerful option is needed. This is where the Re-Prioritization dialog comes into play. You can summon it through the "Re-Prioritize Results" item in the "Actions" menu. This dialog allows you to select criteria according to which a reference dupe will be selected in each dupe group. The list of available criteria is on the left and the list of criteria you've selected is on the right. A criteria is a category followed by an argument. For example, "Size (Highest)" means that the dupe with the biggest size will win. "Folder (/foo/bar)" means that dupes in this folder will win. To add a criterion to the rightmost list, first select a category in the combobox, then select a subargument in the list below, and then click on the right pointing arrow button. The order of the list on the right is important (you can re-order items through drag & drop). When picking a dupe for reference position, the first criterion is used. If there's a tie, the second criterion is used and so on and so on. For example, if your arguments are "Size (Highest)" and then "Filename (Doesn't end with a number)", the reference file that will be picked in a group will be the biggest file, and if two or more files have the same size, the one that has a filename that doesn't end with a number will be used. When all criteria result in ties, the order in which dupes previously were in the group will be used.dupeguru-4.3.1/help/en/results.rst000066400000000000000000000272011426171743600171610ustar00rootroot00000000000000Results ======= .. contents:: When dupeGuru is finished scanning for duplicates, it will show its results in the form of duplicate group list. About duplicate groups ---------------------- A duplicate group is a group of files that all match together. Every group has a **reference file** and one or more **duplicate files**. The reference file is the first file of the group. Its mark box is disabled. Below it, and indented, are the duplicate files. You can mark duplicate files, but you can never mark the reference file of a group. This is a security measure to prevent dupeGuru from deleting not only duplicate files, but their reference. You sure don't want that, do you? What determines which files are reference and which files are duplicates is first their folder state. A file from a reference folder will always be reference in a duplicate group. If all files are from a normal folder, the size determine which file will be the reference of a duplicate group. dupeGuru assumes that you always want to keep the biggest file, so the biggest files will take the reference position. You can change the reference file of a group manually. To do so, select the duplicate file you want to promote to reference, and click on **Actions-->Make Selected into Reference**. Reviewing results ----------------- Although you can just click on **Edit-->Mark All** and then **Actions-->Send Marked to Recycle bin** to quickly delete all duplicate files in your results, it is always recommended to review all duplicates before deleting them. To help you reviewing the results, you can bring up the **Details panel**. This panel shows all the details of the currently selected file as well as its reference's details. This is very handy to quickly determine if a duplicate really is a duplicate. You can also double-click on a file to open it with its associated application. If you have more false duplicates than true duplicates (If your filter hardness is very low), the best way to proceed would be to review duplicates, mark true duplicates and then click on **Actions-->Send Marked to Recycle bin**. If you have more true duplicates than false duplicates, you can instead mark all files that are false duplicates, and use **Actions-->Remove Marked from Results**. Marking and Selecting --------------------- A **marked** duplicate is a duplicate with the little box next to it having a check-mark. A **selected** duplicate is a duplicate being highlighted. The multiple selection actions can be performed in dupeGuru in the standard way (Shift/Command/Control click). You can toggle all selected duplicates' mark state by pressing **space**. Show Dupes Only --------------- When this mode is enabled, the duplicates are shown without their respective reference file. You can select, mark and sort this list, just like in normal mode. The dupeGuru results, when in normal mode, are sorted according to duplicate groups' **reference file**. This means that if you want, for example, to mark all duplicates with the "exe" extension, you cannot just sort the results by "Kind" to have all exe duplicates together because a group can be composed of more than one kind of files. That is where Dupes Only mode comes into play. To mark all your "exe" duplicates, you just have to: * Enable the Dupes Only mode. * Add the "Kind" column with the "Columns" menu. * Click on that "Kind" column to sort the list by kind. * Locate the first duplicate with a "exe" kind. * Select it. * Scroll down the list to locate the last duplicate with a "exe" kind. * Hold Shift and click on it. * Press Space to mark all selected duplicates. .. _deltavalues: Delta Values ------------ If you turn this switch on, numerical columns will display the value relative to the duplicate's reference instead of the absolute values. These delta values will also be displayed in a different color, orange, so you can spot them easily. For example, if a duplicate is 1.2 MB and its reference is 1.4 MB, the Size column will display -0.2 MB. Moreover, non-numerical values will also be in orange if their value is different from their reference, and stay black if their value is the same. Combined with column sorting in Dupes Only mode, this allows for very powerful post-scan filtering. Dupes Only and Delta Values --------------------------- The Dupes Only mode unveil its true power when you use it with the Delta Values switch turned on. When you turn it on, relative values will be displayed instead of absolute ones. So if, for example, you want to remove from your results all duplicates that are more than 300 KB away from their reference, you could sort the dupes only results by Size, select all duplicates under -300 in the Size column, delete them, and then do the same for duplicates over 300 at the bottom of the list. Same thing for non-numerical values: When Dupes Only and Delta Values are enabled at the same time, column sorting groups rows depending on whether they're orange or not. Example: You ran a contents scan, but you would only like to delete duplicates that have the same filename? Sort by filename and all dupes with their filename attribute being the same as the reference will be grouped together, their value being in black. You could also use it to change the reference priority of your duplicate list. When you make a fresh scan, if there are no reference folders, the reference file of every group is the biggest file. If you want to change that, for example, to the latest modification time, you can sort the dupes only results by modification time in **descending** order, select all duplicates with a modification time delta value higher than 0 and click on **Make Selected into Reference**. The reason why you must make the sort order descending is because if 2 files among the same duplicate group are selected when you click on **Make Selected into Reference**, only the first of the list will be made reference, the other will be ignored. And since you want the last modified file to be reference, having the sort order descending assures you that the first item of the list will be the last modified. Filtering --------- dupeGuru supports post-scan filtering. With it, you can narrow down your results so you can perform actions on a subset of it. For example, you could easily mark all duplicates with their filename containing "copy" from your results using the filter. To use the filtering feature, type your filter in the "Filter" search field at the top-right corner of the results window. What you type in that box will be applied to the *whole path* of every duplicate in the results. Only duplicate *groups* having at least one duplicate matching the filter will be shown. When having groups where not all duplicates match the filter, we still show all duplicates of the group. However, non-matching duplicates are in "reference mode". Therefore, you can perform actions like "Mark All" and be sure to only mark filtered duplicates. To go back to unfiltered result, blank out the field or click on the "X". In simple mode (the default mode), whatever you type as the filter is the string used to perform the actual filtering, with the exception of one wildcard: **\***. Thus, if you type "[*]" as your filter, it will match anything with [] brackets in it, whatever is in between those brackets. For more advanced filtering, you can turn "Use regular expressions when filtering" on. The filtering feature will then use **regular expressions**. A regular expression is a language for matching text. Explaining them is beyond the scope of this document. A good place to start learning it is `regular-expressions.info`_. Matches are case insensitive in both simple and regexp mode. For the filter to match, your regular expression don't have to match the whole filename, it just have to contain a string matching the expression. Action Menu ----------- **Clear Ignore List:** Remove all ignored matches you added. You have to start a new scan for the newly cleared ignore list to be effective. **Export Results to XHTML:** Take the current results, and create an XHTML file out of it. The columns that are visible when you click on this button will be the columns present in the XHTML file. The file will automatically be opened in your default browser. **Send Marked to Trash:** Send all marked duplicates to trash, obviously. Before proceeding, you'll be presented deletion options (see below). **Move Marked to...:** Prompt you for a destination, and then move all marked files to that destination. Source file's path might be re-created in destination, depending on the "Copy and Move" preference. **Copy Marked to...:** Prompt you for a destination, and then copy all marked files to that destination. Source file's path might be re-created in destination, depending on the "Copy and Move" preference. **Remove Marked from Results:** Remove all marked duplicates from results. The actual files will not be touched and will stay where they are. **Remove Selected from Results:** Remove all selected duplicates from results. Note that all selected reference files will be ignored, only duplicates can be removed with this action. **Make Selected into Reference:** Promote all selected duplicates to reference. If a duplicate is a part of a group having a reference file coming from a reference folder (in blue color), no action will be taken for this duplicate. If more than one duplicate among the same group are selected, only the first of each group will be promoted. **Add Selected to Ignore List:** This first removes all selected duplicates from results, and then add the match of that duplicate and the current reference in the ignore list. This match will not come up again in further scan. The duplicate itself might come back, but it will be matched with another reference file. You can clear the ignore list with the Clear Ignore List command. **Open Selected with Default Application:** Open the file with the application associated with selected file's type. **Reveal Selected in Finder:** Open the folder containing selected file. **Invoke Custom Command:** Invokes the external application you've set up in your preferences using the current selection as arguments in the invocation. **Rename Selected:** Prompts you for a new name, and then rename the selected file. Deletion Options ---------------- These options affect how duplicate deletion takes place. Most of the time, you don't need to enable any of them. **Link deleted files:** The deleted files are replaced by a link to the reference file. You have a choice of replacing it either with a `symlink`_ or a `hardlink`_. It's better to read the whole wikipedia pages about them to make a informed choice, but in short, a symlink is a shortcut to the file's path. If the original file is deleted or moved, the link is broken. A hardlink is a link to the file *itself*. That link is as good as a "real" file. Only when *all* hardlinks to a file are deleted is the file itself deleted. On OSX and Linux, this feature is supported fully, but under Windows, it's a bit complicated. Windows XP doesn't support it, but Vista and up support it. However, for the feature to work, dupeGuru has to run with administrative privileges. **Directly delete files:** Instead of sending files to trash, directly delete them. This is used for troubleshooting and you normally don't need to enable this unless dupeGuru has problems deleting files normally, something that can happens when you try to delete files on network storage (NAS). .. _regular-expressions.info: http://www.regular-expressions.info .. _hardlink: http://en.wikipedia.org/wiki/Hard_link .. _symlink: http://en.wikipedia.org/wiki/Symbolic_link dupeguru-4.3.1/help/en/scan.rst000066400000000000000000000211331426171743600164020ustar00rootroot00000000000000The scanning process ==================== .. contents:: dupeGuru has 3 basic ways of scanning: :ref:`worded-scan` and :ref:`contents-scan` and :ref:`picture blocks `. The first two types are for the Standard and Music modes, the last is for the Picture mode. The scanning process is configured through the :doc:`Preference pane `. .. _worded-scan: Worded scans ------------ Worded scans extract a string from each file and split it into words. The string can come from two different sources: **Filename** or **Tags** (Music Edition only). When our source is music tags, we have to choose which tags to use. If, for example, we choose to analyse *artist* and *title* tags, we'd end up with strings like "The White Stripes - Seven Nation Army". Words are split by space characters, with all punctuation removed (some are replaced by spaces, some by nothing) and all words lowercased. For example, the string "This guy's song(remix)" yields *this*, *guys*, *song* and *remix*. Once this is done, the scanning dance begins. Finding duplicates is only a matter of finding how many words in common two given strings have. If the :ref:`filter hardness ` is, for example, ``80``, it means that 80% of the words of two strings must match. To determine the matching percentage, dupeGuru first counts the total number of words in **both** strings, then count the number of words matching (every word matching count as 2), and then divide the number of words matching by the total number of words. If the result is higher or equal than the filter hardness, we have a duplicate match. For example, "a b c d" and "c d e" have a matching percentage of 57 (4 words matching, 7 total words). Fields ^^^^^^ Song filenames often come with multiple and distinct parts and this can cause problems. For example, let's take these two songs: "Dolly Parton - I Will Always Love You" and "Whitney Houston - I Will Always Love You". They are clearly not the same song (they come from different artists), but they still still have a matching score of 71%! This means that, with a naive scanning method, we would get these songs as a false positive as soon as we try to dig a bit deeper in our dupe hunt by lowering the threshold a bit. This is why we have the "Fields" concept. Fields are separated by dashes (``-``). When the "Filename - Fields" scan type is chosen, each field is compared separately. Our final matching score will only be the lowest of all the fields. In our example, the title has a 100% match, but the artist has a 0% match, making our final match score 0. Sometimes, our song filename policy isn't completely homogenous, which means that we can end up with "The White Stripes - Seven Nation Army" and "Seven Nation Army - The White Stripes". This is why we have the "Filename - Fields (No Order)" scan type. With this scan type, all fields are compared with each other, and the highest score is kept. Then, the final matching score is the lowest of them all. In our case, the final matching score is 100. Note: Each field is used once. Thus, "The White Stripes - The White Stripes" and "The White Stripes - Seven Nation Army" have a match score of 0 because the second "The White Stripes" can't be compared with the first field of the other name because it has already been "used up" by the first field. Our final match score would be 0. *Tags* scanning method is always "fielded". When choosing this scan method, we also choose which tags are going to be compared, each being a field. .. _word-weighting: Word weighting ^^^^^^^^^^^^^^ When enabled, this option slightly changes how matching percentage is calculated by making bigger words worth more. With word weighting, instead of having a value of 1 in the duplicate count and total word count, every word have a value equal to the number of characters they have. With word weighting, "ab cde fghi" and "ab cde fghij" would have a matching percentage of 53% (19 total characters, 10 characters matching (4 for "ab" and 6 for "cde")). .. _similarity-matching: Similarity matching ^^^^^^^^^^^^^^^^^^^ When enabled, similar words will be counted as matches. For example "The White Stripes" and "The White Stripe" would have a match score of 100 instead of 66 with that option turned on. Two words are considered similar if they can be made equal with only a few edit operations (removing a letter, adding one etc.). The process used is not unlike the `Levenshtein distance`_. For the technically inclined, the actual function used is Python's `get_close_matches`_ with a ``0.8`` cutoff. **Warning:** Use this option with caution. It is likely that you will get a lot of false positives in your results when turning it on. However, it will help you to find duplicates that you wouldn't have found otherwise. The scan process also is significantly slower with this option turned on. .. _contents-scan: Contents scans -------------- Contents scans are much simpler than worded scans. We read files and if the contents is exactly the same, we consider the two files duplicates. This is, of course, quite longer than comparing filenames and, to avoid needlessly reading whole file contents, we start by looking at file sizes. After having grouped our files by size, we discard every file that is alone in its group. Then, we proceed to read the contents of our remaining files. MD5 hashes are used to compute compare contents. Yes, it is widely known that forging files having the same MD5 hash is easy, but this file has to be knowingly forged. The possibilities of two files having the same MD5 hash *and* the same size by accident is still very, very small. The :ref:`filter hardness ` preference is ignored in this scan. Folders ^^^^^^^ This is a special Contents scan type. It works like a normal contents scan, but instead of trying to find duplicate files, it tries to find duplicate folders. A folder is duplicate to another if all files it contains have the same contents as the other folder's file. This scan is, of course, recursive and subfolders are checked. dupeGuru keeps only the biggest fishes. Therefore, if two folders that are considered as matching contain subfolders, these subfolders will not be included in the final results. With this mode, we end up with folders as results instead of files. .. _picture-blocks-scan: Picture blocks -------------- dupeGuru Picture mode stands apart of its two friends. Its scan types are completely different. The first one is its "Contents" scan, which is a bit too generic, hence the name we use here, "Picture blocks". We start by opening every picture in RGB bitmap mode, then we "blockify" the picture. We create a 15x15 grid and compute the average color of each grid tile. This is the "picture analysis" phase. It's very time consuming and the result is cached in a database (the "picture cache"). Once we've done that, we can start comparing them. Each tile in the grid (an average color) is compared to its corresponding grid on the other picture and a color diff is computer (it's simply a sum of the difference of R, G and B on each side). All these sums are added up to a final "score". If that score is smaller or equal to ``100 - threshold``, we have a match. A threshold of 100 adds an additional constraint that pictures have to be exactly the same (it's possible, due to averaging, that the tile comparison yields ``0`` for pictures that aren't exactly the same, but since "100%" suggests "exactly the same", we discard those ocurrences). If you want to get pictures that are very, very similar but still allow a bit of fuzzy differences, go for 99%. This second part of the scan is CPU intensive and can take quite a bit of time. This task has been made to take advatange of multi-core CPUs and has been optimized to the best of my abilities, but the fact of the matter is that, due to the fuzziness of the task, we still have to compare every picture to every other, making the algorithm quadratic (if ``N`` is the number of pictures to compare, the number of comparisons to perform is ``N*N``). This algorithm is very naive, but in the field, it works rather well. If you master a better algorithm and want to improve dupeGuru, by all means, let me know! EXIF Timestamp -------------- This one is easy. We read the EXIF information of every picture and extract the ``DateTimeOriginal`` tag. If the tag is the same for two pictures, they're considered duplicates. **Warning:** Modified pictures often keep the same EXIF timestamp, so watch out for false positives when you use that scan type. .. _Levenshtein distance: http://en.wikipedia.org/wiki/Levenshtein_distance .. _get_close_matches: http://docs.python.org/3/library/difflib.html#difflib.get_close_matches dupeguru-4.3.1/help/fr/000077500000000000000000000000001426171743600147315ustar00rootroot00000000000000dupeguru-4.3.1/help/fr/faq.rst000066400000000000000000000240421426171743600162340ustar00rootroot00000000000000Foire aux questions =================== .. contents:: Qu'est-ce que dupeGuru? ------------------------ .. only:: edition_se dupeGuru est un outil pour trouver des doublons parmi vos fichiers. Il peut comparer soit les noms de fichiers, soit le contenu. Le comparateur de nom de fichier peut trouver des doublons même si les noms ne sont pas exactement pareils. .. only:: edition_me dupeGuru Music Editon est un outil pour trouver des doublons parmi vos chansons. Il peut comparer les noms de fichiers, les tags ou bien le contenu. Les comparaisons de nom de fichier ou de tags peuvent trouver des doublons même si les noms de sont pas exactement pareils. .. only:: edition_pe dupeGuru Picture Edition est un outil pour trouver des doublons parmi vos images. Non seulement il permet de trouver les doublons exactes, mais il est aussi capable de trouver les images ayant de légères différences, étant de format différent ou bien ayant une qualité différente. En quoi est-il mieux que les autres applications? ------------------------------------------------- dupeGuru est hautement configurable. Vous pouvez changer les options de comparaison afin de trouver exactement le type de doublons recherché. Plus de détails sur la :doc:`page de préférences `. dupeGuru est-il sécuritaire? ---------------------------- Oui. dupeGuru a été conçu afin d'être certain que vous conserviez toujours au moins une copie des doublons que vous trouvez. Il est aussi possible de configurer dupeGuru afin de déterminer certains dossier à partir desquels aucun fichier ne sera effacé. Quelles sont les limitation démo de dupeGuru? --------------------------------------------- En mode de démonstration, les actions sont limitées à 10 doublons par session. En mode `Fairware`_, il n'y a pas de limitation. Je ne peux pas marquer le doublons que je veux effacer, pourquoi? ----------------------------------------------------------------- Tour groupe de doublons contient au moins un fichier dit "référence" et ce fichier ne peut pas être effacé. Par contre, ce que vous pouvez faire c'est de le remplacer par un autre fichier du groupe. Pour ce faire, sélectionnez un fichier du groupe et cliquez sur l'action **Transformer sélectionnés en références**. Notez que si le fichier référence du groupe vient d'un dossier qui a été défini comme dossier référence, ce fichier ne peut pas être déplacé de sa position de référence du groupe. J'ai un dossier duquel je ne veut jamais effacer de fichier. ------------------------------------------------------------ Si vous faites un scan avec un dossier qui ne doit servir que de référence pour effacer des doublons dans un autre dossier, changez le type de dossier à "Référence" dans la fenêtre de :doc:`sélection de dossiers `. Que veut dire '(X hors-groupe)' dans la barre de statut? -------------------------------------------------------- Lors de certaines comparaisons, il est impossible de correctement grouper les paires de doublons et certaines paires doivent être retirées des résultats pour être certain de ne pas effacer de faux doublons. Example: Nous avons 3 fichiers, A, B et C. Nous les comparons en utilisant un petit seuil de filtre. La comparaison détermine que A est un double de B, A est un double C, mais que B n'est **pas** un double de C. dupeGuru a ici un problème. Il ne peut pas créer un groupe avec A, B et C. Il décide donc de jeter C hors du groupe. C'est de là que vient la notice '(X hors-groupe)'. Cette notice veut dire que si jamais vous effacez tout les doubles contenus dans vos résultats et que vous faites un nouveau scan, vous pourriez avoir de nouveaux résultats. Je veux marquer tous les fichiers provenant d'un certain dossier. Quoi faire? ----------------------------------------------------------------------------- Activez l'option :doc:`Ne pas montrer les références ` et cliquez sur la colonne Dossier afin de trier par dossier. Il sera alors facile de sélectionner tous les fichiers de ce dossier (avec Shift+selection) puis ensuite d'appuyer sur Espace pour marquer les fichiers sélectionnés. .. only:: edition_se or edition_pe Je veux enlever tous les doublons qui ont une différence de plus de 300KB avec leur référence. ---------------------------------------------------------------------------------------------- * Activez l'option :doc:`Ne pas montrer les références `. * Activez l'option **Montrer les valeurs en tant que delta**. * Cliquez sur la colonne Taille pour changer le tri. * Sélectionnez tous les fichiers en dessous de -300. * Cliquez sur l'action **Retirer sélectionnés des résultats**. * Sélectionnez tous les fichiers au dessus de 300. * Cliquez sur l'action **Retirer sélectionnés des résultats**. Je veux que le fichier avec la plus grande date de dernière modification soit la référence. ------------------------------------------------------------------------------------------- * Activez l'option :doc:`Ne pas montrer les références `. * Activez l'option **Montrer les valeurs en tant que delta**. * Cliquez sur la colonne Modification (deux fois, afin d'avoir un ordre descendant) pour changer le tri. * Sélectionnez tous les fichiers au dessus de 0. * Cliquez sur l'action **Transformer sélectionnés en références**. Je veux marquer tous les fichiers contenant le mot "copie". ----------------------------------------------------------- * Entrez le mot "copie" dans le champ "Filtre" dans la fenêtre de résultats puis appuyez sur Entrée. * Cliquez sur **Tout Marquer** dans le menu Marquer. .. only:: edition_me Je veux enlever les doublons qui ont une différence de plus de 3 secondes avec leur référence. ---------------------------------------------------------------------------------------------- * Activez l'option :doc:`Ne pas montrer les références `. * Activez l'option **Montrer les valeurs en tant que delta**. * Cliquez sur la colonne Temps pour changer le tri. * Sélectionnez tous les fichiers en dessous de -00:03. * Cliquez sur l'action **Retirer sélectionnés des résultats**. * Sélectionnez tous les fichiers au dessus de 00:03. * Cliquez sur l'action **Retirer sélectionnés des résultats**. Je veux que mes chansons aux bitrate le plus élevé soient mes références. ------------------------------------------------------------------------- * Activez l'option :doc:`Ne pas montrer les références `. * Activez l'option **Montrer les valeurs en tant que delta**. * Cliquez sur la colonne Bitrate (deux fois, afin d'avoir un ordre descendant) pour changer le tri. * Sélectionnez tous les fichiers au dessus de 0. * Cliquez sur l'action **Transformer sélectionnés en références**. Je veux enlever les chansons contenant "[live]" ou "[remix]" de mes résultat. ----------------------------------------------------------------------------- Si votre seuil de filtre est assez bas, il se pourrait que vos chansons live ou vos remix soient détectés comme des doublons. Vous n'y pouvez rien, mais ce que vous pouvez faire est d'enlever ces fichiers de vous résultats après le scan. Si, par exemple, vous voulez enlever tous les doublons contenant quelque mot que ce soit entre des caractères "[]", faites: * Entrez "[*]" dans le champ "Filtre" dans la fenêtre de résultats puis appuyez sur Entrée. * Cliquez sur **Tout Marquer** dans le menu Marquer. * Cliquez sur l'action **Retirer marqués des résultats**. J'essaie d'envoyer mes doublons à la corbeille, mais dupeGuru me dit que je ne peux pas. Pourquoi? -------------------------------------------------------------------------------------------------- La plupart du temps, la raison pour laquelle dupeGuru ne peut pas envoyer des fichiers à la corbeille est un problème de permissions. Vous devez avoir une permission d'écrire dans les fichiers que vous voulez effacer. Si vous n'êtes pas familiers avec la ligne de commande, vous pouvez utiliser des outils comme `BatChmod`_ pour modifier vos permissions. Si malgré cela vous ne pouvez toujours pas envoyer vos fichiers à la corbeille, essayez l'option "Supprimer les fichiers directement" qui vous est offerte lorsque vous procédez à l'effacement des doublons. Cette option fera en sorte de supprimer directement les fichiers sans les faire passer par la corbeille. Dans certains cas, ça règle le problème. .. only:: edition_pe Si vous essayez d'effacer des photos dans iPhoto, alors la raison du problème est différente. L'opération rate parce que dupeGuru ne peut pas communiquer avec iPhoto. Il faut garder à l'esprit qu'il ne faut pas toucher à iPhoto pendant l'opération parce que ça peut déranger la communication entre dupeGuru et iPhoto. Aussi, quelque fois, dupeGuru ne peut pas trouver l'application iPhoto. Il faut mieux alors démarrer iPhoto avant l'opération. Dans le pire des cas, `contactez le support HS`_, on trouvera bien. Où sont les fichiers de configuration de dupeGuru? -------------------------------------------------- Si, pour une raison ou une autre, vous voulez effacer ou modifier les fichiers générés par dupeGuru, voici où ils sont: * Linux: ``~/.local/share/data/Hardcoded Software/dupeGuru`` * Mac OS X: ``~/Library/Application Support/dupeGuru`` * Windows: ``\Users\\AppData\Local\Hardcoded Software\dupeGuru`` Les fichiers de préférences sont ailleurs: * Linux: ``~/.config/Hardcoded Software/dupeGuru.conf`` * Mac OS X: Dans le système ``defaults`` sous ``com.hardcoded-software.dupeguru`` * Windows: Dans le Registre, sous ``HKEY_CURRENT_USER\Software\Hardcoded Software\dupeGuru`` Pour la Music Edition et Picture Edition, remplacer "dupeGuru" par "dupeGuru Music Edition" et "dupeGuru Picture Edition", respectivement. .. _Fairware: http://open.hardcoded.net/about/ .. _BatChmod: http://www.lagentesoft.com/batchmod/index.html .. _contactez le support HS: http://www.hardcoded.net/support dupeguru-4.3.1/help/fr/folders.rst000066400000000000000000000065131426171743600171260ustar00rootroot00000000000000Sélection de dossiers ===================== La première fenêtre qui apparaît lorsque dupeGuru démarre est la fenêtre de sélection de dossiers à scanner. Elle détermine la liste des dossiers qui seront scannés lorsque vous cliquerez sur **Scan**. Pour ajouter un dossier, cliquez sur le bouton **+**. Si vous avez ajouté des dossiers dans le passé, un menu vous permettra de rapidement choisir un de ceux ci. Autrement, il vous sera demandé d'indiquer le dossier à ajouter. Vous pouvez aussi utiliser le drag & drop pour ajouter des dossiers à la liste. Pour retirer un dossier, sélectionnez le et cliquez sur **-**. Si le dossier sélectionné est un sous-dossier, son type changera pour **exclus** (voyez plus bas) au lieu d'être retiré. Types de dossiers ----------------- Tout dossier ajouté à la liste est d'un type parmis ces trois: * **Normal:** Les doublons trouvés dans ce dossier peuvent être effacés. * **Reference:** Les doublons trouvés dans ce dossier ne peuvent **pas** être effacés. Les fichiers provenant de ce dossier ne peuvent qu'être en position "Référence" dans le groupes de doublons. * **Excluded:** Les fichiers provenant de ce dossier ne sont pas scannés. Le type par défaut pour un dossier est, bien entendu, **Normal**. Vous pouvez utiliser le type **Référence** pour les dossiers desquels vous ne voulez pas effacer de fichiers. Le type d'un dossier s'applique à ses sous-dossiers, excepté si un sous-dossier a un autre type explicitement défini. .. only:: edition_pe Bibliothèques iPhoto et Aperture ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dupeGuru PE supporte iPhoto et Aperture, ce qui veut dire qu'il sait comment lire le contenu de ces bibliothèques et comment communiquer avec ces applications pour correctement supprimer des photos de celles-ci. Pour utiliser cette fonctionnalité, vous devez ajouter iPhoto et/ou Aperture avec les boutons spéciaux "Ajouter librairie iPhoto" et "Ajouter librairie Aperture", qui apparaissent quand on clique sur le petit "+". Les dossiers ajoutés seront alors correctement interprétés par dupeGuru. Quand une photo est supprimée d'iPhoto, elle est envoyée dans la corbeille d'iPhoto. Quand une photo est supprimée d'Aperture, il n'est malheureusement pas possible de l'envoyer dans sa corbeille. Ce que dupeGuru fait à la place, c'est de créer un projet "dupeGuru Trash" et d'envoyer les photos dans ce projet. Vous pouvez alors supprimer toutes les photos de ce projet manuellement. .. only:: edition_me Bibliothèques iTunes ^^^^^^^^^^^^^^^^^^^^ dupeGuru ME supporte iTunes, ce qui veut dire qu'il sait comment lire le contenu de sa bibliothèque et comment communiquer avec iTunes pour correctement supprimer des chansons de sa bibliothèque. Pour utiliser cette fonctionnalité, vous devez ajouter iTunes avec le bouton spécial "Ajouter librairie iTunes", qui apparait quand on clique sur le petit "+". Le dossier ajouté sera alors correctement interprété par dupeGuru. Quand une chanson est supprimée d'iTunes, elle est envoyée à la corebeille du système, comme un fichier normal. La différence ici, c'est qu'après la suppression, iTunes est correctement mis au fait de cette suppression et retire sa référence à cette chanson de sa bibliothèque. dupeguru-4.3.1/help/fr/index.rst000066400000000000000000000026221426171743600165740ustar00rootroot00000000000000Aide dupeGuru =============== .. only:: edition_se Ce document est aussi disponible en `anglais `__, en `allemand `__ et en `arménien `__. .. only:: edition_se or edition_me dupeGuru est un outil pour trouver des doublons parmi vos fichiers. Il peut comparer soit les noms de fichiers, soit le contenu. Le comparateur de nom de fichier peut trouver des doublons même si les noms ne sont pas exactement pareils. .. only:: edition_pe dupeGuru Picture Edition est un outil pour trouver des doublons parmi vos images. Non seulement il permet de trouver les doublons exactes, mais il est aussi capable de trouver les images ayant de légères différences, étant de format différent ou bien ayant une qualité différente. Bien que dupeGuru puisse être utilisé sans lire l'aide, une telle lecture vous permettra de bien comprendre comment l'application fonctionne. Pour un guide rapide pour une première utilisation, référez vous à la section :doc:`Démarrage Rapide `. C'est toujours une bonne idée de garder dupeGuru à jour. Vous pouvez télécharger la dernière version sur sa http://dupeguru.voltaicideas.net. Contents: .. toctree:: :maxdepth: 2 quick_start folders preferences results reprioritize faq changelog dupeguru-4.3.1/help/fr/preferences.rst000066400000000000000000000203301426171743600177620ustar00rootroot00000000000000Préférences =========== .. only:: edition_se **Type de scan:** Cette option détermine quels aspects du fichier doit être comparé. Un scan par **Nom de fichier** compare les noms de fichiers mot-à-mot et, dépendant des autres préférences ci-dessous, déterminera si les noms se ressemblent assez pour être considérés comme doublons. Un scan par **Contenu** trouvera les doublons qui ont exactement le même contenu. Le scan **Dossiers** est spécial. Si vous le sélectionnez, dupeGuru cherchera des doublons de *dossiers* plutôt que des doublons de fichiers. Pour déterminer si deux dossiers sont des doublons, dupeGuru regarde le contenu de tous les fichiers dans les dossiers, et si **tous** sont les mêmes, les dossiers sont considérés comme des doublons. **Seuil du filtre:** Pour les scan de type **Nom de fichier**, cette option détermine le degré de similtude nécessaire afin de considérer deux noms comme doublons. Avec un seuil de 80, 80% des mots doivent être égaux. Pour déterminer ce pourcentage, dupeGuru compte le nombre de mots total des deux noms, puis compte le nombre de mots égaux, puis fait la division des deux. Un résultat égalisant ou dépassant le seuil sera considéré comme un doublon. Exemple: "a b c d" et "c d e" ont un pourcentage de 57 (4 mots égaux, 7 au total). .. only:: edition_me **Type de scan:** Cette option détermine quels aspects du fichier doit être comparé. La nature de la comparaison varie grandement, dépendant de l'option choisie ici. * **Nom de fichier:** Le nom de fichier des chansons est comparé, mot-à-mot. * **Nom de fichier (Champs):** Les noms de fichiers sont séparés en plusieurs champs séparés par le caractère "-". Le pourcentage de comparaison final est le plus petit parmi les champs. Ce type de scan est utile pour comparer les noms de fichier au format "Artiste - Titre" pour lequel le nom de l'artist contient beaucoup de mots (et donc augmente faussement le pourcentage de comparaison). * **Nom de fichier (Champs sans ordre):** Comme **Nom de fichier (Champs)**, excepté que l'ordre des champs n'a pas d'importance. Par exemple, "Artiste - Titre" et "Titre - Artiste" auraient un pourcentage de 100% au lieu de 0%. * **Tags:** Méthode de loin la plus utile, elle lit les métadonnées des chansons et le compare mot-à-mot. Comme pour **Nom de fichier (Champs)**, le pourcentage final est le plus bas des champs comparés. * **Contenu:** Compare le contenu des chansons. Seul un contenu exactement pareil sera considéré comme un doublon. * **Contenu audio:** Comme **Contenu**, excepté que les métadonnée no sont pas comparées, seulement le contenu audio lui même. Encore une fois, le contenu doit être exactement le même. **Seuil du filtre:** Pour les scans basés sur le nom de fichier ou les tags, cette option détermine le degré de similtude nécessaire afin de considérer deux noms comme doublons. Avec un seuil de 80, 80% des mots doivent être égaux. Pour déterminer ce pourcentage, dupeGuru compte le nombre de mots total des deux noms, puis compte le nombre de mots égaux, puis fait la division des deux. Un résultat égalisant ou dépassant le seuil sera considéré comme un doublon. Exemple: "a b c d" et "c d e" ont un pourcentage de 57 (4 mots égaux, 7 au total). **Tags à scanner:** Pour les scans de type **Tags**, cette option détermine les tags qui seront comparés. .. only:: edition_se or edition_me **Proportionalité des mots:** Pour les scans basés sur les mots, cette option change la méthode de calcul afin que les mots plus long pèsent plus dans la balance. Avec cette option, les mots ont une valeur égale à leur longeur. Par exemple, "ab cde fghi" et "ab cde fghij" ont un pourcentage de 53% (19 caractères au total, 10 caractères de mots égaux (4 pour "ab" et 6 pour "cde")). **Comparer les mots similaires:** Avec cette options, les mots similaires sont comptés comme égaux. Par exemple, "The White Stripes" et "The White Stripe" ont un pourcentage de 100% au lieu de 66%. **Attention:** Cette option a la potentialité de créer beaucoup de faux doublons. Soyez certains de manuellement vérifier vos résultats avant de les effacer. .. only:: edition_pe **Type de scan:** Détermine le type de scan qui sera fait sur vos images. Le type **Contenu** compare le contenu des images de façon "fuzzy", rendant possible de trouver non seulement les doublons exactes, mais aussi les similaires. Le type **EXIF Timestamp** compare les métadonnées EXIF des images (si existantes) et détermine si le "timestamp" (moment de prise de la photo) est pareille. C'est beaucoup plus rapide que le scan par Contenu. **Attention:** Les photos modifiées gardent souvent le même timestamp, donc faites attention aux faux doublons si vous utilisez cette méthode. **Seuil du filtre:** *Scan par Contenu seulement.* Plus il est élevé, plus les images doivent être similaires pour être considérées comme des doublons. Le défaut de 95% permet quelques petites différence, comme par exemple une différence de qualité ou bien une légère modification des couleurs. **Comparer les images de tailles différentes:** Le nom dit tout. Sans cette option, les images de tailles différentes ne sont pas comparées. **Comparer les fichiers de différents types:** Sans cette option, seulement les fichiers du même type seront comparés. **Ignorer doublons avec hardlink vers le même fichier:** Avec cette option, dupeGuru vérifiera si les doublons pointent vers le même `inode `_. Si oui, ils ne seront pas considérés comme doublons. (Seulement pour OS X et Linux) **Utiliser les expressions régulières pour les filtres:** Avec cette option, les filtres appliqués aux résultats seront lus comme des `expressions régulières `_. **Effacer les dossiers vides après un déplacement:** Avec cette option, les dossiers se retrouvant vides après avoir effacé ou déplacé des fichiers seront effacés aussi. **Déplacements de fichiers:** Détermine comment les opérations de copie et de déplacement s'organiseront pour déterminer la destination finale des fichiers: * **Directement à la destination:** Les fichiers sont envoyés directement dans le dossier cible, sans essayer de recréer leur ancienne hierarchie. * **Re-créer chemins relatifs:** Le chemin du fichier relatif au dossier sélectionné dans la :doc:`sélection de dossier ` sera re-créé. Par exemple, si vous ajoutez ``/Users/foobar/MonDossier`` lors de la sélection de dossier et que vous déplacez ``/Users/foobar/MonDossier/SousDossier/MonFichier.ext`` vers la destination ``/Users/foobar/MaDestination``, la destination finale du fichier sera ``/Users/foobar/MaDestination/SousDossier``. * **Re-créer chemins absolus:** Le chemin du fichier est re-créé dans son entièreté. Par exemple, si vous déplacez ``/Users/foobar/MonDossier/SousDossier/MonFichier.ext`` vers la destination ``/Users/foobar/MaDestination``, la destination finale du fichier sera ``/Users/foobar/MaDestination/Users/foobar/MonDossier/SousDossier``. Dans tous les cas, dupeGuru résout les conflits de noms de fichier en ajoutant un numéro en face du nom. **Commande personelle:** Cette option vous permet de définir une ligne de commande à appeler avec le fichier sélectionné (ainsi que sa référence) comme argument. Cette commande sera invoquée quand vous cliquerez sur **Invoquer commande personnalisée**. Cette command est utile si, par exemple, vous avez une application de comparaison visuelle de fichiers que vous aimez bien. Le format de la ligne de commande est la même que celle que vous écrireriez manuellement, excepté pour les arguments, **%d** et **%r**. L'endroit où vous placez ces deux arguments sera remplacé par le chemin du fichier sélectionné (%d) et le chemin de son fichier référence dans le groupe (%r). Si le chemin de votre executable contient un espace, vous devez le placer entre guillemets "". Vous devriez aussi placer vos arguments %d et %r entre guillemets parce qu'il est très possible d'avoir des chemins de fichier contenant des espaces. Voici un exemple de commande personnelle:: "C:\Program Files\SuperDiffProg\SuperDiffProg.exe" "%d" "%r" dupeguru-4.3.1/help/fr/quick_start.rst000066400000000000000000000017071426171743600200210ustar00rootroot00000000000000Démarrage rapide ================= Voici les étapes à suivre pour faire un simple scan par défaut: * Démarrer dupeGuru. * Ajouter les dossiers à scanner soit avec le drag & drop, soit avec le boutton "+". * Cliquez sur **Scan**. * Attendez que le scan soit completé. * Vérifiez que les doublons (les fichiers légèrement indentés) soient vraiment le doublon de la référence du groupe (le fichier au haut du groupe qui ne peut pas être marqué). * Si vous voyer un faux doublon, sélectionnez le puis cliquez sur l'action **Retirer sélectionnés des résultats**. * Quand vous êtes certains de ne pas avoir de faux doublons dans vos résultats, cliquez sur **Tout marquer** dans le menu Marquer et cliquez sur l'action **Envoyer marqués à la corbeille**. Ceci est seulement un scan de base. Il est possible de configurer dupeGuru afin d'obtenir exactement le type de résultat recherché. Pour en savoir plus, il lisez le reste du fichier d'aide. dupeguru-4.3.1/help/fr/reprioritize.rst000066400000000000000000000033411426171743600202130ustar00rootroot00000000000000Re-Prioritizing duplicates ========================== dupeGuru tries to automatically determine which duplicate should go in each group's reference position, but sometimes it gets it wrong. In many cases, clever dupe sorting with "Delta Values" and "Dupes Only" options in addition to the "Make Selected into Reference" action does the trick, but sometimes, a more powerful option is needed. This is where the Re-Prioritization dialog comes into play. You can summon it through the "Re-Prioritize Results" item in the "Actions" menu. This dialog allows you to select criteria according to which a reference dupe will be selected in each dupe group. The list of available criteria is on the left and the list of criteria you've selected is on the right. A criteria is a category followed by an argument. For example, "Size (Highest)" means that the dupe with the biggest size will win. "Folder (/foo/bar)" means that dupes in this folder will win. To add a criterion to the rightmost list, first select a category in the combobox, then select a subargument in the list below, and then click on the right pointing arrow button. The order of the list on the right is important (you can re-order items through drag & drop). When picking a dupe for reference position, the first criterion is used. If there's a tie, the second criterion is used and so on and so on. For example, if your arguments are "Size (Highest)" and then "Filename (Doesn't end with a number)", the reference file that will be picked in a group will be the biggest file, and if two or more files have the same size, the one that has a filename that doesn't end with a number will be used. When all criteria result in ties, the order in which dupes previously were in the group will be used.dupeguru-4.3.1/help/fr/results.rst000066400000000000000000000234341426171743600171720ustar00rootroot00000000000000Résultats ========== Quand dupeGuru a terminé de scanner, la fenêtre de résultat apparaît avec la liste de groupes de doublons trouvés. À propos des groupes de doublons --------------------------------- Un groupe de doublons est un groupe de fichier dans lequel tous les fichiers sont le doublon de tous les autres fichiers. Chaque groupe a son **fichier de référence** (le premier fichier du groupe). Ce fichier est celui qui n'est jamais effacé, et il est donc impossible de le marquer. Les critères utilisés pour décider de quel fichier d'un groupe devient la référence sont multiples. Il y a d'abord les dossiers référence. Tout fichier provenant d'un dossier de type "Référence" ne peut être autre chose qu'une référence dans un groupe. Si il n'y a pas de fichiers provenant d'un dossier référence, alors le plus gros fichier est placé comme référence. Bien entendu, dans certains cas, il est possible que dupeGuru ne choisisse pas le bon fichier. Dans ce cas, sélectionnez un doublon à placer en position de référence, puis cliquez sur l'action **Transformer sélectionnés en références**. Vérifier les résultats ------------------------ Bien que vous pouvez tout simplement faire **Tout marquer** puis tous envoyer à la corbeille, il est recommandé de vérifier les résultats avant, surtout si votre seuil de filtre est bas. Pour vous aider dans cette tâche, vous pouvez utiliser le panneau de détails. Ce panneau montre les détails du fichier sélectionné côte-à-côte avec sa référence. Vous pouvez aussi double-cliquer sur un fichier pour l'ouvrir avec son application associée. Si vous avez plus de faux doublons que de vrais (si votre seuil de filtre est très bas), la meilleure façon de procéder, au lieu de retirer les faux doublons des résultat, serait de marquer seulement les vrais doublons. Marquer et sélectionner ----------------------- Dans le vocabulaire de dupeGuru, il y a une nette différence entre sélectionner et marquer. Les fichiers **sélectionnés** sont ceux qui sont surlignés dans la liste. On peut sélectionner plusieurs fichiers à la fois en tenant Shift, Control ou Command lorsqu'on clique sur un fichier. Les fichiers **marqués** sont ceux avec la petite boite cochée. Il est possible de marquer les fichiers sélectionnés en appuyant sur **espace**. Ne pas montrer les références ------------------------------- Quand ce mode est activé, les groupes de doublons sont (momentanément) brisés et les doublons sont montrés individuellement, sans leurs références. On peut agir sur les fichiers sous ce mode de la même façon que sous le mode normal. L'attrait principal de ce mode est le tri. En mode normal, les groupes ne peuvent pas être brisés, et donc les résultats sont triés en fonction de leur référence. Sous ce mode spécial, le tri est fait au niveau des fichiers individuels. Il est alors possible, par exemple, de facilement marquer tous les fichiers de type "exe": * Activer le mode **Ne pas montrer les références**. * Ajouter la colonne "Type" par le menu "Colonnes". * Cliquez sur la colonne Type pour changer le tri. * Trouvez le premier fichier avec un type "exe". * Sélectionnez-le. * Trouvez le dernier fichier avec un type "exe". * Tenez Shift et sélectionnez-le. * Appuyez sur espace pour marquer les fichiers sélectionnés. Montrer les valeurs en tant que delta ------------------------------------- Sous ce mode, certaines colonnes montreront leur valeurs relativement à la valeur de la référence du groupe (de couleur orange, pour bien les différencier des autres valeurs). Par exemple, si un fichier a une taille de 1.2 MB alors que la référence a une taille de 1.4 MB, la valeur affichée sous ce mode sera -0.2 MB. Les valeurs non numériques sont aussi affectées par le mode delta. Par contre, plutôt que montrer la différence avec la valeur de référence (ce qui est impossible), elles indiquent si elles sont pareilles en adoptant la couleur orange dans le cas ou la valeur est différent. Il est ainsi possible de facilement identifier, par exemple, tous le doublons qui ont un nom de fichier différent de leur référence. Les deux modes ensemble ----------------------- Quand on active ces deux modes ensemble, il est alors possible de faire de la sélection de ficher assez avancée parce que le tri de fichier se fait alors en fonction des valeurs delta. Il devient alors possible de, par exemple, sélectionner tous les fichiers qui ont une différence de plus de 300 KB par rapport à leur référence, ou d'autres trucs comme ça. Même chose pour les valeurs non numériques: quand les deux modes sont activés, les règles de tri pour les valeurs non-numériques change. On commence par grouper les doublon selon si leur valeur est orange ou non, pour ensuite procéder aux règles de tri normales. Avec ce système, il est alors facile de, par exemple, marquer toues les doublons qui ont un nom de fichier différent de leur référence: il suffit de trier par nom de fichier. Filtrer les résultats --------------------- Il est possible de filtrer les résultats pour agir sur un sous-ensemble de ceux-ci, par exemple tous les fichiers qui contiennent le mot "copie". Pour filtrer les résultats, entrer le filtrer dans le champ de la barre d'outils, puis appuyer sur Entrée. Pour annuler le filtre, appuyez sur le X dans le champ. En mode simple (le mode par défaut), ce que vous tapez est ce qui est filtré. Il n'y a qu'un caractère spécial: **\***. Ainsi, si vous entrez "[*]", le filtre cherchera pour tout nom contenant les "[" et "]" avec quelquechose au milieu. Pour un filtrage avancé, activez **Utiliser les expressions régulières pour les filtres** dans les :doc:`preferences`. Votre filtre sera alors appliqué comme une `expression régulière`_. Les filtres sont dans tous les cas insensibles aux majuscules et minuscules. Les expression régulière pour s'appliquer à un fichier n'ont pas besoin de correspondre au nom entier. Une correspondance partielle suffit. Vous remarquerez peut-être que ce ne sont pas tous les fichiers de vos résultats filtrés qui s'appliquent au filtre. C'est parce que les groupes ne sont pas brisés par les filtres afin de permettre une meilleure mise en context. Par contre, ces fichier seront en mode "Lecture seule" et ne pourront être marqués. Actions ------- Voici la liste des actions qu'il est possible d'appliquer aux résultats. * **Vider la liste de fichiers ignorés:** Ré-initialise la liste des paires de doublons que vous avez ignorés dans le passé. * **Exporter vers HTML:** Exporte les résultats vers un fichier HTML et l'ouvre dans votre browser. * **Envoyer marqués à la corbeille:** Le nom le dit. * **Déplacer marqués vers...:** Déplace les fichiers marqués vers une destination de votre choix. La destination finale du fichier dépend de l'option "Déplacements de fichiers" dans les :doc:`preferences`. * **Copier marqués vers...:** Même chose que le déplacement, sauf que c'est une copie à la place. * **Retirer marqués des résultats:** Retire les fichiers marqués des résultats. Ils ne seront donc ni effacés, ni déplacés. * **Retirer sélectionnés des résultats:** Retire les fichiers sélectionnés des résultats. Notez que si il y a des fichiers références parmi la sélection, ceux-ci sont ignorés par l'action. * **Transformer sélectionnés en références:** Prend les fichiers sélectionnés et les place à la position de référence de leur groupe respectif. Si l'action est impossible (si la référence provient d'un dossier référence), rien n'est fait. * **Ajouter sélectionnés à la liste de fichiers ignorés:** Retire les fichiers sélctionnés des résultats, puis les place dans une liste afin que les prochains scans ignorent les paires de doublons qui composaient le groupe dans lequel ces fichiers étaient membres. * **Ouvrir sélectionné avec l'application par défaut:** Ouvre le fichier sélectionné avec son application associée. * **Ouvrir le dossier contenant le fichier sélectionné:** Le nom dit tout. * **Invoquer commande personnalisée:** Invoque la commande personnalisé que vous avez définie dans les :doc:`preferences`. * **Renommer sélectionné:** Renomme le fichier sélectionné après vous avoir demandé d'entrer un nouveau nom. **Déplacer des fichiers dans iPhoto/iTunes:** Attention, quand vous déplacez des fichiers des bibliothèques iPhoto ou iTunes, elles ne sont pas vraiment déplacées, mais copiée. Il n'y a pas d'action de déplacement possible dans ces bibliothèques. Options de suppression ---------------------- Ces options, présentées lors de l'action de suppression de doublons, déterminent comment celle-ci s'exécute. La plupart du temps, ces options n'ont pas a être activées. * **Remplacer les fichiers effacés par des liens:** les fichiers supprimés seront replacés par des liens (`symlink`_ ou `hardlink`_) vers leur fichiers de référence respectifs. Un symlink est un lien symbolique (qui devient caduque si l'original est supprimé) et un hardlink est un lien direct au contenu du fichier (même si l'original est supprimé, le lien reste valide). Sur OS X et Linux, cette fonction est supportée pleinement, mais sur Windows, c'est un peu compliqué. Windows XP ne le supporte pas, mais Vista oui. De plus, cette fonction ne peut être utilisée que si dupeGuru roule avec les privilèges administratifs. Ouaip, Windows c'est la joie. * **Supprimer les fichiers directement:** Plutôt que d'envoyer les doublons à la corbeille, directement les supprimer. Utiliser cette option si vous avez de la difficulté à supprimer des fichiers (ce qui arrive quelquefois quand on travaille avec des partages réseau). .. _expression régulière: http://www.regular-expressions.info .. _hardlink: http://en.wikipedia.org/wiki/Hard_link .. _symlink: http://en.wikipedia.org/wiki/Symbolic_link dupeguru-4.3.1/help/hy/000077500000000000000000000000001426171743600147425ustar00rootroot00000000000000dupeguru-4.3.1/help/hy/faq.rst000066400000000000000000000354271426171743600162560ustar00rootroot00000000000000Հաճախ Տրվող Հարցեր ========================== .. topic:: Ի՞նչ է dupeGuru-ը: .. only:: edition_se dupeGuru-ն ծրագիր է, որ գտնում է համակարգչի ֆայլերի կրկնօրինակները: Այն կարող է ստուգել ըստ ֆայլի անվան կամ բովանդակության: Ֆայլի անվամբ փնտրման հնարավորությունը ոչ ճշգրիտ համընկնումներ է տալիս երբեմն: Շատ ժամանակ անունները նույնն են, բայց ֆայլերը տարբեր են: .. only:: edition_me dupeGuru Music Edition-ը ծրագիր է, որ գտնում է համակարգչի երաժշտական ֆայլերի կրկնօրինակները: Այն կարող է հիմնվել ֆայլի անունները ստուգելու վրա՝ ըստ կցապիտակների և բովանդակության: Ֆայլի անունների և կցապիտակների ստուգումը ոչ ճշգրիտ ալգորիթմ է, քանզի այն կարող է գտնել համընկնումներ, որոնք իրականում նույնը չեն: .. only:: edition_pe dupeGuru Picture Edition (PE՝ կարճ)-ը ծրագիր է, որ գտնում է համակարգչի նկարների ֆայլերի կրկնօրինակները: Այն կարող է գտնել ոչ միայն ճշգրիտ համընկնումները, այլև այն կարող է գտնել տարբեր որակի և տեսակի նկարների (PNG, JPG, GIF և այլն...) համընկնումներ: .. topic:: Ի՞նչն է այս ծրագիրը առանձնացնում մյուս նմանատիպ ծրագրերից: Ստուգելու համակարգը չափազանց նուրբ է: Կարող եք ինքներդ հարմարեցնել այն՝ ստանալու համար այն արդյունքը, ինչը որ Ձեզ պետք է: Կարող եք լրացուցիչ կարդալ այս մասին dupeGuru-ի կարգավորման ընտրանքներում՝ :doc:`Կարգավորումների էջում `: .. topic:: Ո՞րքանով է անվտանգ օգտագործելու dupeGuru-ը: Շատ անվտանգ է: dupeGuru-ը նախագծվել է՝ համոզված լինելու համար, որ Դուք չջնջեք այն ֆայլերը, որոնք չպետք է ջնջեք: Նախ, կա հղմամբ համակարգային թղթապանակ, որը հնարավորություն է տալիս Ձեզ որոշելու թղթապանակներ, որտեղ Դուք բացարձակ **չեք** ցանկանում, որ dupeGuru-ն հնարավորություն տա Ձեզ ջնջելու ֆայլերը այստեղից, և ապա կա խմբի հղմամբ համակարգային համակարգ, որը համոզմունք է ստեղծում, որ Դուք **միշտ** պետք է պահեք գոնե մեկ անդամ կրկնօրինակվող խմբի: .. topic:: Ո՞րոն եք dupeGuru-ի լիցենզիայի սահմանափակումները: Փորձնական եղանակում, Դուք կարող եք միայն կատարել գործողություններ 10 կրկնօրինակների հետ միաժամանակ: Ծրագրի `Անվճար տարբերակում `_ mode, այնուհանդերձ չկան էական սահմանափակումներ: .. topic::Ջնջելու համար նշելու դաշտի պատուհանը ակտիվ չէ: Ի՞նչ անել: Չեք կարող նշել հղումը (Առաջին ֆայլը) կրկնօրինակվող խմբի: Այնուհանդերձ, ինչ կարող եք Դուք անել առաջ մղելու համար կրկնօրինակվող ֆայլը հղմանը: Այսպիսով, եթե ֆայլը ցանկանում եք նշել որպես հղում, ընտրեք կրկնօրինակվող ֆայլը խմբից, որը ցանկանում եք տանել հղման մեջ, և սեղմեք **Գործողություններ-->Դարձնել ընտրվածը հղում**: Եթե հղվող ֆայլը հղման թղթապանակից է (ֆայլի անունը գրված է կապույտ տառերով), Դուք չեք կարող ջնջել այն հղման դիրքից: .. topic:: Ես ունեմ թղթապանակ, որտեղից ես իրապես չեմ ցանկանում ջնջել ֆայլեր: Եթե Դուք ցանկանում եք համոզված լինել, որ dupeGuru-ն երբեք չի ջնջի ֆայլ կոնկրետ թղթապանակից, համոզված եղեք, որ տվել եք կարգը **Հղման** :doc:`folders`: .. topic:: Ի՞նչ է սա '(X վնասված)'՝ նշված դրության տողում: Որոշ դեպքերում, որոշ համընկնումներ ներառված չեն վերջնական արդյունքում երկրորդական պատճառներով: Եկեք նայենք կոնկրետ օրինակի վրա: Մենք ունենք 3 ֆայլ. A, B և C: Մենք ստուգում ենք այն՝ օգտագործելով ֆիլտրի ցածր մակարդակով: Ստուգիչը արդեն որոշել է, որ A համընկնումները B-ի, A-ի համընկնումները C-ին, բայց B-ն **չի** համընկնում C-ին: Այստեղ dupeGuru-ն ունի մի շարք խնդիրներ: Այն չի կարող ստեղծել կրկնօրինակվող խումբ A, B և C իրենում, որովհետև ոչ բոլոր ֆայլերն են խմբում համընկնում միմյանց: Այն կարող է ստեղծել 2 խումբ. մեկ A-B խումբ և ապա մեկ A-C խումբ, բայց այն չի լինի՝ անվտանգության նկատառումներից ելնելով: Եկեք մտածենք սրա մասին. Եթե B-ն չի համընկնում C-ին, հնարավոր է դա նշանակում է, որ անգամ B, C կամ երկուսն էլ իրականում կրկնօրինակներ չեն: Եթե այնտեղ լինեն 2 խմբեր (A-B և A-C), ապա Դուք պետք է ջնջեք B-ն և C-ն: Եթե դրանցից մեկը կրկնօրինակ չէ, ապա դա այն չէ, ինչը որ Ձեզ պետք է, այնպես չէ՞: Այսպիսով, ինչ dupeGuru չի մի դեպքում նման սրան՝ բացառելով A-C համընկնումը (և ավելացնում է տեղեկացում դրության տողում): Այսպիսով, եթե Դուք ջնջեք B-ն և վերսկսեք ստուգումը, ապա կունենք A-C համընկնում հաջորդ արդյունքներում: .. topic:: Ես ցանկանում եմ նշել բոլոր ֆայլերը որոշված թղթապանակից: Ի՞նչ կարող եմ ես անել: Միացնել :doc:`Միայն Սխալները ` եղանակը և սեղմեք թղթապանակի սյանը՝ դասավորելու համար կրկնօրինակները ըստ թղթապանակների: Հետագայում հեշտ կլինի ընտրելու բոլոր կրկնօրինակները նույն թղթապանակից և ապա սեղմեք Space՝ ընտրելու ահմար բոլոր կրկնօրինակները: .. only:: edition_se or edition_pe .. topic:: Ես ցանկանում եմ հեռացնել բոլոր ֆայլերը, որոնք 300 ԿԲ-ից ավելի են հղվող ֆայլից: Ի՞նչ կարող եմ ես անել: * Միացնել :doc:`Միայն Սխալները ` եղանակում: * Միացնել **Դելտա նշանակությունները** եղանակը: * Սեղմեք "Չափը" սյանը՝ դասավորելու համար արդյունները ըստ չափի; * Ընտրեք բոլոր կրկնօրինակները՝ -300-ից ցածր: * Սեղմեք **Ջնջել ընտրվածը Արդյունքներից**: * Ընտրեք բոլոր կրկնօրինակերը, որոնք մեծ են 300-ից: * Սեղմեք **Ջնջել ընտրվածները Արդյունքներից**: .. topic:: Ես ցանկանում եմ դարձնել վերջին փոփոխված ֆայլերը հղման ֆայլեր: Ի՞նչ կարող եմ ես անել: * Միացնել :doc:`Միայն Սխալները ` եղանակում: * Միացնել **Դելտա նշանակությունները** եղանակը: * Սեղմեք "Ըստ փոփոխության" սյանը՝ արդյունքները ըստ փոփոխման դասավորելու համար: * Սեղմեք "Ըստ փոփոխության" սյանը՝ կրկնելու համար դասավորման կարգը: * Ընտրել բոլոր կրկնօրինակները 0-ից բարձր: * Սեղմեք **Դարձնել ընտրվածը հղում**: .. topic:: Ես ցանկանում եմ նշել բոլոր այն կրկնօրինակները, որոնք պարունակում են "պատճենել" բառը: Ինչպե՞ս դա անել: * **Windows**. Սեղմեք **Գործողություններ --> կիրառել ֆիլտրը**, ապա նշեք "պատճենել", հետո սեղմեք ԼԱՎ: * **Mac OS X**. Նշեք "պատճենել" "Ֆիլտրում" դաշտում՝ գործիքների վահանակում: * Սեղմեք **Նշել --> Նշել բոլորը**: .. only:: edition_me .. topic:: Ես ցանկանում եմ հեռացնել բոլոր երգերը, որոնք 3 վայրկյանից հեռու են իրենց հղման ֆայլից: Ի՞նչ կարող եմ ես անել: * Միացնել :doc:`Միայն Սխալները ` եղանակում: * Միացնել **Դելտա նշանակությունները** եղանակը: * Սեղմեք "Ժամանակը" սյանը՝ դասավորելու համար արդյունքները ըստ ժամանակի: * Ընտրեք բոլոր կրկնօրինակները՝ -00:03-ից ցածր: * Սեղմեք **Ջնջել ընտրվածը արդյունքներից**: * Ընտրել բոլոր կրկնօրինակները 00:03-ից բարձր: * Սեղմեք **Ջնջել ընտրվածը արդյունքներից**: .. topic:: Ես ցանկանում եմ դարձնել իմ բարձրագույն բիթրեյթ ունեցող երգերը հղման ֆայլեր: Ի՞նչ կարող եմ ես անել: * Միացնել :doc:`Միայն Սխալները ` եղանակում: * Միացնել **Դելտա նշանակությունները** եղանակը: * Սեղմեք "Բիթրեյթը" սյանը՝ դասավորելու համար արդյունքները ըստ բիթրեյթի: * Սեղմեք "Բիթրեյթը" սյանը՝ կրկնելու համար դասավորման կարգը: * Ընտրել բոլոր կրկնօրինակները 0-ց բարձր; * Սեղմեք **Դարձնել ընտրվածը հղում**: .. topic:: Ես չեմ ցանկանում [live] և [remix] տարբերակները իմ երգերի՝ հաշված որպես կրկնօրինակ: Ինչպե՞ս դա անել: Եթե Ձեր համեմատության սահմանը բավականին ցածր է, հնարավոր է Դուք ավարտվեք կենդանի և ռեմիքս տարբերակներով Ձեր երգերի արդյունեքներում: Դուք ոչինչ չեք կարող անել դրա համար, բայց կա ինչ-որ եղանակ՝ դրանք ստուգման արդյունքներից ջնջելու համար: Եթե օրինակի համար, Դուք ցանկանում եք ջնջել ամեն մի երգ, որը գտնվում է գծիկների միջև []:. * **Windows**. Սեղմեք **Գործողություններ --> Կիրառել ֆիլտրը**, ապա տեսակը "[*]", ապա սեղմեք ԼԱՎ: * **Mac OS X**. Տեսակը "[*]" "Ֆիլտր" դաշտում՝ գործիքաշերտի: * Սեղմեք **Նշել --> Նշել բոլորը**: * Սեղմեք **Գործողություններ --> Ջնջել ընտրվածը արդյունքներից**. .. topic:: Ես փորձում եմ կրկնօրինակները ուղարկել Աղբարկղ, բայց dupeGuru-ն ինձ ասում է, որ չես կարող: Ինչու՞: Ի՞նչ կարող եմ ես անել: Շատ ժամանակ, պատճառը, թե ինչու dupeGuru-ն չի կարողանում տեղափոխել ֆայլերը Աղբարկղ, կայանում է ֆայլի լիազորությունների մեջ: Դուք պետք է *գրեք* լիազորությունները ֆայլերում, որոնք որ ցանկանում եք ուղարկել Աղբարկղ: Եթե Ձեզ անծանոթ է Հրամանի տողը, ապա Դուք կարող եք օգտագործել լրացուցիչ գործիքներ, ինչպես օրինակ `BatChmod `_ լիազորումները նշելու համար: Եթե dupeGuru-ն դեռ շարունակում է խնդիրներ առաջ բերել կապված լիազորությունների հետ, ապա կան խնդիրներ կապված՝ "Տեղափոխել նշվածը..." որպես շրջանցիկ խորամանկություն: Ուստի ֆայլերը Աղբարկ տեղափոխելիս Դուք ուղարկում եք այն ժամանակավոր թղթապանակ "Տեղափոխել նշվածը..." գործողությամբ և ապա Դուք կջնջեք այդ թղթապանակը ձեռադիր; .. only:: edition_pe Եթե Դուք փորձում եք ջնջել *iPhoto* նկարները, ապա ձախողման պատճառը տարբեր է: Ջնջելը ձախողվել է, որովհետև dupeGuru-ը չի կարողանում համագործակցել iPhoto: Լինել տեղեկացված, որ ջնջումը նորմալ է աշխատում, Դուք չեք նախատեսում խաղարկել ձայն iPhoto-ին, քանսզի dupeGuru-ն աշխատում է: Նաև, երբեմն, Applescript համակարգը չի կողմնորոշվում որտեղ փնտրել iPhoto՝ բացելու համար: Հավանական է, այս դեպքերում պետք է բացել iPhoto-ն *մինչև* Դուք ուղարկեք Ձեր կրկնօրինակները Աղբարկղ: Եթե այս ամենը ձախողվի, `կապնվեք HS աջակցության թիմի հետ `_, մենք կփորձեք օգնել Ձեզ: .. todo:: This FAQ qestion is outdated, see english version.dupeguru-4.3.1/help/hy/folders.rst000066400000000000000000000063431426171743600171400ustar00rootroot00000000000000Թղթապանակի ընտրություն ======================= Առաջին թղթապանակը, որ Դուք տեսնում եք dupeGuru-ն բացելիս դա թղթապանակի ընտրությունն է: Այս պատուհանը պարունակում է թղթապանակների ցանկը, որոնք կստուգվեն **Ստուգել** սեղմելիս: Այս պատուհանը շատ հեշտ է օգտագործել: Եթե ցանկանում եք ավելացնել թղթապանակ, ապա սեղմեք **+** կոճակը: Եթե մինչ այդ ավելացնեք թղթապանակը, ապա կերևա ավելացված վերջին թղթապանակների ցանկը: Կարող եք սեղմել նրանցից մեկի վրա՝ ավելացնելու համար ուղղակի Ձեր ցանկում: Եթե սեղմեք հայտնվող պատուհանի առաջին ֆայլին՝ **Ավելացնել նոր թղթապանակ...**, ապա Ձեզ հարցում կկատարվի թղթապանակ ավելացնելու մասին: Եթե երբեք չեք ավելացրել թղթապանակ, ապա ոչ մի ընտրացանկ չի երևա և Ձեզ ուղղակի հարցում կարվի նոր թղթապանակ ավելացնելու մասին: Այլընտրանքյին ճանապարհով թղթապանակներ կարող եք ավելացնել պարզապես դրանք գցելով ցանկում: Թղթապանակը հեռացնելու համար ընտրեք թղթապանակը, սեղմեք **-**: Եթե ընտրված է ենթաթղթպանակը, երբ Դուք սեղմում եք կոճակին, ընտրված թղթապանակը կնշվի որպես **բացառված** (նայեք այստեղ)՝ ջնջվելու փոխարեն: Թղթապանակի վիճակը ------------------ Յուրաքանչյուր թղթապանակ կարող է լինել հետևյալ 3 եղանակներից մեկում. * **Նորմալ.** Այս թղթապանակում գտնված կրկնօրինակները կարող են ջնջվել: * **Հղված.** Կրկնօրինակներ են գտնվել այս թղթապանակում, որոնք **չեն կարող** ջնջվել: Ֆայլերը այս թղթապանակից կարող են միայն ավարտվել **հղում** դիրքով խմբում: Եթե մեկ ֆայլից ավելի են հղման թղթապանակների հղումները, ապա միայն մեկը կպահվի: Մնացածը կջնջվեմ խմբից: * **Բացառված.** Ֆայլերը այս թղթապանակում կներառվեն ստուգման մեջ: Թղթապանակի հիմնական վիճակը, իհարկե՛ **Նորմալ է**: Կարող եք օգտագործել **Հղված** վիճակը թղթապանակի համար, եթե ցանկանում եք համոզված լինել, որ ոչ մի ֆայլ չի ջնջվի: Եթե նշել եք թղթապանակի վիճակը, բոլոր ենթաթղթապանակները միանգամից կժառանգեն այս վիճակը, եթե վիճակը պարզորոշ տրված է թղթապանակի կարգում: .. todo:: Add iPhoto/Aperture/iTunes libraries notes dupeguru-4.3.1/help/hy/index.rst000066400000000000000000000040021426171743600165770ustar00rootroot00000000000000dupeGuru help =============== .. only:: edition_se Այս փաստաթուղթը հասանելի է նաև՝ `Ֆրանսերեն `__ և `Գերմաներեն `__. .. only:: edition_se or edition_me dupeGuru ծրագիր է՝ գտնելու կրկնօրինակ ունեցող ֆայլեր Ձեր համակարգչում: Այն կարող է անգամ ստուգել ֆայլի անունները կան բովանդակությունը: Ֆայլի անվան ստուգման հնարավորությունները ոչ ճշգրիտ համընկման ալգորիթմով, որը կարող է գտնել ֆայլի անվան կրկնօրինակներ, անգամ եթե դրանք նույնը չեն: .. only:: edition_pe dupeGuru Picture Edition-ը (PE՝ կարճ) գործիք է, որը գտնում է նկարների կրկնօրինակները Ձեր համակարգչում: Գտնում է ոչ միայն նույնանման կրկնօրինակները, այլ նաև կարող է գտնել տարբեր տեսակի և որակի նկարներ (PNG, JPG, GIF և այլն...): Չնայած dupeGuru-ն կարող է հեշտությամբ օգտագործվել առանց օգնության, այնուհանդերձ եթե կարդաք այս ֆայլը, այն մեծապես կօգնի Ձեզ ընկալելու ծրագրի աշխատանքը: Եթե Դուք նայում եք ձեռնարկը կրկնօրինակների առաջին ստուգման համար, ապա կարող եք ընտրել :doc:`Արագ Սկիզբ ` հատվածը: Շատ լավ միտք է պահելու dupeGuru թարմացված: Կարող եք բեռնել վեբ կայքի համապատասխան էջից http://dupeguru.voltaicideas.net: Պարունակությունը. .. toctree:: :maxdepth: 2 quick_start folders preferences results reprioritize faq changelog dupeguru-4.3.1/help/hy/preferences.rst000066400000000000000000000373431426171743600200070ustar00rootroot00000000000000Կարգավորումներ ================ .. only:: edition_se **Ստուգելու տեսակը.** Այս ընտրանքը որոշում է, թե ֆայլերի որ ասպեկտը կհամեմատվի կրկնօրինակված ստուգման հետ: Եթե Դուք ընտրեք **Ֆայլի անունը**, ապա dupeGuru-ն կհամեմատի յուրաքանչյուրը բառ-առ-բառ և կախված է հետևյալ այլ ընտրանքներից, այն կորոշի արդյոք բավական են համընկնող բառերը դիտելու համար 2 ֆայլերի կրկնօրինակները: Եթե ընտրեք միայն **Բովանդակությունը**, ապա նույնատիպ ֆայլերը նույն բովանդակությամբ կհամընկնեն: **Թղթապանակներ.** ստուգելու հատուկ տեսակ է: Երբ ընտրեք սա, dupeGuru-ն կստուգի կրկնօրինակ *թղթապանակները*՝ կրկնօրինակ ֆայլերի փոխարեն: Որոշելու համար արդյոք անկախ երկու թղթապանակները կրկնօրինակ են, կստուգվեն թղթապանակների ամբողջ պարունակությունը և եթե **բոլոր** ֆայլերի բովանդակությունը համընկնի, ապա թղթապանակները կորոշվեն որպես կրկնօրինակներ: **Ֆիլտրի խստությունը.** Եթե Դուք ընտրեք **Ֆայլի անունը** ստուգելու տեսակը, այս ընտրանքը կորոշի, թե ինչքանով նման պետք է լինեն ֆայլերի անունները, որ dupeGuru-ն ճանաչի դրանք որպես կրկնօրինակներ: Եթե ֆիլտրը առավել խիստ է, օրինակ՝ 80, ապա դա նշանակում է, որ երկու ֆայլերի անունների բառերի 80%-ը պետք է համընկնի: Որոշելու համար համընկնման տոկոսը, dupeGuru-ն նախ հաշվում է բառերի ընդհանուր քիանակը **երկու** ֆայլերի անուններում, ապա հաշվում է համընկնումների քանակը (ամեն բառ համընկնում է 2-ի հաշվին) և բաժանում ընդհանուր գտնված բառերի համընկնումների միջև: Եթե արդյունքը բարձր է կամ հավասար ֆիլտրի խստությանը, ապա մենք ունեք կրկնօրինակի համընկնում: Օրինակ՝ "a b c d" և "c d e" ունեն համընկնման տոկոս, որը հավասար է 57-ի (4 բառ են համընկնում, 7 ընդհանուր բառից): .. only:: edition_me **Ստուգելու եղանակը.** Այս ընտրանքը որոշում է, թե որ ասպեկտն է ֆայլերի՝ համեմատելի կրկնօրինակման ստուգմանը: Կրկնօրինակների ստուգման բնույթը փոխվում է մեծապես կախված, թե ինչի եք ընտրում այս ընտրանքը: * **Ֆայլի անունը.** Ցանկացած երգ ունի իր ֆայլի անվան մասնատումը բառերի և ապա ամեն բառ կհամեմատվի՝ հաշվելու համար համընկնման տոկոսը: Եթե այս տոկոսը ավելի բարձր է կամ հավասար **Ֆիլտրի խստությանը** (նայել՝ մանրամասների համար), dupeGuru-ն կդիտարկի երկու երգերը որպես կրկնօրինակներ: * **Ֆայլի անունը - Դաշտերը.** Ինչպես օրինակ **Ֆայլի անունը**, բացառում է, որ մեկ ֆայլի անունը բաժանվի բառերի, այս բառերը ապա կխմբավորվեն դաշտերում: Դաշտերի բաժանիչը " - " է: Համընկնման վերջնական տոկոսը կլինի համընկնման ցածրագույն տոկոսը դաշտերի միջև: Այսպիսով, "Կատարողը - Վերնագիրը" և "Կատարողը - Այլ վերնագիրը" կունենա համընկման տոկոս՝ 50 (**Ֆայլի անունը** ստուգմամբ, կլինի 75). * **Ֆայլի անունը - Դաշտերը (անկարգ).** Ինչպես օրինակ **Ֆայլի անունը - Դաշտերը** բացառությամբ, որ դաշտի կարգը չի համընկնում: Օրինակ՝ "Կատարողը - Վերնագիրը" և "Վերնագիրը - Կատարողը" կունենան համընկնման 100 տոկոս՝ 0-ի փոխարեն: * **Կցապիտակներ.** Այս եղանակը կարդում է յուրաքանչյուր երգի կցապիտակները (մետատվյալները) և համեմատում է նրանց դաշտերը: Այս եղանակը, ինչպես օրինակ **Ֆայլի անունը - Դաշտերը** դիտարկում են համընկնման ցածրագույն դաշտը՝ համեմատման վերջնական տոկոսից: * **Պարունակությունը.** Ստուգման այս եղանակը օգտագործում են երգերի բովանդակությունը՝ որոշելու համար, թե որն են կրկնօրինակները: 2 երգերը համընկնեցնելու համար այս եղանակով, դրանք պետք է ունենան **բացառապես նույն բովանդակությունը**: * **Ձայնի բովանդակությունը.** Նույնն են բովանդակությամբ, բայց միայն ձայնի պարունակությունն է համեմատելի (առանց մետատվյալների): **Ֆիլտրի խստությունը.** Եթե ընտրում եք ֆայլի անունը կամ կցապիտակը՝ հիմնված ստուգման եղանակի վրա, ապա այս ընտրանքը որոշում է, թե ինչքան են նման երկու ֆայլի անունները/կցապիտակները պետք է լինեն dupeGuru-ի կողմից դիտարկվող կրկնօրինակներ: Եթե ֆիլտրի խստությունը օրինակի համար 80 է, դա նշանակում է, որ երկու ֆայլի անունների բառերի համընկնումը 80% է: Որոշելու համար համընկնման տոկոսը, dupeGuru-ն առաջին հաշվով որոշում է **երկու** ֆայլի անունների առաջին հաշվարկի ընդհանուր քանակը, ապա համընկնող բառերի համընկնման քանակը (բոլոր բառերը համընկնում են 2-ի) և ապա բաժանել համընկնող բառերի թիվը ընդհանուր բառերի թվին: Եթե արդյունքը բարձր է կամ հավասար ֆիլտրի խստությանը, ապա մենք ունենք կրկնօրինակի համընկնում: Օրինակ՝ "a b c d" և "c d e" ունի համընկնման 57 տոկոս (4 բառերի համընկնում, 7 ընդամենը բառեր): **Ստուգվող կցապիտակները.** Երբ օգտագործվում է **Կցապիտակներ** ստուգելու եղանակը, կարող եք ընտրել կցապիտակներ, որոնք կօգտագործվեն համեմատման համար: .. only:: edition_se or edition_me **Բառի կշիռը.** Եթե ընտրում եք **Ֆայլի անունը** ստուգելու եղանակը, ապա այս ընտրանքը որոշակիորեն փոխում է համընկնման տոկոսը հաշվելու եղանակը: Բառի կշռմամբ կրկնօրինակի քանակի փոխարենը 1 նշանակությունը ունենալու համար, ամեն բառը ունի հավասարազոր նշանակություն՝ առկա գրանշանների թվին: Բառի կշռմամբ, "ab cde fghi" և "ab cde fghij" կունենա համընկնման տոկոս՝ 53% (19 ընդամենը գրանշաններ, 10 գրանշանների համընկնում (4-ը "ab"-ի և 6-ը "cde"-ի համար)): **Նմանատիպ բառերի համընկնում.** Եթե միացնեք այս ընտրանքը, նմանատիպ բառերը կհաշվեն որպես համընկնումներ: Օրինակ՝ "Սպիտակ շրջանակ" և "Սպիտակ շրջանակ" ունի համընկնման % հավասարազոր 100-ի՝ 66-ի փոխարեն, եթե ընտրանքը միացված է: **Զգուշացում.** Այս ընտրանքը զգուշությամբ օգտագործեք: Հավանական է ստացված տվյալների մեծ մասը կեղծ լինեն: Այնուհանդերձ, այն կօգնի Ձեզ գտնելու կրկնօրինակներ, որոնք այլ ճանապարհով հնարավոր չի եղել գտնել: Ստուգելու ընթացքը նաև նշանակալի դանդաղ է, եթե այս ընտրանքը միացված է: .. only:: edition_pe **Ստուգելու եղանակը.** Այս ընտրանքը որոշում է ստուգելու եղանակը, որը կկիրառվի նկարների նկատմամբ: **Պարունակությունը** ստուգելու եղանակը համեմատում է ակտուալ նկարների բովանդակությունը ոչ ճշգրիտ եղանակով (հնարավորություն տալով գտնելու ոչ միայն անմիջապես կրկնօրինակները, այլ նաև նմանատիպ այլ ֆայլերը): **EXIF Timestamp** ստուգելու եղանակը նայում է նկարի EXIF մետատվյալը (եթե այն կա) և համընկնող նկարները, որոնք որ նույնն են: Սա ավելի արագ է, քան բովանդակությամբ ստուգելը: **Զգուշացում.** Փոփոխված նկարները սովորաբար պահում են նույն EXIF timestamp-ը, ուստի նախ նայեք արդյունքները, ապա գործեք: **Ֆիլտրի խստությունը.** *Ստուգում է միայն բովանդակությունը:* Այս ընտարնքի բարձրագույն նիշը, բնորոշում է ֆիլտրի "խստությունը" (Այլ կերպ ասաց, արդյունքը ավելի քիչ է լինում): Նույն որակի նկարներից շատերը երբեմն համընկնում են 100%-ով՝ անգամ եթե տեսակը ուրիշ է (PNG և JPG օրինակի համար): Այնուհանդերձ, եթե ցանկանում եք, որ PNG-ն համապատասխանի ցածր որակի JPG-ին, պետք է նշեք ֆիլտրի խստությունը 100-ից ցածր: Ծրագրայինը 95 է: **Տարբեր չափերով նկարների համապատասխանեցում.** Եթե ընտրեք սա, տարբեր չափերի նկարները կթույլատրվեն կրկնօրինակվող նույն խմբում: **Կարող է ուղղել ֆայլի տեսակը.** Եթե ընտրում եք այս վանդակը, ապա կրկնօրինակվող խմբերը կթույլատրվեն ունենալու տարբեր ընդլայնումներ: Եթե չընտրեք, ապա դրանք չեն լինի! **Անտեսել կրկնօրինակների հղումը նույն ֆայլին file:** Եթե այս ընտրանքը միացված է, dupeGuru-ն կստուգի կրկնօրինակները՝ տեսնելու համար արդյոք դրանք հղվում են նույնին `inode `_: Եթե այո, ապա դրանք չեն որոշվի որպես կրկնօրինակ: (Միայն OS X և Linux-ում) **Ֆիլտրելիս օգտագործել կանոնավոր սահմանումներ.** Եթե ընտրեք սա, ապա ֆիլտրման հնարավորությունը կդիմի ֆիլտրման հերթին, ինչպես որ **կանոնավոր սահմանում**: Դրա բացատրությունը դուրս կգա այս փաստաթղթի շրջանակից: Ավելին կարող եք կարդալ այստեղ՝ `regular-expressions.info `_: **Ջնջել դատարկ թղթապանակները ջնջելուց կամ տեղափոխելուց.** Երբ այս ընտրանքը միացված է, թղթապանակները կջնջվեն, երբ որ ֆայլը ջնջվի կամ տեղափոխվի և թղթապանակը դատարկ լինի: **Պատճենել և տեղափոխել.** Որոշում է, թե Պատճենելու և Տեղափոխելու գործողությունները (գործողություն ընտրացանկից): * **Տեղադրությունից աջ.** Բոլոր ֆայլերը կուղարկվեն ընտրված տեղ՝ առանց փորձելու վերստեղծելու աղբյուրի ճանապարհը բոլորի համար: * **Վերստեղծել հարաբերական ճանապարհը.** Աղբյուր ֆայլի ճանապարհը կվերստեղծվի նշանակված թղթապանակում՝ խորքայինից մեկ աստիճան վեր՝ Թղթապանակներ վահանակից: Օրինակ՝ եթե ավելացնեք``/Users/foobar/SomeFolder`` Թղթապանակներ վահանակ և տեղափոխեք ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` նշանակության թղթապանակ ``/Users/foobar/MyDestination``, ֆայլի վերջնական տեղավորությունը կլինի ``/Users/foobar/MyDestination/SubFolder`` (``SomeFolder`` բաժանվել է աղբյուր ճանապարհից վերջնական տեղադրությունում): * **Վերստեղծել հարաբերական ճանապարհը.** Աղբյուր ֆայլի ճանապարհը կվերստեղծվի նշանակված թղթապանակում՝ իր հարթությունում: Օրինակ՝ եթե տեղափոխեք ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` նշանակության թղթապանակ ``/Users/foobar/MyDestination``, ֆայլի վերջնական տեղավորությունը կլինի ``/Users/foobar/MyDestination/Users/foobar/SomeFolder/SubFolder``: Ամեն դեպքում, dupeGuru լավ է հարթում անունների կոնֆլիկտը՝ նախապատրաստելով նշանակության ֆայլի անվան թիվը՝ եթե ֆայլը արդեն առկա է նշված տեղում: **Ընտրված հրամանը.** Այս կարգավորումը որոշում է հրամանը, որը կկանչվի "Կանչել Ընտրված հրամանը" գործողությամբ: Կարող եք կանչել ցանկացած արտաքին ծրագիր՝ այս գործողությամբ: Սա կարող է օգտակար լինել եթե օրինակ փոխարենը ունեք տվյալների փոխանցման լավ ծրագիր: Հրամանի տեսակը նույնն է, ինչ Դուք կգրեք Հրամանի տողում, բացառությամբ որտեղ կան 2 լրացումներ. **%d** և **%r**: Այս լրացումները կվերագրվեն ընտրված զոհի (%d) ճանապարհով և ընտրված զոհի հղման ֆայլով (%r): Եթե կատարելի ֆայլի ճանապարհը պարունակում է բացատներ, ապա պետք է փակեք այն "" չակերտներով: Նաև պետք է փակեք լրացումները չակերտներով, որովհետև շատ հնարավոր է, որ զոհի ճանապարհները և հղումները կպարունակեն բացատներ: Ահա ընտրված հրամանի օրինակ՝ :: "C:\Program Files\SuperDiffProg\SuperDiffProg.exe" "%d" "%r" dupeguru-4.3.1/help/hy/quick_start.rst000066400000000000000000000031021426171743600200210ustar00rootroot00000000000000Արագ Սկիզբ =========== Արագ սկսելու համար dupeGuru-ն, պարզապես կատարեք ստանդարտ ստուգում՝ օգտագործելով ծրագրային կարգավորումները: * Բացել dupeGuru-ն: * Ավելացնել թղթապանակներ՝ ստուգելու համար նաև վերցնել & գցելը կամ "+" կոճակը: * Սեղմեք **Ստուգել**: * Սպասեք, մինչ ստուգումը կավարտվի: * Նայեք ցանկացած կրկնօրինակին (Ֆայլեր, որոնք նշվել են) և ստուգվել, իրականում կրկնօրինակել խմբի հղմանը (Ֆայլը կրկնօրինակելուց առաջ չի նշվում և ընտրված չէ): * Եթե ֆայլը սխալ կրկնօրինակ է, ապա ընտրեք այն և սեղմեք **Գործողություններ-->Հեռացնել ընտրվածը Արդյունքներից**: * Եթե համոզված եք, որ կրկնօրինակը արդյունքներում կա, ապա սեղմեք **Խմբագրել-->Նշել բոլորը**, և ապա **Գործողություններ-->Ուղարկել Նշվածը Աղբարկղ**: Սա միայն բազային ստուգում է: Կան բազմաթիվ կարգավորումներ, որոնք հնարավորություն են տալիս նշելու տարբեր արդյունքներ և մի քանի եղանակներ արդյունքների փոփոխման: Մանրամասների համար կարդացեք Օգնության ֆայլը: dupeguru-4.3.1/help/hy/reprioritize.rst000066400000000000000000000057351426171743600202350ustar00rootroot00000000000000Վերաառաջնայնության կրկնօրինակներ ================================ dupeGuru-ը փորձում է որոշել, թե որ կրկնօրինակները պետք է գնան յուրաքանչյուր խմբի դիրքում, բայց երբեմն սխալ է ստանում: Շատ դեպքերում, խելամիտ դասավորումը "Դելտա նշանակության" և "Միայն սխալները" ընտրանքների ավելացնելով "Դարձնել ընտրվածը հղում" գործողության խորամանկություն է, բայց երբեմն, պահանջվում են ավելի լավ ընտրանքներ: Ահա այստեղ է, որ վերաառաջնայնավորման պատուհանը բացվում է: Կարող եք կանչել այն "Վերաառաջնայնավորման արդյունքները" կետից՝ "Գործողություններ" ընտրացանկից: Այս պատուհանը հնարավորություն է տալիս Ձեզ ընտրելու չափանիշներ՝ հղման սխալին համապատասխան և կընտրվի յուրաքանչյուր սխալի խումբը: Հասանելի չափանիշների ցանկը ձախում է և Ձեր ընտրած չափանիշների ցանկը գտնվում է աջում: Չափանիշն դա բաժինն է, որը հետևում է փաստարկին: Օրինակ՝ "Չափը (Բարձրագույն)" նշանակում է, որ սխալը հետևում է մեծագույն չափի հաղթողին: "Թղթապանակը (/foo/bar)" նշանակում է, որ սխալները թղթապանակում կհաղթեն: Ավելացնելու համար փաստարկ ամենաաջ մասում, նախ ընտրեք բաժինը, ապա ընտրեք ենթափաստարկ հետևյալ ցանկում և ապա սեղմեք կոճակի սլաքի աջ մասում: Ցանկի կարգը աջից շատ կարևոր է (կարող եք վերակարգավորել ֆայլերը վերցնել և գցելու միջոցով): Երբ սխալի տեղորոշումը հղման դիրքում է, ապա օգտագործվում է առաջին փաստարկը: Եթե դա կապված է, ապա երկրորդ փաստարկն է օգտագործվում և այլն և այլն: Օրինակ, եթե Ձեր փաստարկները "Չափը (բարձրագույն)" են և ապա "Ֆայլի անունը (Չի ավարտվում թվով)", ապա հղման ֆայլը, որը կընտրվի խմբում, ապա կլինի մեծագույն ֆայլը և եթե երկու կամ ավելի ֆայլեր ունեն նույն չափը, ապա մեկը ունի ֆայլի անուն, որը չի ավարտվում թվով, կօգտագործվի: Երբ փաստարկի արդյունքը կապված է, կարգը, որի սխալները նախկինում էին, խումբը պետք է օգտագործվի: dupeguru-4.3.1/help/hy/results.rst000066400000000000000000000433021426171743600171770ustar00rootroot00000000000000Արդյունքները ============= Երբ dupeGuru-ն ավարտի կրկնօրինակների ստուգումը, կցուցադրի արդյունքները կրկնօրինակ խմբերի ցանկում: Կրկնօրինակ խմբերի մասին ------------------------- Կրկնօրինակման խումբը դա ֆայլերի խումբ է, որոնք բոլորը համընկնում են միմյանց: Ամեն խումբ ունի իր **հղվող ֆայլը** և մեկ կամ մի քանի **կրկնօրինակ ֆայլեր**: Հղվող ֆայլը դա խմբի առաջին ֆայլն է: Այն ընտրված չէ, նրանից ցածր և փոխարեն կրկնօրինակ ֆայլերի: Կարող եք նշել կրկնօրինակ ֆայլերը, բայց երբեք չեք կարող նշել հղվող ֆայլը խմբում: Սա երկրորդ պատճառն է՝ կանխելու dupeGuru-ին ջնջելու ոչ միայն կրկնօրինակ ֆայլերը, այլև դրանց հղումները: Համոզվա՞ծ եք, չէ, որ պետք չէ անել դա: Ինչն է որոշում, թե որ ֆայլերը հղմամբ են և որ ֆայլերը կրկնօրինակ են՝ թղթապանակի նախնական վիճակում: Ֆայլը հղվող թղթապանակից միշտ հղվում է կրկնօրինակի խմբում: Եթե բոլոր ֆայլերը նորմալ թղթապանակից են, ապա չափն է որոշում, թե որ ֆայլը կլինի կրկնօրինակ խմբի հղումը: dupeGuru-ն ընդունում է, որ Դուք կցանկանաք պահել մեծ ֆայլերը, ուստի դրանք կտեղադրվեն հղման խմբում: Կարող եք փոխել հղման ֆայլը խմբում ձեռադիր: Դա անելու համար ընտրեք կրկնօրինակ ֆայլը և սեղմեք **Գործողություններ-->Դարձնել ընտրվածը հղմամբ**: Նայել արդյունքները -------------------- Չնայած պարզապես կարող եք սեղմել **Խմբագրել-->Նշել բոլորը** և ապա **Գործողություններ-->Ուղարկել նշվածը Աղբարկղ** արագորեն ջնջելու համար բոլոր կրկնօրինակ ֆայլերը արդյունքներից, միշտ խորհուրդ է տրվում նախ նայել կրկնօրինակները և հետո միայն ջնջել: Օգնելու համար Ձեզ նայելու արդյունքները, կարող եք օգտագործել **Մանրամասների վահանակը**: Այս վահանակը ցուցադրում է բոլոր մանրամասները ընտրված ընթացիկ ֆայլի, ինչպես նաև հղման մանրամասները: Սա շատ հարմար է արագորեն որոշելու, թե արդյոք կրկնօրինակը իրոքից կրկնօրինակ է, թե ոչ: Կարող եք նաև կրկնակի սեղմեք ֆայլի վրա՝ բացելու համար այն նրա հետ ասոցիացված ծրագրով: Եթե ունեք շատ սխալ կրկնօրինակներ, ապա ճիշտ կրկնօրինակները (Եթե Ձեր ֆիլտրի խստությունը շատ է ցածր) որոշելու լավագույն եղանակը դրանք նայելն է, ընտրեք ճիշտ կրկնօրինակները և ապա սեղմեք **Գործողություններ-->Ուղարկել նշվածները Աղբարկղ**: Եթե իսկական կրկնօրինակները ավելի շատ են, քան սխալները, ապա կարող եք օգտագործել **Գործողություններ-->Ջնջել նշվածները արդյունքներից**: Նշում և Ընտրում --------------------- **նշվածը** կրկնօրինակ է՝ նշված նշանով, որը ընտրվում է: **ընտրվածը** կրկնօրինակ է, որը ընդգծվում է կամ առանձնացվում է: Ընտրելու բազմակի եղանակները կարող են կատարվել dupeGuru-ում ստանդարտ ճանապարհով (Shift/Command/Control սեղմամբ): կարող եք փոփոխել բոլոր ընտրված կրկնօրինակների վիճակը՝ սեղմելով **space**: Ցուցադրել Միայն Սխալները ------------------------- Եթե այս ընտրանքը միացված է, ապա կրկնօրինակները ցուցադրվում են առանց իրենց համապատասխան հղվող ֆայլի։ Կարող եք ընտրել, նծել կամ դասավորել այս ցանկը, ինչպես օոր նորմալ եղանակում։ dupeGuru-ի արդյունքները, երբ այն նորմալ եղանակում է, դասավորվում են համաձայն կրկնօրինակվող խմբերի' **հղվող ֆայլի** ։ Սա նշանակում է, որ եթե Դուք ցանկանաք, օրինակի համար, նշել բոլոր կրկնօրինակները "exe" ընդլայնմամբ, ապա չեք կարող պարզապես դասավորել արդյունքները ըստ "Տեսակի"՝ ունենալու համար բոլոր exe կրկնօրինակները միասին, որովհետև խումբը կարող է կազմված լինի մեկից ավելի ֆայլերից։ Ահա այստեղ է, որ աշխատում է Միայն Սխալները եղանակը։ Նշելու համար բոլոր "exe" կրկնօրինակները, Դուք պարզապես պետք է՝ * Միացնեք Միայն Սխալները եղանակը։ * Ավելացնեք "Տեսակը" սյունը՝ "Սյուներ" ընտրացանկին։ * Սեղմեք "Տեսակը" սյանը՝ դասավորելու համար ցանկը ըստ տեսակի։ * Տեղադրել "exe" տեսակի առաջին կրկնօրինակը։ * Ընտրեք այն։ * Պտտեք ներքև ցանկում՝ տեղադրելու համար "exe" տեսակի վերջին կրկնօրինակը։ * Սեղմած պահեք Shift-ը և սեղմեք նրա վրա։ * Սեղմեք Space՝ նշելու համար բոլոր կրկնօրինակները։ Դելտա նշանակությունները -------------------------- Եթե միացնեք սա, որոշ սյուներ կցուցադրվեն նշանակություն՝ հարաբերական կրկնօրինակների հղմանը՝ բացարձակ նշանակությունների փոխարեն։ Այս դելտա նշանակությունները նաև կցուցադրվեն տարբեր գույներով, ուստի կարող եք դրանք հեշտությամբ տեսնել։ Օրինակ՝ եթե կրկնօրինակը 1.2 ՄԲ է և եթե նրա հղումը 1.4 ՄԲ է, ապա Չափը սյունում կցուցադրվի -0.2 ՄԲ։ Միայն Սխալները և Դելտա նշանակությունները ------------------------------------------ Միայն Սխալները եղանակը բացում է իր իսկական ուժը, երբ Դուք օգտագործում եք այն Դելտա նշանակությունների հետ։ Երբ միացնեք այն, բացարձակ նշանակությունների փոխարեն կցուցադրվեն հարաբերական նշանակությունները։ Այսպիսով օրինակ, եթե ցանկանում եք արդյունքներից հեռացնել բոլոր կրկնօրինակները, որոնք 300 Կբ-ից մեծ են իրենց հղումներից, ապա չեք կարող դասավորել միայն սխալները արդյունքները ըստ չափի, այդ դեպքում ընտրեք բոլոր կրկնօրինակները, որոնք -300 են Չափը սյունից, ջնջեք դրանք և ապա արեք նույն գործողությունը նաև 300-ից բարձր կրկնօրինակների համար՝ ցանկի ներքևում։ Կարող եք նաև օգտագործել սա՝ փոխելու համար կրկնօրինակման ցանկի հղման առաջնայնությունը։ Նոր ստուգումը կատարելուց հետո, եթե չլինեն հղվող թղթապանակներ, հղվող ֆայլը ամեն խմբի կլինի ամենամեծ ֆայլը։ Եթե ցանկանում եք փոխել այն, օրինակի համար, ըստ վերջին փոփոխման ժամանակի, կարող եք դասավորել միայն սխալները արդյունքները ըստ փոփոխման ժամանակի **նվազման** կարգով, ընտրեք բոլոր կրկնօրինակները, որոնց փոփոխման դելտա ժամանակը բարձր է 0-ից և սեղմեք **Դարձնել ընտրվածը հղում**։ Պատճառը,որ դասավորում եք ըստ նվազման կարգի այն է, որ եթե 2 ֆայլերից, որոնք կընտրվեն նույն կրկնօրինակ խմբում, երբ սեղմեք **Դարձնել ընտրվածը հղում**, ապա ցանկի միայն առաջինը կդառնա հղում, մյուսները կանտեսվեն։ Եվ մինչև Դուք ցանկանաք վերջին փոփոխված ֆայլը դարձնել փոփոխված՝ ունենալով դասավորման նվազման կարգը, ապա ցանկի առաջին ֆայլը կլինի վերջին փոփոխված ֆայլը։ .. todo:: Add "Non-numerical delta" information. Ֆիլտրում --------- dupeGuru-ն աջակցում է հետստուգման ֆիլտրում։ Սրանով Դուք կարող եք սեղմել ներքև արդյունքները, որպեսզի կարողանաք կատարեք գործողություններ դրա հետ։ Օրինակ՝ կարող եք հեշտությամբ նշել բոլոր կրկնօրինակերը նրանց անվան մեջ պարունակող "պատճեն" հատկությամբ՝ ֆիլտրի կողմից օգտագործված արդյունքներից։ .. todo:: Qt has a toolbar search field now, not a menu item. **Windows.** Ֆիլտրելու հնարավորությունը օգտագործելու համար սեղմեք Գործողություններ --> Կիրառել ֆիլտրը, գրեք կիրառվող ֆիլտրը և սեղմեք Կիրառել։ Չֆիլտրված արդյունքներին վերադառնալու համար սեղմեք Գործողություններ --> Չեղարկել ֆիլտրը։ **Mac OS X.** Ֆիլտրելու հնարավորությունը օգտագործելու համար նշեք Ձեր ֆիլտրը "Ֆիլտր" որոնման դաշտում գործիքաշերտի։ Չֆիլտրված արդյունքներին վերադառնալու համար սեղմեք դատարկ թողեք դաշտը կամ սեղմեք "X"։ Պարզ եղանակում (ծրագրային եղանակն է), ինչ տեսակի ֆիլտր է տողում օգտագործվել փաստացի ֆիլտրման համար, խմբային նիշի բացառությամբ **\***. Այսպիսով, եթե նշում եք "[*]" որպես ֆիլտր, այն կհամընկնի [] փակագծերի հետ, այնուհանդերձ կլինի այդ փակագծերի միջև։ Լրացուցիչ ընդլայնված ֆիլտրման համար, կարող եք միացնել "Ֆիլտրելիս օգտագործել կանոնավոր սահմանումները"։ Ապա ֆիլտրման հնարավորությունը կօգտագործվի **կանոնավոր սահմանմամբ** ։ Կանոնավոր սահմանումը դա համապատասխանացման տեքստի լեզուն է։ Առավել մանրամասն կարող եք կարդալ `regular-expressions.info `_ կայքում։ Համապատասխանեցումները զգայուն չեն ո՛չ պարզ, ո՛չ էլ regexp եղանակում։ Համապատասխանեցման ֆիլտրի դեպքում, Ձեր կանոնավոր սահմանումը չի ունենա ամբողջական ֆայլի անունը, այն միայն կպարունակի սահմանմանը համապատասխան տողին։ Կարող եք տեղեկացնել, որ ոչ բոլոր կրկնօրինակներն են ֆիլտրված արդյունքներում համապատասխանում ֆիլտրին։ Ահա թե ինչու ինչքան շուտ որ պարզ կրկնօրինակը խմբում համապատասխանի ֆիլտրին ամբողջ խումբը կմնա արդյունքներում, ուստի ավելի հեշտ կլինի նայելու կրկնօրինակների կազմը։ Այնուհանդերձ, չհամապատասխանող կրկնօրինակերը "հղման եղանակում են"։ Չնայած որ Դուք կարող եք կատարել գործողություններ, ինչպես օրինակ նշել բոլորը և համոզված լինեք, որ միայն նշված են ֆիլտրված կրկնօրինակերը։ Գործողություններ Ընտրացանկը ---------------------------- * **Մաքրել անտեսման ցանկը.** Հեռացնում է Ձեր ավելացրած բոլոր անտեսված համընկնումները։ Դուք պետք է սկսեք նոր ստուգում, որպեսզի նոր մաքրված անտեսումների ցանկը էֆֆեկտիվ լինի։ * **Արտածել արդյունքները XHTML-ով.** Վերցնում է ընթացիկ արդյունքները և ստեղծում XHTML ֆայլը։ Սյուննրը, որոնք տեսանելի են այս կոճակը սեղմելիս կլինեն նաև XHTML ֆայլում։ Ֆայլը միանգամից կբացվի հիմնական դիտարկիչում։ * **Ուղարկել նշվածները Աղբարկղ.** Բոլոր նշված կրկնօրինակերը հեռացնում է Աղբարկղ։ * **Ջնջել նշվածը և Վերագրել հղմամբ.** Բոլոր նշված կրկնօրինակերը հեռացնում է Աղբարկղ, բայց դա անելուց հետո ջնջված ֆայլերը վերագրվում են ըստ `հղման `_ հղվող ֆայլում (Միայն OS X և Linux-ում) * **Տեղափոխել նշվածը՝...:** Հարցնում է Ձեզ թղթապանակի մասին և ապա տեղափոխում է բոլոր նշված ֆայլերը այդ թղթապանակ։ Աղբյուր ֆայլերի ճանապարհը կարող է վերստեղծվել նշանակության թղթապանակում՝ կախված "Պատճենելու և Տեղափոխելու" կարգավորումներից։ * **Պատճենել նշվածը՝...:** Հարցնում է Ձեզ թղթապանակի մասին և ապա պատճենում է բոլոր նշված ֆայլերը այդ թղթապանակ։ Աղբյուր ֆայլերի ճանապարհը կարող է վերստեղծվել նշանակության թղթապանակում՝ կախված "Պատճենելու և Տեղափոխելու" կարգավորումներից։ * **Հեռացնել նշվածները արդյունքներից.** Հեռացնում է բոլոր նշված կրկնօրինակները արդյունքներից։ Ակտուալ ֆայլերին դա չի վերաբերվի և դրանք կմնան։ * **Հեռացնել ընտրվածները արդյունքներից.** Հեռացնում է բոլոր ընտրված կրկնօրինակները արդյունքներից։ Հիշեք, որ ընտրված բոլոր հղվող ֆայլերը կանտեսվեն,այս գործողությամբ կջնջվեն միայն կրկնօրինակերը։ * **Դարձնել ընտրվածը հղում.** Առաջ է մղում բոլոր ընտրված կրկնօրինակները որպես հղումներ։ Եթե կրկնօրինակը խմբի մասն է, որը ունի հղման թղթապանակ (կապույտ գույնով), ապա ոչ մի գործողություն չի կատարվի դրա համար։ Իսկ եթե միևնույն խմբում կան մեկից ավելի ընտրված կրկնօրինակներ, ապա առաջ կմղվի ամեն խմբից միայն առաջինը։ * **Ավելացնել ընտրվածը անտեսումների ցանկին.** Նախ բոլոր կրկնօրինակները հեռացվում են արդյունքների ցանկից, ապա ավելացվում է կրկնօրինակի համընկումը և ընթացիկ հղումը անտեսումների ցանկին։ Այս համընկնումը այլևս առաջ չի գա հետագա ստուգումների ժամանակ։ Կրկնօրինակը կարող է հետ բերվել, բայց այն կհամապատասխանի հղման այլ ֆայլի։ Կարող եք մաքրել անտեսումների ցանկը Մաքրել անտեսումների ցանկը հրամանով։ * **Բացել ընտրվածը հիմական ծրագրով.** Բացում է ֆայլը իր հետ ասոցիացված ծրագրով։ * **Ցուցադրել ընտրվածը որոնման մեջ.** Բացում է ֆայլը պարունակող թղթապանակը։ * **Կանչել Ընտրված հրամանը.** Բացում է կարգավորումներոմ Ձեր կողմից նշված արտաքին ծրագիրը։ * **Անվանափոխել ընտրվածը.** Ձեզ հարցում կկատարվի նոր անվան համար, ապա ընտրված ֆայլը կանվանափոխվի։ .. todo:: Add Move and iPhoto/iTunes warning .. todo:: Add "Deletion Options" section.dupeguru-4.3.1/help/ru/000077500000000000000000000000001426171743600147505ustar00rootroot00000000000000dupeguru-4.3.1/help/ru/faq.rst000066400000000000000000000355051426171743600162610ustar00rootroot00000000000000Часто задаваемые вопросы ========================== .. topic:: Что такое dupeGuru? .. only:: edition_se dupeGuru это инструмент для поиска дубликатов файлов на вашем компьютере. Он может сканировать либо имен файлов или контента.Имя файла функций сканирования нечеткого соответствия алгоритма, который позволяет найти одинаковые имена файлов, даже если они не совсем то же самое. .. only:: edition_me dupeGuru Music Edition представляет собой инструмент для поиска дублирующихся песен в вашей музыкальной коллекции. Он может строить свою сканирование файлов, тегам или содержания.Имя файла и тэг проверяет функция нечеткого соответствия алгоритм, который может находить дубликаты файлов или теги, даже если они не совсем то же самое. .. only:: edition_pe dupeGuru Picture Edition (PE для краткости) представляет собой инструмент для поиска дубликатов фотографий на вашем компьютере. Не только он может найти точные соответствия, но он также может найти дубликаты среди фотографий разного рода (PNG, JPG, GIF и т.д..) И качество. .. topic:: Что делает его лучше, чем другие сканеры дублировать? Сканирования является чрезвычайно гибкой. Вы можете настроить его, чтобы действительно получить, каких результатов вы хотите. Вы можете прочитать больше о опция настройки dupeGuru в :doc:`Настройки `. .. topic:: Насколько безопасно использовать dupeGuru? Очень безопасной. dupeGuru был разработан, чтобы убедиться, что вы не удаляете файлы, которые вы не хотели удалить. Во-первых, существует система отсчета папку, которая позволяет определить папки, в которых вы абсолютно не хотите dupeGuru, чтобы вы удаляете файлы там, и тогда есть система контрольной группы, что гарантирует, что вы всегда держать по крайней мере один член группы дубликатов. .. topic:: Каковы ограничения демо dupeGuru? В демо-режиме, вы можете только выполнять действия над 10 дубликаты сразу. в `Fairware `_ режиме, однако, Есть никаких ограничений. .. topic:: Знак коробку файл я хочу удалить отключена. Что я должен сделать? Вы не можете пометить ссылки (первый файл) дубликат группы. Однако то, что вы можете сделать, заключается в содействии дублировать файл справки. Таким образом, если файл, который Вы хотите, чтобы отметить это ссылки, выделите дубликатов файлов в группу, которую вы хотите продвигать на ссылку, и нажмите на кнопку **Действия -> Добавить выбранной ссылки** . Если ссылка файл из папки ссылки (имя файла написаны на синими буквами), вы не можете удалить его из исходного положения. .. topic:: У меня есть папка, из которой я действительно не хочу, чтобы удалить файлы. IЕсли вы хотите быть уверены, что dupeGuru никогда не будет удалять файл из определенной папки, убедитесь, что установили в состояние **Ссылка** на: документ: `папки`. .. topic:: Что это за '(X отбрасывается) "уведомление в строке состояния? В некоторых случаях, несколько матчей не включены в окончательные результаты по соображениям безопасности. Позвольте мне привести пример. У нас есть 3 файла: A, B и C. Мы сканируем их с помощью фильтра низких твердости.Сканер определяет, что матчи с B, матчи с С, но делает B ** не ** матч с С. При этом, dupeGuru имеет вид проблемы. Она не может создать дубликат группы А, В и С в это, потому что не все файлы в группе будет соответствовать вместе. Это может создать 2 группы: одна группа AB, а затем одна группа AC, но это не будет, по соображениям безопасности. Давайте думать об этом: если Б не совпадает с С, она, вероятно, означает, что либо B, C или оба на самом деле не дубликаты. Если не было бы 2 группы (АВ и АС), вы бы в конечном итоге удалить оба B и C. И если один из них не дублировать, что на самом деле не то, что вы хотите делать, правильно? Так что dupeGuru делает в таком случае является, чтобы отменить матч AC (и добавляет уведомление в строке состояния). Таким образом, если вы удалите B и повторно запустить сканирование, вам придется соответствовать переменного тока в следующий результат. .. topic:: Я хочу, чтобы отметить все файлы из определенной папки. Что я могу сделать? Включить: документ: `обманутые Только <результаты>` режим и нажать на папку колонки для сортировки дубликатов по папкам. Затем он будет легким для вас, чтобы выбрать все дубликаты из той же папке, а затем нажать клавишу пробел, чтобы отметить все выбранные дубликатов. .. only:: edition_se or edition_pe .. topic:: Я хочу, чтобы удалить все файлы, которые более 300 Кб от их ссылке на файл. Что я могу сделать? * Включить :doc:`Только обманутые ` режим. * Включить **Значения Делта** режим. * Нажмите на "размер" столбца для сортировки результатов по размеру. * Выбрать все дубликаты ниже -300. * Нажмите на **Удалить выбранные из результатов**. * Выбрать все дубликаты более 300. * Нажмите на **Удалить выбранные из результатов**. .. topic:: Я хочу, чтобы мои последние измененные файлы файлы справки. Что я могу сделать? * Включить: документ: `обманутые Только <результаты>` режим. * Включить **Значения делта** режим. * Нажмите на колонку "Модификация" для сортировки результатов по дате изменения. * Нажмите на колонку "Модификация" снова изменить порядок сортировки. * Выберите все дубликаты на 0. * Нажмите на **Сделать выбранной ссылки**. .. topic:: Я хочу, чтобы отметить все дубликаты, содержащие слово "копия". Как мне это сделать? * **Windows**: Нажмите на **Действия -> Применить фильтр**, затем введите "копия", нажмите кнопку ОК. * **Mac OS X**: тип "копия" в "Фильтр" поле на панели инструментов. * Нажмите на **Отметить -> Отметить все**. .. only:: edition_me .. topic:: Я хочу, чтобы удалить все песни, которые более чем на 3 секунды от своей ссылке на файл. Что я могу сделать? * Включить: документ: `обманутые Только <результаты>` режим. * Включить **Значения делта** режим. * Нажмите на "Время" колонку для сортировки результатов по времени. * Выберите все дубликаты ниже -00:03. * Нажмите на **Удалить выбранные из результатов**. * Выберите все дубликаты на 00:03. * Нажмите на **Удалить выбранные из результатов**. .. topic:: Я хочу, чтобы мой высокий битрейт файлов песни ссылки. Что я могу сделать? * Включить: документ: `обманутые Только <результаты>` режиме * Включить **Значения делта** режим. * Нажмите на кнопку "Битрейт" колонку для сортировки результатов по битрейт. * Нажмите на кнопку "Битрейт" колонна снова изменить порядок сортировки. * Выберите все дубликаты на 0. * Нажмите на **Сделать выбранной ссылки**. .. topic:: Я не хочу [жить] и [ремикс] версии моих песен считаться дубликатами. Как мне это сделать? Если ваше сравнение порог достаточно низким, вы, вероятно, в конечном итоге с живой и ремикс версии ваших песен в своих результатах. Там вы ничего не можете сделать, чтобы предотвратить это, но есть кое-что можно сделать, чтобы легко удалить их со своего результаты после сканирования: после сканирования, фильтрации. Если, например, вы хотите удалить все песни с чем-либо в квадратных скобках []: * **Windows**: Нажмите на **Действия -> Применить фильтр**, а затем введите "[*]", нажмите кнопку ОК. * **Mac OS X**: Тип "[*]" в "Фильтр" поле на панели инструментов. * Нажмите на Отметить **-> Отметить все**. * Нажмите на **Действия -> Удалить выбранные из результатов**. .. topic:: Я пытался отправить свои дубликаты в корзину, но dupeGuru говорит мне, он не может это сделать. Почему? Что я могу сделать? Большую часть времени, поэтому dupeGuru не можете отправлять файлы в корзину из-за права доступа к файлам. Вы должны написать * * разрешения на файлы, которые вы хотите отправить в корзину. Если вы не знакомы с командной строкой, вы можете использовать утилиты, такие как `BatChmod ` _ исправить Ваши права. Если dupeGuru еще дает вам неприятности после фиксации ваших прав, было несколько случаев, когда с помощью "Перемещение Помечено к ..." в качестве обходного пути сделали свое дело. Таким образом, вместо отправки файлов в корзину, вы посылаете их во временную папку с "Переместить Отмеченные к ..." действия, а затем вы удалите эту временную папку вручную. .. only:: edition_pe Если вы пытаетесь удалить *Iphoto* фотографии, то причина сбоя иная.Удаление не выполняется, так dupeGuru не может общаться с Iphoto. Учтите, что для удаления корректной работы, вы не должны играть вокруг Iphoto в то время как dupeGuru работает. Кроме того, иногда, система Applescript, кажется, не знают, где найти Iphoto запустить его. Это может помочь в таких случаях для запуска Iphoto *до* вы посылаете дубликатов в корзину. Если все это не так, `контакт с поддержки HS `_, мы поможем Вас. .. todo:: This FAQ qestion is outdated, see english version. dupeguru-4.3.1/help/ru/folders.rst000066400000000000000000000064551426171743600171520ustar00rootroot00000000000000Выбор папки ================ Первое окно, вы видите, когда вы запускаете dupeGuru это окно выбора папки. Это окно содержит список папок, которые будут сканироваться при нажатии на **Scan**. Это окно довольно проста в использовании. Если вы хотите добавить папку, нажмите на кнопку **+**. Если вы добавили папки прежде, всплывающее меню со списком последних папки добавил появится. Вы можете нажать на одну из них, чтобы добавить его прямо в свой список. Если нажать на первый пункт всплывающего меню, **Добавить новую папку ...**, вам будет предложено ввести папку добавить. Если вы никогда не добавляется папка, не появится меню, и вы будете непосредственно будет предложено ввести новую папку добавить. Альтернативный способ для добавления папок в список, чтобы перетащить их в списке. Чтобы удалить папку, выберите папку, удалить, и нажмите на **-**. Если папке выбирается при нажатии кнопки, выбранной папки будет установлен в ** ** исключены состояния (см. ниже), а не удален. Папка государств ---------------- Каждая папка может находиться в одном из этих 3-х государств: * **Нормальный:** дубликаты найдены в эту папку можно удалить. * **Справка:** Дубликаты найти в этой папке не может **быть удалены** . Файлы из этой папки можно только в конечном итоге в **ссылка** позиция в группе обмануть. Если более чем один файл из папки ссылку в конечном итоге в той же группе обмануть, только один, будут сохранены.Другие будут удалены из группы. * **Не включено:** Файлы в этом каталоге не будет включен в проверку. Состояние по умолчанию к папке, конечно, **Нормальный**. Вы можете использовать **Ссылка** состояние для папки, если вы хотите быть уверены, что вы не будете удалять любые файлы из него. Когда вы устанавливаете состояние каталог, все подпапки этой папки автоматически наследует это состояние, если явно не включенное состояние подпапку в. .. todo:: Add iPhoto/Aperture/iTunes libraries notes dupeguru-4.3.1/help/ru/index.rst000066400000000000000000000041101426171743600166050ustar00rootroot00000000000000dupeGuru help =============== Этот документ также доступна на `французском `__, `немецком `__ и `армянский `__. .. only:: edition_se or edition_me dupeGuru есть инструмент для поиска дубликатов файлов на вашем компьютере. Он может сканировать либо имен файлов или содержимого.Имя файла функций сканирования нечеткого соответствия алгоритма, который позволяет найти одинаковые имена файлов, даже если они не совсем то же самое. .. only:: edition_pe dupeGuru Picture Edition (PE для краткости) представляет собой инструмент для поиска дубликатов фотографий на вашем компьютере. Не только он может найти точные соответствия, но он также может найти дубликаты среди фотографий разного рода (PNG, JPG, GIF и т.д..) И качество. Хотя dupeGuru может быть легко использована без документации, чтение этого файла поможет вам освоить его. Если вы ищете руководство для вашей первой дублировать сканирования, вы можете взглянуть на раздел :doc:`Быстрый ` Начало. Это хорошая идея, чтобы сохранить dupeGuru обновлен. Вы можете скачать последнюю версию на своей http://dupeguru.voltaicideas.net. Содержание: .. toctree:: :maxdepth: 2 quick_start folders preferences results reprioritize faq changelog dupeguru-4.3.1/help/ru/preferences.rst000066400000000000000000000375041426171743600200140ustar00rootroot00000000000000Предпочтения ============= .. only:: edition_se **Тип сканирования:** Этот параметр определяет, какой аспект файлы будут сравниваться в дубликат сканирования. Если выбрать **Имя файла**, dupeGuru будем сравнивать каждое имена файлов слово за слово, и, в зависимости от других параметров ниже, он будет определять, достаточно ли слов соответствие рассмотреть 2 файлов дубликатов. Если выбрать **Содержимое**, только файлы с точно такой же контент будет матч. **Папки** типа сканирования немного особенным. Когда вы выбираете его, dupeGuru проведет поиск дубликатов *папки* вместо того, чтобы дубликатов файлов. Для определения того, две папки, дублируют друг друга, все файлы, содержащиеся в папках будут проверяться, и если содержание **все** файлы в матче папки, папки будут считаться дубликатами. **Фильтра Твердость:** Если вы выбрали **Имя файла** типа сканирования, эта опция определяет, как похожи два имени должно быть для dupeGuru рассматривать их дубликатов. Если фильтр твердости, например 80, то это означает, что 80% слов из двух имен файлов должны совпадать. Для определения соответствия процент, dupeGuru первой подсчитывает общее количество слов в **обоих** файла, то подсчитать количество слов соответствия (каждое слово соответствия считаются 2), а затем разделите количество слов соответствия на общее число слов. Если результат больше или равно фильтр твердость, у нас есть дубликаты матча. Например, "ABCD" и "CDE" имеют соответствующий процент 57 (4 слова соответствия, 7 всего слов). .. only:: edition_me **Тип сканирования:** Этот параметр определяет, какой аспект файлы будут сравниваться в дубликат сканирования.Характер дублировать сканирования варьируется в зависимости от того, что вы выбираете для этой опции. * **Имя файла:** Каждая песня будет иметь свой файл разбит на слова, а затем каждое слово будет по сравнению с вычислить соответствующие проценты. Если этот процент выше или равна **жесткость фильтра** (см. ниже подробнее), dupeGuru рассмотрит 2 песни дубликатов. * **Имя файла - Поля: **Как**Имя файла**, за исключением того, что как только имя файла были разделены на слова, эти слова затем группируются в поля.Разделитель полей "-".Окончательный процент соответствия будет самым низким соответствующий процент среди полей. Таким образом, "Исполнитель - Название" и "Артист - Другие Название" будет иметь соответствующий процент 50 (С **Имя файла** сканирования, это будет 75). * **Имя файла - Поля (нет приказа):**Как**Имя файла-Поля**, кроме того, что порядок полей не имеет значения. Например, "Исполнитель - Название" и "Название - Артист" будет иметь соответствующий процент из 100 вместо 0. * **Теги:** Этот метод считывает метки (метаданные) каждой песни и сравнить их полям. Этот метод, как Супер **- Поля**, считает низкий соответствующее поле в качестве окончательного соответствующий процент. * **Состав:** Этот метод сканирования использовать фактическое содержание песни, чтобы определить, какие являются дубликатами. За 2 песни в соответствии с этим методом, они должны иметь точно **такой же содержания**. * **Аудио контента:** То же содержание, но только в аудио-контент сравнивается (без метаданных). **Фильтра Твердость:** Если вы выбрали имя файла или тегами типа сканирования, эта опция определяет, как похожи два имени / теги должны быть для dupeGuru рассматривать их дубликатов. Если фильтр твердости, например 80, то это означает, что 80% слов из двух имен файлов должны совпадать. Для определения соответствия процент, dupeGuru первой подсчитывает общее количество слов в **обоих** файла, то подсчитать количество слов соответствия (каждое слово соответствия считаются 2), а затем разделите количество слов соответствия на общее число слов. Если результат больше или равно фильтр твердость, у нас есть дубликаты матча. Например, "ABCD" и "CDE" имеют соответствующий процент 57 (4 слова соответствия, 7 всего слов). **Теги для сканирования:** При использовании **Тега** Слова типа сканирования, вы можете выбрать теги, которые будут использоваться для сравнения. .. only:: edition_se or edition_me **Слово взвешивания:** Если вы выбрали **Имя файла** типа сканирования, этот вариант несколько изменений, как соответствующий процент рассчитывается. При слове взвешивания, вместо того, значение 1 в дубликат счета и общее количество слов, каждое слово имеет значение, равное количество символов, которые они имеют. При слове взвешивание, "AB CDE FGHI" и "AB CDE fghij" будет иметь соответствующий процент 53% (19 Персонажей, 10 символов, соответствующая (4 для "б" и 6 "CDE")). **Совпадения похожих слов:** Если вы включите эту опцию, подобные слова будут засчитаны как спички. Например, "Белая полоса" будет совпадать% из 100 вместо 66 с, что функция включена. **Внимание:** Используйте эту опцию с осторожностью. Вполне вероятно, что вы получите много ложных срабатываний в результатах при его включении. Тем не менее, это поможет вам найти дубликаты, что вы не нашли бы в противном случае.Процесс сканирования также значительно медленнее, эта опция включена. .. only:: edition_pe **Тип сканирования:** Этот параметр определяет тип сканирования, которые будут сделаны на ваши картины.**Сканирования** Содержание типа сравнивает фактическое содержание фотографий нечеткие пути (что делает его можно найти не только точными копиями, но и подобные).**EXIF Timestamp** тип сканирования смотрит на метаданные EXIF с фото (если он существует) и соответствует фотографии, которые имеют такой же. Это намного быстрее, чем сканирование содержимого. **Внимание:** Измененные фотографии часто держат же метка EXIF, так что следите за ложных срабатываний, когда вы используете, что тип сканирования. **Фильтра Твердость:** *Содержание тип сканирования только.* Чем больше этот параметр, "тяжелее" является фильтром (Другими словами, тем меньше результатов Вы получите). Большинство фотографий одного и того же матча качества на 100%, даже если формат отличается (PNG и JPG, например.). Однако, если вы хотите, чтобы соответствовать PNG с более низким качеством JPG, вам придется установить фильтром твердость ниже, чем 100.По умолчанию, 95, это сладкое место. **Совпадения рисунки разных размеров:** Если вы установите этот флажок, фотографии разных размеров будет разрешен в том же дубликат группы. **Можно смешивать файл вида:** Если вы установите этот флажок, дублировать группам разрешается есть файлы с различными расширениями. Если вы не проверить его, ну, они не являются! **Игнорировать дубликаты hardlinking в тот же файл:** Если эта опция включена, dupeGuru проверит дубликаты, чтобы увидеть если они ссылаются на тот же индексный `дескриптор `_. Если они это сделают, они не будут считаться дубликатами. (Только для OS X и Linux) **Использование регулярных выражений при фильтрации:** Если вы отметите этот флажок, фильтрация будет рассматривать ваш запрос фильтра, как **регулярное выражение**. Объясняя их выходит за рамки этого документа.Хорошее место для начала обучения он `regular-expressions.info `_. **Удаление пустых папок после удаления или перемещения:** Когда эта опция включена, папки будут удалены через файл удален или перемещен и папка пуста. **Копирование и перемещение:** Определяет, как операции копирования и перемещения (в меню Действия) будет себя вести. * **Право на назначение:** Все файлы будут отправлены непосредственно в пункт назначения, не пытаясь воссоздать исходный путь вообще. * **Повторно относительный путь:** путь исходный файл будет воссоздан в папке назначения, вплоть до корневого выделение в панели директорей. Например, если вы добавили ``/Users/foobar/SomeFolder`` на панель Каталоги и перемещении ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` до места назначения ``/Users/foobar/MyDestination``, конечным пунктом назначения для файла будет ``/Users/foobar/MyDestination/SubFolder`` (``SomeFolder`` были сокращены с пути источника в конечный пункт назначения.). * **Повторно абсолютный путь:** путь исходный файл будет воссоздан в папке назначения в полном комплекте. Например, если вы перемещаете ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` до места назначения ``/Users/foobar/MyDestination``, конечным пунктом назначения для файла будет ``/Users/foobar/MyDestination/Users/foobar/SomeFolder/SubFolder``. Во всех случаях, dupeGuru красиво ручки конфликтов имен путем добавления номера назначения имя файла, если имя файла уже существует в месте назначения. **Специальной команды:** Это предпочтение определяет команду, которая будет вызываться "Вызов специальной команды" действия. Вы можете ссылаться ни на какие внешние приложения через это действие. Это может быть полезно, если, например, у вас есть хорошее приложение сравниваете установлены. Формат команды такой же, как то, что вы должны написать в командной строке, за исключением того, что Есть 2 заполнителей: **%d** and **%r**. Эти заполнители будут заменены на путь выбран обманут (%d) и путь к ссылке на файл выбранного обмануть (%r). Если путь к исполняемому содержит пробелы, необходимо заключить его в "" кавычки. Вы также должны приложить заполнителей в кавычки, потому что это очень возможно, что путь к обманутых и ссылки будут содержать пробелы. Вот пример пользовательской команды: "C:\Program Files\SuperDiffProg\SuperDiffProg.exe" "%d" "%r" dupeguru-4.3.1/help/ru/quick_start.rst000066400000000000000000000033651426171743600200420ustar00rootroot00000000000000Быстрый старт ============= Чтобы вы быстро начали с dupeGuru, давайте просто делать сканирование с помощью стандартных настроек по умолчанию. * Запуск dupeGuru. * Добавление папок для сканирования либо перетащить & капли или кнопку "+". * Нажмите на **сканирование**. * Подождите, пока процесс сканирования завершен. * Посмотрите на каждый дубликат (файлы, которые отступом) и убедитесь, что это действительно дубликат ссылкой группы (файл выше дублировать без отступа и инвалидов окна знак). * Если файл ложных дубликатов, выделите ее и нажмите **Действия -> Удалить выбранные из результатов**. * Если вы уверены, что нет ложных дубликатов в результатах, нажмите на **Изменить -> Отметить Все**, а затем **Действия -> Отправить Помечено в Корзину**. Это только основные сканирования. Есть много настройки вы можете сделать, чтобы получить разные результаты и несколько методов изучения и изменения ваших результатов. Чтобы узнать о них, только что прочитал остальную часть этого файла справки.dupeguru-4.3.1/help/ru/reprioritize.rst000066400000000000000000000062071426171743600202360ustar00rootroot00000000000000Повторное приоритетов дубликатов ================================ dupeGuru пытается автоматически определить, какие дубликат должен отправиться в ссылку каждой группы позиции, но иногда это делается неправильно. Во многих случаях, умный обмануть сортировки с "Ценности Дельта" и "обманутые Только" варианты в дополнение к "Сделать выбранной ссылки" действие делает трюк, но иногда, более мощный вариант не требуется. Здесь изменения приоритетов в диалог вступает в играть. Вы можете вызвать его через "изменить приоритеты Результаты" пункт в меню "Действия". Этот диалог позволяет вам выбрать критерии, по которым ссылка обмануть будут отобраны в каждой группе обмануть.Список доступных критериев слева и перечень критериев вы Выбранная справа. Критериев категории следуют аргумент. Например, "Размер (Высший)" означает, что обмануть с крупным размером победит. "Свойства папки (/ Foo / Bar)" означает, что обманутые в этой папке будет победить. Для добавления критерий правом списке, сначала выберите категорию в выпадающем списке, затем выберите subargument в приведенном ниже списке, а затем нажмите на правую стрелку кнопки. Порядок списка справа важно (вы можете изменить порядок элементов через перетащить и отпустить). когда сбор обмануть для справки позицию, первый критерий используется. Если есть галстук, второй критерий используется и так далее и так далее. Например, если ваши аргументы "Размер (высший)", а затем "Имя файла (Не оканчивается на номер)", ссылке на файл, который будет выбран в группе будет крупнейших файл, а если два или несколько файлов имеют одинаковый размер, который имеет имя файла с не заканчивается номер будет использоваться. Когда все критерии привести к связи, порядок, в котором обманутые ранее были в группе будет использоваться.dupeguru-4.3.1/help/ru/results.rst000066400000000000000000000447311426171743600172140ustar00rootroot00000000000000Результаты ========== Когда dupeGuru завершения сканирования на наличие дубликатов, он покажет его результаты в виде дубликата список группы. О дубликат группы ---------------------- Дубликат группа представляет собой группу файлов, которые весь матч вместе. Каждая группа имеет **ссылке** на файл и одного или более **одинаковых файлов**. Ссылки файл первый файл группы. Его марка окно отключено. Под ним, и с отступом, которые дубликатов файлов. Вы можете отметить дубликатов файлов, но вы никогда не можете пометить ссылки файл группы. Это мера безопасности, чтобы предотвратить dupeGuru от удаления не только повторяющиеся файлы, но их ссылки. Ты уверен, что не хочу этого, не так ли? Что определяет, какие файлы ссылки и какие файлы являются дубликатами сначала свою папку государства. Файл с ссылкой папка всегда будет ссылка в дубликат группы. Если все файлы из обычной папки, размер определить, какой файл будет ведения дубликат группы. dupeGuru предполагает, что вы всегда хотите сохранить крупнейших файл, так что крупных файлов займет исходное положение. Вы можете изменить ссылку файл группы вручную. Для этого выберите дубликат файла, который вы хотите продвигать на ссылку, и нажмите на кнопку **Действия -> Добавить выбранной ссылки**. Просмотр результатов -------------------- Хотя вы можете просто нажать на **Правка -> Выделить все, а затем** **Действия -> Отправить Помечено в Корзину** быстро удалить все дубликаты файлов в результатах, всегда рекомендуется пересмотреть все дубликаты перед удаляя их. Чтобы помочь вам обзор результатов, вы можете вызвать панель **Подробнее**. Эта панель показывает все детали выбранного файла, а также подробности своей ссылки в. Это очень удобно, чтобы быстро определить, если дубликат действительно дубликат. Вы также можете дважды щелкнуть по файлу, чтобы открыть его и связанные с ним приложения. Если у вас есть больше ложных дубликатов, чем правда дубликатов (Если Ваш фильтр жесткость очень низкая), лучший способ продолжить бы пересмотреть дубликатов, знак истинного дубликаты и нажмите **Действия -> Отправить Помечено в Корзину** . Если у вас есть более верно, чем ложных дубликатов дубликатов, вместо этого можно пометить все файлы, которые являются ложными дубликатов, а также использовать **Действия -> Удалить Помеченные от результатов**. Маркировка и выбор --------------------- **Отмеченные** дубликат двух экземплярах с небольшой флажок рядом с ним, имеющие галочки. **Выбран дубликат дубликата** быть выделены. Несколько действий, выбор может быть выполнена в dupeGuru стандартным образом (Shift / Command / Control клик). Вы можете переключать знак состояние всех выбранных дубликаты ", нажав **пространстве**. Показать только обманутые ------------------------- Когда этот режим включен, дубликаты отображаются без их соответствующего файла справки. Вы можете выбрать, марка и сортировать этот список, как и в обычном режиме. DupeGuru результаты, когда в нормальном режиме, сортируются в соответствии с дубликат группы '**ссылке на файл**. Это означает, что если вы хотите, например, чтобы отметить все дубликаты "EXE" расширением, вы не можете просто сортировать результаты по "Вид", чтобы иметь все EXE дубликатов вместе, потому что группа может состоять из более чем одного типа файлов . Вот где обманутые Только режим вступает в игру. Чтобы отметить все ваши "EXE" дубликаты, вы просто должны: * Включить обманутые Только режим. * Добавить "Вид" колонку "Столбцы" меню. * Нажмите на том, что "Вид" колонки, чтобы отсортировать список по типу. * Найдите первый дубликат с "EXE" рода. * Выберите его. * Прокрутите список, чтобы найти последнего дубликата с "EXE" рода. * Удерживайте Shift и щелкните по нему. * Нажмите Space, чтобы пометить все выбранные дубликатов. Дельта значения --------------- Если включить этот переключатель на некоторые столбцы будут отображать значение по отношению к дубликата ссылке, а не абсолютные значения. Эти дельты значения также будут отображаться в разные цвета, чтобы вы могли заметить их легко. Например, если дубликат 1,2 Мб и свою ссылку в 1,4 Мб, размер столбец отображает -0,2 Мб. Только обманутые и Дельта значения ---------------------------------- Только обманутые режиме раскрыть свою истинную силу, когда вы используете его с Делта Значения переключатель включен. Когда вы включите его, относительные значения будет отображаться вместо абсолютных. Так что если, например, вы хотите удалить из результатов все дубликаты, которые являются более 300 Кб от их ссылке, вы можете отсортировать дубликаты только результаты по размеру, выберите все дубликаты при -300 в столбце Размер, удалять их, , а затем сделать то же самое повторяет более 300 в нижней части списка. Вы можете также использовать его для изменения ссылки приоритет повторяющиеся список. Когда вы делаете свежие сканирования, если Есть нет ссылки папки, ссылке на файл каждой группы является самой большой файл. Если вы хотите изменить, что, например, в последней модификации время, вы можете отсортировать дубликаты только результаты по времени модификации в **убывания** порядке выберите все дубликаты со временем изменения дельты значение больше 0 и нажмите **Убедитесь, выбранной ссылки**. Причина, почему вы должны сделать порядок сортировки по убыванию, потому что если 2 файла среди таких же дубликат группы выбираются при нажатии на **Сделать выбранной ссылки**, только первый из списка будут сделаны ссылки, другие будут проигнорированы . И так как вы хотите Последнее изменение файла для ссылки, имеющие порядок сортировки по убыванию уверяет вас, что первым пунктом в списке будет последнего изменения. .. todo:: Add "Non-numerical delta" information. Фильтрация ---------- dupeGuru поддерживает после сканирования, фильтрации. С его помощью вы можете сузить результаты, чтобы вы могли выполнять действия, на подмножества. Например, вы можете легко пометить все дубликаты с их имя файла, содержащего "копировать" из результатов с помощью фильтра. .. todo:: Qt has a toolbar search field now, not a menu item. **Windows:** Для использования функции фильтрации, нажмите на Действия -> Применить фильтр, запишите фильтр, который вы хотите применить и нажмите ОК. Чтобы вернуться к нефильтрованное результаты, нажмите на Действия -> Отменить фильтр. **Mac OS X:** Для использования функции фильтрации, тип фильтра в "Фильтр" поле поиска на панели инструментов. Чтобы вернуться к нефильтрованное результате, очистите поле, или нажмите на кнопку "X". В простом режиме (режим по умолчанию), что вы вводите в качестве фильтра строку, используемую для выполнения фактической фильтрации, за исключением одной маски: **\***. Таким образом, если вы введете "[*]" как ваш фильтр, он будет соответствовать что-нибудь с [] скобках в нем, все, что между этими скобками. Для более продвинутых фильтров, вы можете включить «Использование регулярных выражений при фильтрации" на. Функция фильтрации будет использовать регулярные выражения. Регулярное выражение языка для согласования текста. Объясняя их выходит за рамки этого документа. Хорошее место для начала обучения он `regular_expressions.info` _. Матчи не чувствительны к регистру, в простых и регулярных выражений режиме. Для фильтра, чтобы соответствовать, регулярное выражение не обязательно должно совпадать целый файл, он просто обязан содержать в цепочку, соответствующую выражению. Вы могли заметить, что не все дубликаты в результате будут соответствовать вашим фильтром. Это потому, что как только одна копия в матчах группового фильтра, то вся группа останется в результатах, таким образом Вы можете иметь более четкое представление о дубликата контексте. Тем не менее, не соответствующие дубликаты в "ссылку режиме". Таким образом, можно выполнять действия, как Марк все и обязательно только знак фильтруется дубликатов. Действие меню ------------- * **Открытый черный список:** Удалите все игнорируют матчи вы добавили. Вы должны начать новый поиск вновь очищается список игнорируемых чтобы быть эффективными. * **Экспорт результатов в XHTML:** Возьмите текущие результаты, а также создавать файл XHTML из него. Столбцов, которые видны при нажатии на эту кнопку будет столбцов в файле XHTML. Файл автоматически откроется в браузере по умолчанию. * **Отправить Помечено в корзину:** Отправить все отмеченные дубликаты, мусор, это очевидно. * **Удалить Помеченные и замена с Жесткие**: Передает все отмеченные дубликаты, мусор, но после того, как сделали это, удаленные файлы заменяются `жестких `_ ссылку к ссылке на файл. (Только для OS X и Linux) * **Перемещение Помечено в ...:** запросит назначения, а затем переместить все отмеченные файлы в том, что назначения. Путь исходного файла может быть воссоздан в пункт назначения, в зависимости от "Копирование и перемещение" предпочтения. * **Скопируйте Помечено в ...:** запросит у вас место, а затем скопировать все выбранные файлы к этому пункту назначения. Путь исходного файла может быть воссоздан в пункт назначения, в зависимости от "Копирование и перемещение" предпочтения. * **Удалить Помеченные из результатов:** Удалить все отмеченные дубликатов из результата поиска. Сами файлы не будут затронуты и останутся, где они. * **Удалить выбранные из результатов:** Удалить все выбранные дубликатов из результата поиска. Обратите внимание, что все выбранные файлы ссылки будут игнорироваться, только дубликаты могут быть удалены с этим действием. * **Сделать Выбранный Справка:** Содействие все выбранные дубликатов ссылки. Если дубликат частью группы, имеющей ссылке на файл ближайшие из ссылки папки (в синий цвет), не будут приняты меры для этого дубликат. Если более чем один дубликат среди той же группы выбраны, только первый из каждой группы будет поощряться. * **Добавить выбранные в черный список:** Это сначала удаляет все выбранные дубликаты из результатов, а затем добавить матча, которые дублируют и опорный ток в черный список. Этот матч не придет снова в дальнейшей проверки. Копировать себя и, возможно, вернется, но он будет искаться в другой ссылке на файл. Вы можете очистить список игнорируемых с Открытый черный список команды. * **Открытое Выбранный с приложений по умолчанию:** Откройте файл с помощью приложения, связанного с типом выбранного файла. * **Показать Выбранный в Finder-е:** Откройте папку, содержащую выбранный файл. * **Вызов специальной команды:** Вызывает внешнюю программу вы установили в настройках с использованием выделенного фрагмента в качестве аргументов в вызове. * **Переименования выбрано:** Запрашивает новое имя, а затем переименовать выбранный файл. .. todo:: Add Move and iPhoto/iTunes warning .. todo:: Add "Deletion Options" section.dupeguru-4.3.1/help/uk/000077500000000000000000000000001426171743600147415ustar00rootroot00000000000000dupeguru-4.3.1/help/uk/faq.rst000066400000000000000000000342571426171743600162550ustar00rootroot00000000000000Часті питання ========================== .. topic:: Що таке dupeGuru? .. only:: edition_se dupeGuru це інструмент для пошуку дублікатів файлів на вашому комп'ютері. Він може сканувати або імен файлів або контенту. Файл функцій сканування нечіткого відповідності алгоритму, який дозволяє знайти однакові імена файлів, навіть якщо вони не зовсім те ж саме. .. only:: edition_me dupeGuru Music Edition являє собою інструмент для пошуку дубльованих пісень у вашій музичній колекції. Він може будувати свою сканування файлів, тегам або змісту. Файл і тег перевіряє функція нечіткого відповідності алгоритм, який може знаходити дублікати файлів або теги, навіть якщо вони не зовсім те ж саме. .. only:: edition_pe dupeGuru Picture Edition (PE для стислості) являє собою інструмент для пошуку дублікатів фотографій на вашому комп'ютері. Не тільки він може знайти точні відповідності, але він також може знайти дублікати серед фотографій різного роду (PNG, JPG, GIF і т.д..) І якість. .. topic:: Що робить його краще, ніж інші сканери дублювати? Сканування є надзвичайно гнучкою. Ви можете налаштувати його, щоб дійсно отримати, яких результатів ви хочете. Ви можете прочитати більше про опція налаштування dupeGuru в :doc:`Установки `. .. topic:: Наскільки безпечно використовувати dupeGuru? Дуже безпечною. dupeGuru був розроблений, щоб переконатися, що ви не видаляєте файли, які ви не хотіли видалити. По-перше, існує система відліку папку, яка дозволяє визначити папки, в яких ви абсолютно не ** ** хочете dupeGuru, щоб ви видаляєте файли там, і тоді є система контрольної групи, що гарантує, що ви завжди ** * * тримати принаймні один член групи дублікатів. .. topic:: Які обмеження демо dupeGuru? У демо-режимі, ви можете тільки виконувати дії над 10 дублікати відразу. В `Fairware `_ mode, однак, Є ніяких обмежень. .. topic:: Знак коробку файл я хочу видалити відключена. Що я повинен зробити? Ви не можете помітити посилання (перший файл) дублікат групи. Однак те, що ви можете зробити, полягає в сприянні дублювати файл довідки. Таким чином, якщо файл, який Ви хочете, щоб відзначити цю посилання, виділіть дублікатів файлів в групу, яку ви хочете просувати на посилання, і натисніть на кнопку **Дії -> Додати вибраної посилання**. Якщо посилання файл з папки посилання (назва файлу написані на синіми літерами), ви не можете видалити його з вихідного положення. .. topic:: У мене є папка, з якої я справді не хочу, щоб видалити файли. Якщо ви хочете бути впевнені, що dupeGuru ніколи не буде видаляти файл з певної папки, переконайтеся, що встановили в стан **Посилання на:** документ: :doc:`folders`. .. topic:: Що це за '(X відкидається) "повідомлення в рядку стану? У деяких випадках, кілька матчів не включені в остаточні результати з міркувань безпеки. Дозвольте мені навести приклад. У нас є 3 файли: A, B і C. Ми скануємо їх за допомогою фільтра низьких твердості. Сканер визначає, що матчі з B, матчі з С, але робить B ** не ** матч з С. При цьому, dupeGuru має вигляд проблеми. Вона не може створити дублікат групи А, В і С в це, тому що не всі файли в групі буде відповідати разом. Це може створити 2 групи: одна група AB, а потім одна група AC, але це не буде, з міркувань безпеки. Давайте думати про це: якщо Б не співпадає з С, вона, ймовірно, означає, що або B, C або обидва на самому ділі не дублікати. Якщо не було б 2 групи (АВ і АС), ви б у кінцевому підсумку видалити обидва B і C. І якщо один з них не дублювати, що насправді не те, що ви хочете робити, правильно? Так що dupeGuru робить у такому випадку є, щоб відмінити матч AC (і додає повідомлення в рядку стану). Таким чином, якщо ви вилучили B і повторно запустити сканування, вам доведеться відповідати змінного струму в наступний результат. .. topic:: Я хочу, щоб відзначити всі файли з визначеної папки. Що я можу зробити? Включити :doc:`ошукані Тільки ` режим і натиснути на папку колонки для сортування дублікатів по папках. Потім він буде легким для вас, щоб вибрати всі дублікати з тієї ж папці, а потім натиснути клавішу пробіл, щоб відзначити всі вибрані дублікатів. .. only:: edition_se or edition_pe .. topic:: Я хочу, щоб видалити всі файли, які більше 300 Кб від їх посиланням на файл. Що я можу зробити? * Включити :doc:`ошукані Тільки ` режимі. * Включити **Значення Delta** режимі. * Натисніть на "Розмір" стовпця для сортування результатів за розміром. * Вибрати всі дублікати нижче -300. * Натисніть на **Видалити вибрані з результатів**. * Вибрати всі дублікати більше 300 осіб. * Натисніть на **Видалити вибрані з результатів**. .. topic:: Я хочу, щоб мої останні змінені файли файли довідки. Що я можу зробити? * Включити :doc:`ошукані Тільки ` режимі. * Включити **Значення Delta** режимі. * Натисніть на "Модифікація" колонку для сортування результатів за датою зміни. * Натисніть на "Модифікація" колона знову змінити порядок сортування. * Вибрати всі дублікати за 0. * Натисніть на **Зробити вибраної посилання**. .. topic:: Я хочу, щоб відзначити все дублікати, що містять слово "копія". Як мені це зробити? * **Windows**: Натисніть на **Дії -> Застосувати фільтр**, потім введіть "копія", натисніть кнопку ОК. * **Mac OS X**: Типу "копія" в "Фільтр" поле на панелі інструментів. * Натисніть на Марка **-> Позначити всі**. .. only:: edition_me .. topic:: Я хочу, щоб видалити всі пісні, які більш ніж на 3 секунди від своєї посиланням на файл. Що я можу зробити? * Включити :doc:`ошукані Тільки ` режимі. * Включити **Значення Delta** режимі. * Натисніть на "Час" колонку для сортування результатів за часом. * Вибрати всі дублікати нижче -00:03. * Натисніть на **Видалити вибрані з результатів**. * Вибрати всі дублікати за 00:03. * Натисніть на **Видалити вибрані з результатів**. .. topic:: Я хочу, щоб мій високий бітрейт файлів пісні посилання. Що я можу зробити? * Включити :doc:`ошукані Тільки ` режимі. * Включити **Значення Delta** режимі. * Натисніть на "Бітрейт" колонку для сортування результатів по бітрейт. * Натисніть на "Бітрейт" колона знову змінити порядок сортування. * Вибрати всі дублікати за 0. * Натисніть на **Зробити вибраної посилання**. .. topic:: Я не хочу [жити] і [ремікс] версії моїх пісень вважатися дублікатами. Як мені це зробити? Якщо ваше порівняння поріг досить низьким, ви, ймовірно, в кінцевому підсумку з живою і ремікс версії ваших пісень у своїх результатах. Там ви нічого не можете зробити, щоб запобігти цьому, але є дещо можна зробити, щоб легко видалити їх зі свого результати після сканування: після сканування, фільтрації. Якщо, наприклад, ви хочете видалити всі пісні з чим-небудь у квадратних дужках []: * **Windows**: Натисніть на **Дії -> Застосувати фільтр**, а потім введіть "[*]", натисніть кнопку ОК. * **Mac OS X**: Тип "[*]" в "Фільтр" поле на панелі інструментів. * Натисніть на Марка **-> Позначити всі**. * Натисніть на **Дії -> Видалити вибрані з результатів**. .. topic:: Я намагався відправити свої дублікати в корзину, але dupeGuru говорить мені, він не може це зробити. Чому? Що я можу зробити? Більшу частину часу, тому dupeGuru не можете відправляти файли до кошика через права доступу до файлів. Ви повинні * написати * дозволу на файли, які ви хочете відправити у кошик. Якщо ви не знайомі з командним рядком, ви можете використовувати утиліти, такі як `BatChmod `_ виправити Ваші права. Якщо dupeGuru ще дає вам неприємності після фіксації ваших прав, було кілька випадків, коли за допомогою "Переміщення Позначено до ..." як обхідного шляху зробили свою справу. Таким чином, замість відправки файлів в корзину, ви посилаєте їх в тимчасову папку з "Переміщати Позначено до ..." дії, а потім видалити цю тимчасову папку вручну. .. only:: edition_pe Якщо ви намагаєтеся видалити *iPhoto*, то причина збою інша. Видалення не виконується, так dupeGuru не може спілкуватися з iPhoto. Врахуйте, що для видалення коректної роботи, ви не повинні грати навколо iPhoto в той час як dupeGuru працює. Крім того, іноді, система Applescript, здається, не знають, де знайти Iphoto запустити його. Це може допомогти в таких випадках для запуску Iphoto * до * ви посилаєте дублікатів в корзину. Якщо все це не так, `контакт УГ підтримки `_, ми зрозуміти це. .. todo:: This FAQ qestion is outdated, see english version. dupeguru-4.3.1/help/uk/folders.rst000066400000000000000000000063231426171743600171350ustar00rootroot00000000000000Вибір папки ================ Перше вікно, ви бачите, коли ви запускаєте dupeGuru це вікно вибору папки. Це вікно містить список папок, які будуть скануватися при натисканні на **Сканування**.Це вікно досить проста у використанні. Якщо ви хочете додати папку, натисніть на кнопку **+**. Якщо ви додали папки перш, спливаюче меню зі списком останніх папки додав з'явиться. Ви можете натиснути на одну з них, щоб додати його прямо в свій список. Якщо натиснути на перший пункт меню, **Додати новий папку ...**, вам буде запропоновано ввести папку додати. Якщо ви ніколи не додається папка, не з'явиться меню, і ви будете безпосередньо буде запропоновано ввести нову папку додати. Альтернативний спосіб для додавання папок в список, щоб перетягнути їх в списку. Щоб видалити папку, виберіть папку, видалити, і натисніть на **-**. Якщо папці вибирається при натисканні кнопки, обраної папки буде встановлений в **виключені** стану (див. нижче), а не видалений. Папка держав ------------- Кожна папка може знаходитися в одному з цих 3-х держав: * ** Нормальний: ** дублікати знайдені в цю папку можна видалити. * ** Довідка: ** Дублікати знайти в цій папці **не може** бути видалені. Файли з цієї папки можна тільки в кінцевому підсумку в **посилання** позиція в групі обдурити. Якщо більш ніж один файл з папки посилання в кінцевому підсумку в тій же групі обдурити, тільки один, будуть збережені. Інші будуть видалені з групи. * ** Не включено: ** Файли в цьому каталозі не буде включений у перевірку. Стан за замовчуванням до папки, звичайно, **Нормальний**. Ви можете використовувати **Посилання** стан для папки, якщо ви хочете бути впевнені, що ви не будете видаляти будь-які файли з нього. Коли ви встановлюєте стан каталог, все підпапки цієї папки автоматично успадковує цей стан, якщо явно не включений стан підпапку в. .. todo:: Add iPhoto/Aperture/iTunes libraries notes dupeguru-4.3.1/help/uk/index.rst000066400000000000000000000040111426171743600165760ustar00rootroot00000000000000dupeGuru help =============== .. only:: edition_se Цей документ також доступна на `французькому `__, `німецький `__ і `Вірменський `__. .. only:: edition_se or edition_me dupeGuru це інструмент для пошуку дублікатів файлів на вашому комп'ютері. Він може сканувати або імен файлів або вмісту. Файл функцій сканування нечіткого відповідності алгоритму, який дозволяє знайти однакові імена файлів, навіть якщо вони не зовсім те ж саме. .. only:: edition_pe dupeGuru Picture Edition (PE для стислості) являє собою інструмент для пошуку дублікатів фотографій на вашому комп'ютері. Не тільки він може знайти точні відповідності, але він також може знайти дублікати серед фотографій різного роду (PNG, JPG, GIF і т.д..) І якість. Хоча dupeGuru може бути легко використана без документації, читання цього файлу допоможе вам освоїти його. Якщо ви шукаєте керівництво для вашої першої дублювати сканування, ви можете поглянути на: :doc:`Quick Start ` Це гарна ідея, щоб зберегти dupeGuru оновлено. Ви можете завантажити останню версію на своєму http://dupeguru.voltaicideas.net. Contents: .. toctree:: :maxdepth: 2 quick_start folders preferences results reprioritize faq changelog dupeguru-4.3.1/help/uk/preferences.rst000066400000000000000000000362401426171743600200010ustar00rootroot00000000000000Уподобання =========== .. only:: edition_se **Тип сканування:** Цей параметр визначає, який аспект файли будуть порівнюватися в дублікат сканування. Якщо вибрати **Файл** , dupeGuru будемо порівнювати кожне імена файлів слово за слово, і, залежно від інших параметрів нижче, він буде визначати, чи достатньо слів відповідність розглянути 2 файлів дублікатів. Якщо вибрати **Вміст**, тільки файли з точно такою ж контент буде матч. **Папки** типу сканування трохи особливим. Коли ви обираєте його, dupeGuru проведе пошук дублікатів *папки* замість того, щоб дублікатів файлів. Для визначення того, дві папки, дублюють один одного, всі файли, що містяться в папках будуть перевірятися, і якщо вміст **всі** файли в матчі папки, папки будуть вважатися дублікатами. **Фільтра Твердість:** Якщо ви вибрали **Папки** Файл типу сканування, ця опція визначає, як схожі два імені повинно бути для dupeGuru розглядати їх дублікатів. Якщо фільтр твердості, наприклад 80, то це означає, що 80% слів з ​​двох імен файлів повинні збігатися. Для визначення відповідності відсоток, dupeGuru перший підраховує загальну кількість слів в **обох** файлу, то підрахувати кількість слів відповідності (кожне слово відповідності вважаються 2), а потім розділіть кількість слів відповідності на загальне число слів. Якщо результат більше або дорівнює фільтр твердість, у нас є дублікати матчу. Наприклад, "ABCD" і "CDE" мають відповідний відсоток 57 (4 слова відповідності, 7 всього слів). .. only:: edition_me **Тип сканування:** Цей параметр визначає, який аспект файли будуть порівнюватися в дублікат сканування. Характер дублювати сканування варіюється в залежності від того, що ви обираєте для цієї опції. * **Файл:** Кожна пісня буде мати свій файл розбитий на слова, а потім кожне слово буде в порівнянні з обчислити відповідні відсотки. Якщо цей відсоток вище або дорівнює **жорсткість фільтра** (див. нижче детальніше), dupeGuru розгляне 2 пісні дублікатів. * **Файл - Поля:** Як **Файл** , за винятком того, що як тільки ім'я файлу були розділені на слова, ці слова потім групуються в поля. Роздільник полів "-". Остаточний відсоток відповідності буде найнижчим відповідний відсоток серед полів. Таким чином, "Виконавець - Назва" і "Артист - Інші Назва" матиме відповідний відсоток 50 (С **Файл** сканування, це буде 75). * **Файл - Поля (нема наказу):** Як **Супер - Поля**, крім того, що порядок полів не має значення. Наприклад, "Виконавець - Назва" і "Назва - Артист" матиме відповідний відсоток з 100 замість 0. * **Теги:** Цей метод прочитує мітки (метадані) кожної пісні й порівняти їх полям. Цей метод, як **Супер - Поля**, вважає низький відповідне поле в якості остаточного відповідний відсоток. * **Склад:** Цей метод сканування використовувати фактичний зміст пісні, щоб визначити, які є дублікатами. За 2 пісні у відповідності з цим методом, вони повинні мати **такий самий змісту**. * **Аудіо контенту:** Те ж зміст, але тільки в аудіо-контент порівнюється (без метаданих). **Фільтра Твердість:** Якщо ви вибрали ім'я файлу або тегами типу сканування, ця опція визначає, як схожі два імені / теги повинні бути для dupeGuru розглядати їх дублікатів. Якщо фільтр твердості, наприклад 80, то це означає, що 80% слів з двох імен файлів повинні збігатися. Для визначення відповідності відсоток, dupeGuru перший підраховує загальну кількість слів в **обох** файлу, то підрахувати кількість слів відповідності (кожне слово відповідності вважаються 2), а потім розділіть кількість слів відповідності на загальне число слів. Якщо результат більше або дорівнює фільтр твердість, у нас є дублікати матчу. Наприклад, "ABCD" і "CDE" мають відповідний відсоток 57 (4 слова відповідності, 7 всього слів). **Теги для сканування:** При використанні Слова типу сканування, ви можете вибрати теги, які будуть використовуватися для порівняння. .. only:: edition_se or edition_me **Слово зважування:** ЯКЩО ви вибрать Файл типу сканування, цею ВАРІАНТ Трохи змін, Як відповідній відсоток розраховується. При Слові зважування, Замість того, значення 1 в Дублікат Рахунка и загальна кількість слів, кожне слово має значення, рівну кількість сімволів, які смороду мають. При Слові зважування ", AB CDE FGHI" і "AB CDE fghij" матіме відповідній відсоток 53% (19 Персонажів, 10 сімволів, Що відповідає (4 для "б" і 6 "CDE")). **Матч Схожі слова:** ЯКЩО ви дозволите Цю опцію, подібні слова будуть зараховані Як сірники. Наприклад, "White Stripes" і "Біла смуга" буде збігатіся% з 100 Замість 66 з, Що функція включена. **Увага:** використову Цю опцію з обережністю. ЦІЛКОМ імовірно, Що ви отрімаєте Багато помилковості спрацьовувань в результатах при йо включенні. Тім не менше, Це Допоможи вам знайте дублікаті, Що ви НЕ знайшлі б в іншому випадка. Процес сканування кож однозначно повільніше, ця опція включена. .. only:: edition_pe **Тип сканування:** Цей параметр визначає тип сканування, які будуть зроблені на ваші картини. **Сканування** Зміст типу порівнює фактичний зміст фотографій нечіткі шляху (що робить його можна знайти не тільки точними копіями, але і подібні). **EXIF Timestamp** тип сканування дивиться на метадані EXIF з фото (якщо він існує) і відповідає фотографії, які мають такий же. Це набагато швидше, ніж сканування вмісту. **Увага:** Змінені фотографії часто тримають ж мітка EXIF, так що слідкуйте за помилкових спрацьовувань, коли ви використовуєте, що тип сканування. **Фільтра Твердість:** *Вміст тип сканування тільки*. Чим більше цей параметр, "важче" є фільтром (Іншими словами, тим менше результатів Ви отримаєте). Більшість фотографій одного й того ж матчу якості на 100%, навіть якщо формат відрізняється (PNG і JPG, наприклад.). Однак, якщо ви хочете, щоб відповідати PNG з більш низькою якістю JPG, вам доведеться встановити фільтром твердість нижче, ніж 100. За замовчуванням, 95, це солодке місце. **Матч малюнки різних розмірів:** Якщо ви встановите цей прапорець, фотографії різних розмірів буде дозволений в тому ж дублікат групи. **Можна змішувати файл виду:** Якщо ви встановите цей прапорець, дублювати групам дозволяється є файли з різними розширеннями. Якщо ви не перевірити його, ну, вони не є! **Ігнорувати дублікати hardlinking в той же файл:** Якщо ця опція включена, dupeGuru перевірить дублікати, щоб побачити якщо вони посилаються на той самий індексний `дескриптор `__. Якщо вони це зроблять, вони не будуть вважатися дублікатами. (Тільки для OS X і Linux) **Використання регулярних виразів при фільтрації:** Якщо ви відзначите цей прапорець, фільтрація розглядатиме ваш запит фільтра, як **регулярний вираз**. Пояснюючи їх виходить за рамки цього документа. Гарне місце для початку навчання він `регулярного expressions.info `__. **Видалення порожніх папок після видалення або переміщення:** Коли ця опція включена, папки будуть видалені через файл видалений або переміщений і папка порожня. **Копіювання і переміщення:** Визначає, як операції копіювання та переміщення (в меню Дії) буде себе вести. * **Право на призначення:** Всі файли будуть відправлені безпосередньо в пункт призначення, не намагаючись відтворити початковий шлях взагалі. * **Повторно відносний шлях:** шлях вихідний файл буде відтворений в папці призначення, аж до кореневого виділення в панелі Directories. Наприклад, якщо ви додали ``/Users/foobar/SomeFolder`` на панель Каталоги і переміщенні ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` до місця призначення ``/Users/foobar/MyDestination/SubFolder``, кінцевим пунктом призначення для файлу буде ``/Users/foobar/MyDestination/SubFolder`` (``SomeFolder`` були скорочені зі шляху джерела в кінцевий пункт призначення.). * ** Повторно абсолютний шлях: ** шлях вихідний файл буде відтворений в папці призначення в повному комплекті. Наприклад, якщо ви переміщаєте ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` до місця призначення ``/Users/foobar/MyDestination``, кінцевим пунктом призначення для файлу буде ``/Users/foobar/MyDestination/Users/foobar/SomeFolder/SubFolder``. У всіх випадках, dupeGuru красиво ручки конфліктів імен шляхом додавання номера призначення ім'я файлу, якщо ім'я файлу вже існує в місці призначення. **Спеціальної команди:** Це перевагу визначає команду, яка буде викликатися "Викликати спеціальної команди" дії. Ви можете посилатися ні на які зовнішні програми через цю дію. Це може бути корисно, якщо, наприклад, у вас є хороший додаток порівнюєте встановлені. Формат команди такий же, як те, що ви повинні написати в командному рядку, за винятком того, що Є 2 заповнювачів: **%d** and **%r**. Ці наповнювачі будуть замінені на шлях вибраний обдурити (% г) і шлях до заслання на файл вибраного обдурити (%r). Якщо шлях до виконуваного містить прогалини, необхідно укласти його в "" лапки. Ви також повинні докласти заповнювачів в лапки, бо це дуже можливо, що шлях до обдурених і посилання будуть містити пробіли. Ось приклад користувальницької команди:: "C:\Program Files\SuperDiffProg\SuperDiffProg.exe" "%d" "%r" dupeguru-4.3.1/help/uk/quick_start.rst000066400000000000000000000032731426171743600200310ustar00rootroot00000000000000Швидкий старт ============== Щоб ви швидко почали з dupeGuru, давайте просто робити сканування за допомогою стандартних параметрів за замовчуванням. * Запуск dupeGuru. * Додавання папок для сканування або перетягнути & краплі або кнопку "+". * Натисніть на сканування. * Почекайте, поки процес сканування завершено. * Подивіться на кожен дублікат (файли, які відступом) і переконайтеся, що це дійсно дублікат посиланням групи (файл вище дублювати без відступу та інвалідів вікна знак). * Якщо файл помилкових дублікатів, виділіть її та натисніть **Дії -> Видалити вибрані з результатів**. * Якщо ви впевнені, що немає помилкових дублікатів в результатах, натисніть на **Редагувати -> Позначити Всі**, а потім **Дії -> Отправить Позначено до кошику**. Це тільки основні сканування. Є багато налаштування ви можете зробити, щоб отримати різні результати і кілька методів вивчення та зміни ваших результатів. Щоб дізнатися про них, щойно прочитав решту цього файлу довідки.dupeguru-4.3.1/help/uk/reprioritize.rst000066400000000000000000000060631426171743600202270ustar00rootroot00000000000000Повторне пріоритетів дублікатів ================================ dupeGuru намагається автоматично визначити, які дублікат повинен відправитися в заслання кожної групи позиції, але іноді це робиться неправильно. У багатьох випадках, розумний обдурити сортування з "Цінності Дельта" і "ошукані Тільки" варіанти на додаток до "Зробити вибраної посилання" дія робить трюк, але іноді, більш потужний варіант не потрібно. Тут зміни пріоритетів в діалог вступає в грати. Ви можете викликати його через "змінити пріоритети Результати" пункт в меню "Дії". Цей діалог дозволяє вам вибрати критерії, за якими посилання обдурити будуть відібрані в кожній групі обдурити. Список доступних критеріїв зліва і перелік критеріїв ви Обрана справа. Критеріїв категорії слідують аргумент. Наприклад, "Розмір (Вищий)" означає, що обдурити з великим розміром переможе. "Властивості папки (/Foo/Bar)" означає, що ошукані в цій папці буде перемогти. для додавання критерій правом списку, спочатку виберіть категорію в спадному списку і виберіть subargument в наведеному нижче списку, а потім натисніть на праву стрілку кнопки. Порядок списку праворуч важливо (ви можете змінити порядок елементів через перетягнути і відпустити). коли збір обдурити для довідки позицію, перший критерій використовується. Якщо є краватка, другий критерій використовується і так далі і так далі. Наприклад, якщо ваші аргументи "Розмір (вищий)", а потім "Файл (Не закінчується на номер)", заслання на файл, який буде обраний у групі буде найбільших файл, а якщо два або декілька файлів мають однаковий розмір, який має ім'я файлу з не закінчується номер буде використовуватися. Коли всі критерії привести до зв'язку, порядок, в якому ошукані раніше були в групі буде використовуватися.dupeguru-4.3.1/help/uk/results.rst000066400000000000000000000435031426171743600172010ustar00rootroot00000000000000Результати =========== Коли dupeGuru завершення сканування на наявність дублікатів, він покаже його результати у вигляді дубліката список групи. Про дублікат групи ---------------------- Дублікат група являє собою групу файлів, які весь матч разом. Кожна група має **посиланням** на файл і одного або більше **однакових файлів**. Посилання файл перший файл групи. Його марка вікно вимкнено. Під ним, і з відступом, які дублікатів файлів. Ви можете відзначити дублікатів файлів, але ви ніколи не можете помітити посилання файл групи. Це захід безпеки, щоб запобігти dupeGuru від видалення не тільки повторювані файли, але їх посилання. Ти впевнений, що не хочу цього, чи не так? Що визначає, які файли посилання і які файли є дублікатами спочатку свою папку держави. Файл з посиланням папка завжди буде посилання в дублікат групи. Якщо всі файли зі звичайної папки, розмір визначити, який файл буде ведення дублікат групи. dupeGuru припускає, що ви завжди хочете зберегти найбільших файл, так що великих файлів займе вихідне положення. Ви можете змінити посилання файл групи вручну. Для цього виберіть дублікат файлу, який ви хочете просувати на посилання, і натисніть на кнопку **Дії -> Додати вибраної посилання**. Перегляд результатів -------------------- Хоча ви можете просто натиснути на **Правка -> Виділити все, а потім** **Дії -> Отправить Позначено до кошику** швидко видалити всі дублікати файлів в результатах, завжди рекомендується переглянути всі дублікати перед видаляючи їх. Щоб допомогти вам огляд результатів, ви можете викликати панель **Докладніше**. Ця панель показує всі деталі обраного файла, а також подробиці свого заслання в. Це дуже зручно, щоб швидко визначити, якщо дублікат дійсно дублікат. Ви також можете двічі клацнути по файлу, щоб відкрити його і пов'язані з ним програми. Якщо у вас є більше помилкових дублікатів, ніж правда дублікатів (Якщо Ваш фільтр жорсткість дуже низька), кращий спосіб продовжити б переглянути дублікатів, знак істинного дублікати і натисніть **Дії -> Отправить Позначено до кошику** . Якщо у вас є більш вірно, ніж помилкових дублікатів дублікатів, замість цього можна позначити всі файли, які є помилковими дублікатів, а також використовувати **Дії -> Видалити Помічені від результатів**. Маркування і вибір --------------------- **Зазначені** дублікат двох примірниках з невеликою прапорець поруч з ним, мають галочки. **Обрано** дублікат дубліката бути виділені. Кілька дій, вибір може бути виконана в dupeGuru стандартним чином (Shift/Command/Control клік). Ви можете перемикати знак стан всіх вибраних дублікати ", натиснувши **просторі**. Показати тільки ошукані ----------------------- Коли цей режим включений, дублікати відображаються без їх відповідного файлу довідки. Ви можете вибрати, марка і сортувати цей список, як і в звичайному режимі. DupeGuru результати, коли в нормальному режимі, сортуються відповідно до дублікат групи '**посиланням на файл**. Це означає, що якщо ви хочете, наприклад, щоб відзначити все дублікати "EXE" розширенням, ви не можете просто сортувати результати по "Вид", щоб мати всі EXE дублікатів разом, тому що група може складатися з більш ніж одного типу файлів . Ось де обдурені Тільки режим вступає в гру. Щоб позначити всі ваші "EXE" дублікати, ви просто повинні: * Включити ошукані Тільки режим. * Додати "Вид" колонку "Стовпці" меню. * Натисніть на те, що "Вид" колонки, щоб відсортувати список за типом. * Знайдена перша дублікат з "EXE" роду. * Виберіть його. * Перейдіть, щоб знайти останнього дубліката з "EXE" роду. * Утримуйте Shift і клацніть по ньому. * Натисніть Space, щоб позначити всі вибрані дублікатів. Дельта значення ---------------- Якщо включити цей перемикач на деякі стовпці будуть відображати значення по відношенню до дубліката засланні, а не абсолютні значення. Ці дельти значення також будуть відображатися в різні кольори, щоб ви могли помітити їх легко. Наприклад, якщо дублікат 1,2 Мб і своє посилання в 1,4 Мб, розмір стовпець відображає -0,2 Мб. Тільки ошукані і Дельта значення -------------------------------- Тільки ошукані режимі розкрити свою дійсну силу, коли ви використовуєте його з Delta Значення перемикач включений. Коли ви дозволите його, відносні значення буде відображатися замість абсолютних. Так що якщо, наприклад, ви хочете видалити з результатів всі дублікати, які є більш 300 Кб від їх посиланню, ви можете відсортувати дублікати тільки результати за розміром, виберіть всі дублікати при -300 в стовпці Розмір, видаляти їх, , а потім зробити те ж саме повторює більше 300 в нижній частині списку. Ви можете також використовувати його для зміни посилання пріоритет повторювані список. Коли ви робите свіжі сканування, якщо Є немає посилання папки, заслання на файл кожної групи є найбільшою файл. Якщо ви хочете змінити, що, наприклад, в останній модифікації час, ви можете відсортувати дублікати тільки результати за часом модифікації в **убування порядку** , виберіть всі дублікати з часом зміни дельти значення більше 0 і натисніть **Переконайтеся, обраної посилання**. Причина, чому ви повинні зробити порядок сортування за спаданням, тому що якщо 2 файли серед таких же дублікат групи вибираються при натисканні на **Зробити вибраної посилання**, тільки перший із списку будуть зроблені посилання, інші будуть проігноровані . І так як ви хочете Остання зміна файлу для посилання, які мають порядок сортування за спаданням запевняє вас, що першим пунктом у списку буде останньої зміни. .. todo:: Add "Non-numerical delta" information. Фільтрація ----------- dupeGuru підтримує після сканування, фільтрації. З його допомогою ви можете звузити результати, щоб ви могли виконувати дії, на підмножини. Наприклад, ви можете легко помітити всі дублікати з їх ім'я файлу, що містить "копіювати" з результатів за допомогою фільтра. .. todo:: Qt has a toolbar search field now, not a menu item. **Windows:** Для використання функції фільтрації, натисніть на Дії -> Застосувати фільтр, запишіть фільтр, який ви хочете застосувати і натисніть ОК. Щоб повернутися до нефільтроване результати, натисніть на Дії -> Скасувати фільтр. **Mac OS X:** Для використання функції фільтрації, тип фільтра в "Фільтр" поле пошуку на панелі інструментів. Щоб повернутися до нефільтроване результаті, очистіть поле, або натисніть на кнопку "X". У простому режимі (режим), що ви вводите в якості фільтра рядок, що використовується для виконання фактичної фільтрації, за винятком однієї маски: **\***. Таким чином, якщо ви введете "[*]" як ваш фільтр, він буде відповідати що-небудь з [] дужках в ньому, все, що між цими дужками. Для більш просунутих фільтрів, ви можете включити «Використання регулярних виразів при фільтрації" на. Функція фільтрації буде використовувати регулярні вирази. Регулярний вираз мови для узгодження тексту. Пояснюючи їх виходить за рамки цього документа. Гарне місце для початку навчання він `регулярного expressions.info `__. Матчі не чутливі до регістру, в простих і регулярних виразів режимі. Для фільтра, щоб відповідати, регулярний вираз не обов'язково має збігатися цілий файл, він просто зобов'язаний утримувати в ланцюжок, відповідну висловом. Ви могли помітити, що не всі дублікати в результаті будуть відповідати вашим фільтром. Це тому, що як тільки одна копія в матчах групового фільтра, то вся група залишиться в результатах, таким чином Ви можете мати більш чітке уявлення про дубліката контексті. Тим не менш, не відповідні дублікати у "заслання режимі". Таким чином, можна виконувати дії, як Марк все і обов'язково тільки знак фільтрується дублікатів. Дія меню ----------- * **Відкритий чорний список:** Видаліть всі ігнорують матчі ви додали. Ви повинні почати новий пошук знову очищується список ігнорованих щоб бути ефективними. * **Експорт результатів в XHTML:** Візьміть поточні результати, а також створювати файл XHTML з нього. Стовпців, які видно при натисканні на цю кнопку буде стовпців у файлі XHTML. Файл автоматично відкриється в браузері за замовчуванням. * **Надіслати Позначено в кошику:** Відправити всі відмічені дублікати, сміття, це очевидно. * **Видалити Помічені і заміна з Жорсткі**: Передає всі відмічені дублікати, сміття, але після того, як зробили це, вилучені файли замінюються `жорстких `__ посилання до заслання на файл. (Тільки для OS X і Linux) * **Переміщення Позначено в ...:** запросить призначення, а потім перемістити всі відмічені файли в тому, що призначення. Шлях вихідного файлу може бути відтворений в пункт призначення, залежно від "Копіювання і переміщення" переваги. * **Скопіюйте Позначено в ...:** запитає у вас місце, а потім скопіювати всі вибрані файли до цього пункту призначення. Шлях вихідного файлу може бути відтворений в пункт призначення, залежно від "Копіювання і переміщення" переваги. * **Видалити Помічені з результатів:** Видалити все відмічені дублікатів з результату пошуку. Самі файли не будуть порушені й залишаться, де вони. * **Видалити вибрані з результатів:** Видалити всі вибрані дублікатів з результату пошуку. Зверніть увагу, що всі вибрані файли посилання будуть ігноруватися, тільки дублікати можуть бути видалені з цією дією. * **Зробити Обраний Довідка:** Сприяння всі вибрані дублікатів посилання. Якщо дублікат частиною групи, що має посиланням на файл найближчі із заслання папки (в синій колір), не будуть прийняті заходи для цього дублікат. Якщо більш ніж один дублікат серед тієї ж групи обрані, тільки перший з кожної групи буде заохочуватися. * **Додати обрані в чорний список:** Це спочатку видаляє всі вибрані дублікати з результатів, а потім додати матчу, які дублюють та опорний струм в чорний список. Цей матч не прийде знову в подальшої перевірки. Копіювати себе і, можливо, повернеться, але він буде шукатися в іншій посиланням на файл. Ви можете очистити список ігнорованих з Відкритий чорний список команди. * **Відкрите Обраний з додатків за замовчунням:** Відкрийте файл за допомогою програми, пов'язаного з типом обраного файлу. * **Розкривати Обраний в Finder:** Відкрийте папку, яка містить вибраний файл. * **Викликати спеціальної команди:** Викликає зовнішню програму ви встановили в настройках з використанням виділеного фрагмента в якості аргументів у виклику. * **Перейменування обрано:** Запит нове ім'я, а потім перейменувати вибраний файл. .. todo:: Add Move and iPhoto/iTunes warning .. todo:: Add "Deletion Options" section.dupeguru-4.3.1/hscommon/000077500000000000000000000000001426171743600152155ustar00rootroot00000000000000dupeguru-4.3.1/hscommon/LICENSE000066400000000000000000000027651426171743600162340ustar00rootroot00000000000000Copyright 2014, Hardcoded Software Inc., http://www.hardcoded.net All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.dupeguru-4.3.1/hscommon/README000066400000000000000000000003301426171743600160710ustar00rootroot00000000000000This module is common code used in all Hardcoded Software applications. It has no stable API so it is not recommended to actually depend on it. But if you want to copy bits and pieces for your own apps, be my guest. dupeguru-4.3.1/hscommon/__init__.py000066400000000000000000000000001426171743600173140ustar00rootroot00000000000000dupeguru-4.3.1/hscommon/build.py000066400000000000000000000262741426171743600167010ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2009-03-03 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html """This module is a collection of function to help in HS apps build process. """ from argparse import ArgumentParser import os import sys import os.path as op import shutil import tempfile import plistlib from subprocess import Popen import re import importlib from datetime import datetime import glob from typing import Any, AnyStr, Callable, Dict, List, Union from hscommon.plat import ISWINDOWS def print_and_do(cmd: str) -> int: """Prints ``cmd`` and executes it in the shell.""" print(cmd) p = Popen(cmd, shell=True) return p.wait() def _perform(src: os.PathLike, dst: os.PathLike, action: Callable, actionname: str) -> None: if not op.lexists(src): print("Copying %s failed: it doesn't exist." % src) return if op.lexists(dst): if op.isdir(dst): shutil.rmtree(dst) else: os.remove(dst) print("{} {} --> {}".format(actionname, src, dst)) action(src, dst) def copy_file_or_folder(src: os.PathLike, dst: os.PathLike) -> None: if op.isdir(src): shutil.copytree(src, dst, symlinks=True) else: shutil.copy(src, dst) def move(src: os.PathLike, dst: os.PathLike) -> None: _perform(src, dst, os.rename, "Moving") def copy(src: os.PathLike, dst: os.PathLike) -> None: _perform(src, dst, copy_file_or_folder, "Copying") def _perform_on_all(pattern: AnyStr, dst: os.PathLike, action: Callable) -> None: # pattern is a glob pattern, example "folder/foo*". The file is moved directly in dst, no folder # structure from src is kept. filenames = glob.glob(pattern) for fn in filenames: destpath = op.join(dst, op.basename(fn)) action(fn, destpath) def move_all(pattern: AnyStr, dst: os.PathLike) -> None: _perform_on_all(pattern, dst, move) def copy_all(pattern: AnyStr, dst: os.PathLike) -> None: _perform_on_all(pattern, dst, copy) def filereplace(filename: os.PathLike, outfilename: Union[os.PathLike, None] = None, **kwargs) -> None: """Reads `filename`, replaces all {variables} in kwargs, and writes the result to `outfilename`.""" if outfilename is None: outfilename = filename fp = open(filename, encoding="utf-8") contents = fp.read() fp.close() # We can't use str.format() because in some files, there might be {} characters that mess with it. for key, item in kwargs.items(): contents = contents.replace(f"{{{key}}}", item) fp = open(outfilename, "wt", encoding="utf-8") fp.write(contents) fp.close() def get_module_version(modulename: str) -> str: mod = importlib.import_module(modulename) return mod.__version__ def setup_package_argparser(parser: ArgumentParser): parser.add_argument( "--sign", dest="sign_identity", help="Sign app under specified identity before packaging (OS X only)", ) parser.add_argument( "--nosign", action="store_true", dest="nosign", help="Don't sign the packaged app (OS X only)", ) parser.add_argument( "--src-pkg", action="store_true", dest="src_pkg", help="Build a tar.gz of the current source.", ) parser.add_argument( "--arch-pkg", action="store_true", dest="arch_pkg", help="Force Arch Linux packaging type, regardless of distro name.", ) # `args` come from an ArgumentParser updated with setup_package_argparser() def package_cocoa_app_in_dmg(app_path: os.PathLike, destfolder: os.PathLike, args) -> None: # Rather than signing our app in XCode during the build phase, we sign it during the package # phase because running the app before packaging can modify it and we want to be sure to have # a valid signature. if args.sign_identity: sign_identity = f"Developer ID Application: {args.sign_identity}" result = print_and_do(f'codesign --force --deep --sign "{sign_identity}" "{app_path}"') if result != 0: print("ERROR: Signing failed. Aborting packaging.") return elif not args.nosign: print("ERROR: Either --nosign or --sign argument required.") return build_dmg(app_path, destfolder) def build_dmg(app_path: os.PathLike, destfolder: os.PathLike) -> None: """Builds a DMG volume with application at ``app_path`` and puts it in ``dest_path``. The name of the resulting DMG volume is determined by the app's name and version. """ print(repr(op.join(app_path, "Contents", "Info.plist"))) with open(op.join(app_path, "Contents", "Info.plist"), "rb") as fp: plist = plistlib.load(fp) workpath = tempfile.mkdtemp() dmgpath = op.join(workpath, plist["CFBundleName"]) os.mkdir(dmgpath) print_and_do('cp -R "{}" "{}"'.format(app_path, dmgpath)) print_and_do('ln -s /Applications "%s"' % op.join(dmgpath, "Applications")) dmgname = "{}_osx_{}.dmg".format( plist["CFBundleName"].lower().replace(" ", "_"), plist["CFBundleVersion"].replace(".", "_"), ) print("Building %s" % dmgname) # UDBZ = bzip compression. UDZO (zip compression) was used before, but it compresses much less. print_and_do( 'hdiutil create "{}" -format UDBZ -nocrossdev -srcdir "{}"'.format(op.join(destfolder, dmgname), dmgpath) ) print("Build Complete") def add_to_pythonpath(path: os.PathLike) -> None: """Adds ``path`` to both ``PYTHONPATH`` env and ``sys.path``.""" abspath = op.abspath(path) pythonpath = os.environ.get("PYTHONPATH", "") pathsep = ";" if ISWINDOWS else ":" pythonpath = pathsep.join([abspath, pythonpath]) if pythonpath else abspath os.environ["PYTHONPATH"] = pythonpath sys.path.insert(1, abspath) # This is a method to hack around those freakingly tricky data inclusion/exlusion rules # in setuptools. We copy the packages *without data* in a build folder and then build the plugin # from there. def copy_packages( packages_names: List[str], dest: os.PathLike, create_links: bool = False, extra_ignores: Union[List[str], None] = None, ) -> None: """Copy python packages ``packages_names`` to ``dest``, spurious data. Copy will happen without tests, testdata, mercurial data or C extension module source with it. ``py2app`` include and exclude rules are **quite** funky, and doing this is the only reliable way to make sure we don't end up with useless stuff in our app. """ if ISWINDOWS: create_links = False if not extra_ignores: extra_ignores = [] ignore = shutil.ignore_patterns(".hg*", "tests", "testdata", "modules", "docs", "locale", *extra_ignores) for package_name in packages_names: if op.exists(package_name): source_path = package_name else: mod = __import__(package_name) source_path = mod.__file__ if mod.__file__.endswith("__init__.py"): source_path = op.dirname(source_path) dest_name = op.basename(source_path) dest_path = op.join(dest, dest_name) if op.exists(dest_path): if op.islink(dest_path): os.unlink(dest_path) else: shutil.rmtree(dest_path) print(f"Copying package at {source_path} to {dest_path}") if create_links: os.symlink(op.abspath(source_path), dest_path) else: if op.isdir(source_path): shutil.copytree(source_path, dest_path, ignore=ignore) else: shutil.copy(source_path, dest_path) def build_debian_changelog( changelogpath: os.PathLike, destfile: os.PathLike, pkgname: str, from_version: Union[str, None] = None, distribution: str = "precise", fix_version: Union[str, None] = None, ) -> None: """Builds a debian changelog out of a YAML changelog. Use fix_version to patch the top changelog to that version (if, for example, there was a packaging error and you need to quickly fix it) """ def desc2list(desc): # We take each item, enumerated with the '*' character, and transform it into a list. desc = desc.replace("\n", " ") desc = desc.replace(" ", " ") result = desc.split("*") return [s.strip() for s in result if s.strip()] ENTRY_MODEL = ( "{pkg} ({version}) {distribution}; urgency=low\n\n{changes}\n " "-- Virgil Dupras {date}\n\n" ) CHANGE_MODEL = " * {description}\n" changelogs = read_changelog_file(changelogpath) if from_version: # We only want logs from a particular version for index, log in enumerate(changelogs): if log["version"] == from_version: changelogs = changelogs[: index + 1] break if fix_version: changelogs[0]["version"] = fix_version rendered_logs = [] for log in changelogs: version = log["version"] logdate = log["date"] desc = log["description"] rendered_date = logdate.strftime("%a, %d %b %Y 00:00:00 +0000") rendered_descs = [CHANGE_MODEL.format(description=d) for d in desc2list(desc)] changes = "".join(rendered_descs) rendered_log = ENTRY_MODEL.format( pkg=pkgname, version=version, changes=changes, date=rendered_date, distribution=distribution, ) rendered_logs.append(rendered_log) result = "".join(rendered_logs) fp = open(destfile, "w") fp.write(result) fp.close() re_changelog_header = re.compile(r"=== ([\d.b]*) \(([\d\-]*)\)") def read_changelog_file(filename: os.PathLike) -> List[Dict[str, Any]]: def iter_by_three(it): while True: try: version = next(it) date = next(it) description = next(it) except StopIteration: return yield version, date, description with open(filename, encoding="utf-8") as fp: contents = fp.read() splitted = re_changelog_header.split(contents)[1:] # the first item is empty result = [] for version, date_str, description in iter_by_three(iter(splitted)): date = datetime.strptime(date_str, "%Y-%m-%d").date() d = { "date": date, "date_str": date_str, "version": version, "description": description.strip(), } result.append(d) return result def fix_qt_resource_file(path: os.PathLike) -> None: # pyrcc5 under Windows, if the locale is non-english, can produce a source file with a date # containing accented characters. If it does, the encoding is wrong and it prevents the file # from being correctly frozen by cx_freeze. To work around that, we open the file, strip all # comments, and save. with open(path, "rb") as fp: contents = fp.read() lines = contents.split(b"\n") lines = [line for line in lines if not line.startswith(b"#")] with open(path, "wb") as fp: fp.write(b"\n".join(lines)) dupeguru-4.3.1/hscommon/conflict.py000066400000000000000000000055741426171743600174030ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2008-01-08 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html """When you have to deal with names that have to be unique and can conflict together, you can use this module that deals with conflicts by prepending unique numbers in ``[]`` brackets to the name. """ import re import os import shutil from pathlib import Path from typing import Callable, List # This matches [123], but not [12] (3 digits being the minimum). # It also matches [1234] [12345] etc.. # And only at the start of the string re_conflict = re.compile(r"^\[\d{3}\d*\] ") def get_conflicted_name(other_names: List[str], name: str) -> str: """Returns name with a ``[000]`` number in front of it. The number between brackets depends on how many conlicted filenames there already are in other_names. """ name = get_unconflicted_name(name) if name not in other_names: return name i = 0 while True: newname = "[%03d] %s" % (i, name) if newname not in other_names: return newname i += 1 def get_unconflicted_name(name: str) -> str: """Returns ``name`` without ``[]`` brackets. Brackets which, of course, might have been added by func:`get_conflicted_name`. """ return re_conflict.sub("", name, 1) def is_conflicted(name: str) -> bool: """Returns whether ``name`` is prepended with a bracketed number.""" return re_conflict.match(name) is not None def _smart_move_or_copy(operation: Callable, source_path: Path, dest_path: Path) -> None: """Use move() or copy() to move and copy file with the conflict management.""" if dest_path.is_dir() and not source_path.is_dir(): dest_path = dest_path.joinpath(source_path.name) if dest_path.exists(): filename = dest_path.name dest_dir_path = dest_path.parent newname = get_conflicted_name(os.listdir(str(dest_dir_path)), filename) dest_path = dest_dir_path.joinpath(newname) operation(str(source_path), str(dest_path)) def smart_move(source_path: Path, dest_path: Path) -> None: """Same as :func:`smart_copy`, but it moves files instead.""" _smart_move_or_copy(shutil.move, source_path, dest_path) def smart_copy(source_path: Path, dest_path: Path) -> None: """Copies ``source_path`` to ``dest_path``, recursively and with conflict resolution.""" try: _smart_move_or_copy(shutil.copy, source_path, dest_path) except OSError as e: if e.errno in { 21, 13, }: # it's a directory, code is 21 on OS X / Linux and 13 on Windows _smart_move_or_copy(shutil.copytree, source_path, dest_path) else: raise dupeguru-4.3.1/hscommon/desktop.py000066400000000000000000000060231426171743600172410ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2013-10-12 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from enum import Enum from os import PathLike import os.path as op import logging class SpecialFolder(Enum): APPDATA = 1 CACHE = 2 def open_url(url: str) -> None: """Open ``url`` with the default browser.""" _open_url(url) def open_path(path: PathLike) -> None: """Open ``path`` with its associated application.""" _open_path(str(path)) def reveal_path(path: PathLike) -> None: """Open the folder containing ``path`` with the default file browser.""" _reveal_path(str(path)) def special_folder_path(special_folder: SpecialFolder, portable: bool = False) -> str: """Returns the path of ``special_folder``. ``special_folder`` is a SpecialFolder.* const. The result is the special folder for the current application. The running process' application info is used to determine relevant information. You can override the application name with ``appname``. This argument is ingored under Qt. """ return _special_folder_path(special_folder, portable=portable) try: from PyQt5.QtCore import QUrl, QStandardPaths from PyQt5.QtGui import QDesktopServices from qt.util import get_appdata from core.util import executable_folder from hscommon.plat import ISWINDOWS, ISOSX import subprocess def _open_url(url: str) -> None: QDesktopServices.openUrl(QUrl(url)) def _open_path(path: str) -> None: url = QUrl.fromLocalFile(str(path)) QDesktopServices.openUrl(url) def _reveal_path(path: str) -> None: if ISWINDOWS: subprocess.run(["explorer", "/select,", op.abspath(path)]) elif ISOSX: subprocess.run(["open", "-R", op.abspath(path)]) else: _open_path(op.dirname(str(path))) def _special_folder_path(special_folder: SpecialFolder, portable: bool = False) -> str: if special_folder == SpecialFolder.CACHE: if ISWINDOWS and portable: folder = op.join(executable_folder(), "cache") else: folder = QStandardPaths.standardLocations(QStandardPaths.CacheLocation)[0] else: folder = get_appdata(portable) return folder except ImportError: # We're either running tests, and these functions don't matter much or we're in a really # weird situation. Let's just have dummy fallbacks. logging.warning("Can't setup desktop functions!") def _open_url(url: str) -> None: # Dummy for tests pass def _open_path(path: str) -> None: # Dummy for tests pass def _reveal_path(path: str) -> None: # Dummy for tests pass def _special_folder_path(special_folder: SpecialFolder, portable: bool = False) -> str: return "/tmp" dupeguru-4.3.1/hscommon/gui/000077500000000000000000000000001426171743600160015ustar00rootroot00000000000000dupeguru-4.3.1/hscommon/gui/__init__.py000066400000000000000000000000001426171743600201000ustar00rootroot00000000000000dupeguru-4.3.1/hscommon/gui/base.py000066400000000000000000000067751426171743600173040ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html def noop(*args, **kwargs): pass class NoopGUI: def __getattr__(self, func_name): return noop class GUIObject: """Cross-toolkit "model" representation of a GUI layer object. A ``GUIObject`` is a cross-toolkit "model" representation of a GUI layer object, for example, a table. It acts as a cross-toolkit interface to what we call here a :attr:`view`. That view is a toolkit-specific controller to the actual view (an ``NSTableView``, a ``QTableView``, etc.). In our GUIObject, we need a reference to that toolkit-specific controller because some actions have effects on it (for example, prompting it to refresh its data). The ``GUIObject`` is typically instantiated before its :attr:`view`, that is why we set it to ``None`` on init. However, the GUI layer is supposed to set the view as soon as its toolkit-specific controller is instantiated. When you subclass ``GUIObject``, you will likely want to update its view on instantiation. That is why we call ``self.view.refresh()`` in :meth:`_view_updated`. If you need another type of action on view instantiation, just override the method. Most of the time, you will only one to bind a view once in the lifetime of your GUI object. That is why there are safeguards, when setting ``view`` to ensure that we don't double-assign. However, sometimes you want to be able to re-bind another view. In this case, set the ``multibind`` flag to ``True`` and the safeguard will be disabled. """ def __init__(self, multibind: bool = False) -> None: self._view = None self._multibind = multibind def _view_updated(self) -> None: """(Virtual) Called after :attr:`view` has been set. Doing nothing by default, this method is called after :attr:`view` has been set (it isn't called when it's unset, however). Use this for initialization code that requires a view (which is often the whole of the initialization code). """ def has_view(self) -> bool: return (self._view is not None) and (not isinstance(self._view, NoopGUI)) @property def view(self): """A reference to our toolkit-specific view controller. *view answering to GUIObject sublass's view protocol*. *get/set* This view starts as ``None`` and has to be set "manually". There's two times at which we set the view property: On initialization, where we set the view that we'll use for our lifetime, and just before the view is deallocated. We need to unset our view at that time to avoid calls to a deallocated instance (which means a crash). To unset our view, we simple assign it to ``None``. """ return self._view @view.setter def view(self, value) -> None: if self._view is None and value is None: # Initial view assignment return if self._view is None or self._multibind: if value is None: value = NoopGUI() self._view = value self._view_updated() else: assert value is None # Instead of None, we put a NoopGUI() there to avoid rogue view callback raising an # exception. self._view = NoopGUI() dupeguru-4.3.1/hscommon/gui/column.py000066400000000000000000000277661426171743600176720ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2010-07-25 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import copy from typing import Any, List, Tuple, Union from hscommon.gui.base import GUIObject from hscommon.gui.table import GUITable class Column: """Holds column attributes such as its name, width, visibility, etc. These attributes are then used to correctly configure the column on the "view" side. """ def __init__(self, name: str, display: str = "", visible: bool = True, optional: bool = False) -> None: #: "programmatical" (not for display) name. Used as a reference in a couple of place, such #: as :meth:`Columns.column_by_name`. self.name = name #: Immutable index of the column. Doesn't change even when columns are re-ordered. Used in #: :meth:`Columns.column_by_index`. self.logical_index = 0 #: Index of the column in the ordered set of columns. self.ordered_index = 0 #: Width of the column. self.width = 0 #: Default width of the column. This value usually depends on the platform and is set on #: columns initialisation. It will be used if column restoration doesn't contain any #: "remembered" widths. self.default_width = 0 #: Display name (title) of the column. self.display = display #: Whether the column is visible. self.visible = visible #: Whether the column is visible by default. It will be used if column restoration doesn't #: contain any "remembered" widths. self.default_visible = visible #: Whether the column can have :attr:`visible` set to false. self.optional = optional class ColumnsView: """Expected interface for :class:`Columns`'s view. *Not actually used in the code. For documentation purposes only.* Our view, the columns controller of a table or outline, is expected to properly respond to callbacks. """ def restore_columns(self) -> None: """Update all columns according to the model. When this is called, our view has to update the columns title, order and visibility of all columns. """ def set_column_visible(self, colname: str, visible: bool) -> None: """Update visibility of column ``colname``. Called when the user toggles the visibility of a column, we must update the column ``colname``'s visibility status to ``visible``. """ class PrefAccessInterface: """Expected interface for :class:`Columns`'s prefaccess. *Not actually used in the code. For documentation purposes only.* """ def get_default(self, key: str, fallback_value: Union[Any, None]) -> Any: """Retrieve the value for ``key`` in the currently running app's preference store. If the key doesn't exist, return ``fallback_value``. """ def set_default(self, key: str, value: Any) -> None: """Set the value ``value`` for ``key`` in the currently running app's preference store.""" class Columns(GUIObject): """Cross-toolkit GUI-enabled column set for tables or outlines. Manages a column set's order, visibility and width. We also manage the persistence of these attributes so that we can restore them on the next run. Subclasses :class:`.GUIObject`. Expected view: :class:`ColumnsView`. :param table: The table the columns belong to. It's from there that we retrieve our column configuration and it must have a ``COLUMNS`` attribute which is a list of :class:`Column`. We also call :meth:`~.GUITable.save_edits` on it from time to time. Technically, this argument can also be a tree, but there's probably some sorting in the code to do to support this option cleanly. :param prefaccess: An object giving access to user preferences for the currently running app. We use this to make column attributes persistent. Must follow :class:`PrefAccessInterface`. :param str savename: The name under which column preferences will be saved. This name is in fact a prefix. Preferences are saved under more than one name, but they will all have that same prefix. """ def __init__(self, table: GUITable, prefaccess=None, savename: Union[str, None] = None): GUIObject.__init__(self) self.table = table self.prefaccess = prefaccess self.savename = savename # We use copy here for test isolation. If we don't, changing a column affects all tests. self.column_list: List[Column] = list(map(copy.copy, table.COLUMNS)) for i, column in enumerate(self.column_list): column.logical_index = i column.ordered_index = i self.coldata = {col.name: col for col in self.column_list} # --- Private def _get_colname_attr(self, colname: str, attrname: str, default: Any) -> Any: try: return getattr(self.coldata[colname], attrname) except KeyError: return default def _set_colname_attr(self, colname: str, attrname: str, value: Any) -> None: try: col = self.coldata[colname] setattr(col, attrname, value) except KeyError: pass def _optional_columns(self) -> List[Column]: return [c for c in self.column_list if c.optional] # --- Override def _view_updated(self) -> None: self.restore_columns() # --- Public def column_by_index(self, index: int): """Return the :class:`Column` having the :attr:`~Column.logical_index` ``index``.""" return self.column_list[index] def column_by_name(self, name: str): """Return the :class:`Column` having the :attr:`~Column.name` ``name``.""" return self.coldata[name] def columns_count(self) -> int: """Returns the number of columns in our set.""" return len(self.column_list) def column_display(self, colname: str) -> str: """Returns display name for column named ``colname``, or ``''`` if there's none.""" return self._get_colname_attr(colname, "display", "") def column_is_visible(self, colname: str) -> bool: """Returns visibility for column named ``colname``, or ``True`` if there's none.""" return self._get_colname_attr(colname, "visible", True) def column_width(self, colname: str) -> int: """Returns width for column named ``colname``, or ``0`` if there's none.""" return self._get_colname_attr(colname, "width", 0) def columns_to_right(self, colname: str) -> List[str]: """Returns the list of all columns to the right of ``colname``. "right" meaning "having a higher :attr:`Column.ordered_index`" in our left-to-right civilization. """ column = self.coldata[colname] index = column.ordered_index return [col.name for col in self.column_list if (col.visible and col.ordered_index > index)] def menu_items(self) -> List[Tuple[str, bool]]: """Returns a list of items convenient for quick visibility menu generation. Returns a list of ``(display_name, is_marked)`` items for each optional column in the current view (``is_marked`` means that it's visible). You can use this to generate a menu to let the user toggle the visibility of an optional column. That is why we only show optional column, because the visibility of mandatory columns can't be toggled. """ return [(c.display, c.visible) for c in self._optional_columns()] def move_column(self, colname: str, index: int) -> None: """Moves column ``colname`` to ``index``. The column will be placed just in front of the column currently having that index, or to the end of the list if there's none. """ colnames = self.colnames colnames.remove(colname) colnames.insert(index, colname) self.set_column_order(colnames) def reset_to_defaults(self) -> None: """Reset all columns' width and visibility to their default values.""" self.set_column_order([col.name for col in self.column_list]) for col in self._optional_columns(): col.visible = col.default_visible col.width = col.default_width self.view.restore_columns() def resize_column(self, colname: str, newwidth: int) -> None: """Set column ``colname``'s width to ``newwidth``.""" self._set_colname_attr(colname, "width", newwidth) def restore_columns(self) -> None: """Restore's column persistent attributes from the last :meth:`save_columns`.""" if not (self.prefaccess and self.savename and self.coldata): if (not self.savename) and (self.coldata): # This is a table that will not have its coldata saved/restored. we should # "restore" its default column attributes. self.view.restore_columns() return for col in self.column_list: pref_name = f"{self.savename}.Columns.{col.name}" coldata = self.prefaccess.get_default(pref_name, fallback_value={}) if "index" in coldata: col.ordered_index = coldata["index"] if "width" in coldata: col.width = coldata["width"] if col.optional and "visible" in coldata: col.visible = coldata["visible"] self.view.restore_columns() def save_columns(self) -> None: """Save column attributes in persistent storage for restoration in :meth:`restore_columns`.""" if not (self.prefaccess and self.savename and self.coldata): return for col in self.column_list: pref_name = f"{self.savename}.Columns.{col.name}" coldata = {"index": col.ordered_index, "width": col.width} if col.optional: coldata["visible"] = col.visible self.prefaccess.set_default(pref_name, coldata) # TODO annotate colnames def set_column_order(self, colnames) -> None: """Change the columns order so it matches the order in ``colnames``. :param colnames: A list of column names in the desired order. """ colnames = (name for name in colnames if name in self.coldata) for i, colname in enumerate(colnames): col = self.coldata[colname] col.ordered_index = i def set_column_visible(self, colname: str, visible: bool) -> None: """Set the visibility of column ``colname``.""" self.table.save_edits() # the table on the GUI side will stop editing when the columns change self._set_colname_attr(colname, "visible", visible) self.view.set_column_visible(colname, visible) def set_default_width(self, colname: str, width: int) -> None: """Set the default width or column ``colname``.""" self._set_colname_attr(colname, "default_width", width) def toggle_menu_item(self, index: int) -> bool: """Toggles the visibility of an optional column. You know, that optional column menu you've generated in :meth:`menu_items`? Well, ``index`` is the index of them menu item in *that* menu that the user has clicked on to toggle it. Returns whether the column in question ends up being visible or not. """ col = self._optional_columns()[index] self.set_column_visible(col.name, not col.visible) return col.visible # --- Properties @property def ordered_columns(self) -> List[Column]: """List of :class:`Column` in visible order.""" return [col for col in sorted(self.column_list, key=lambda col: col.ordered_index)] @property def colnames(self) -> List[str]: """List of column names in visible order.""" return [col.name for col in self.ordered_columns] dupeguru-4.3.1/hscommon/gui/progress_window.py000066400000000000000000000142541426171743600216140ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from typing import Callable, Tuple, Union from hscommon.jobprogress.performer import ThreadedJobPerformer from hscommon.gui.base import GUIObject from hscommon.gui.text_field import TextField class ProgressWindowView: """Expected interface for :class:`ProgressWindow`'s view. *Not actually used in the code. For documentation purposes only.* Our view, some kind window with a progress bar, two labels and a cancel button, is expected to properly respond to its callbacks. It's also expected to call :meth:`ProgressWindow.cancel` when the cancel button is clicked. """ def show(self) -> None: """Show the dialog.""" def close(self) -> None: """Close the dialog.""" def set_progress(self, progress: int) -> None: """Set the progress of the progress bar to ``progress``. Not all jobs are equally responsive on their job progress report and it is recommended that you put your progressbar in "indeterminate" mode as long as you haven't received the first ``set_progress()`` call to avoid letting the user think that the app is frozen. :param int progress: a value between ``0`` and ``100``. """ class ProgressWindow(GUIObject, ThreadedJobPerformer): """Cross-toolkit GUI-enabled progress window. This class allows you to run a long running, job enabled function in a separate thread and allow the user to follow its progress with a progress dialog. To use it, you start your long-running job with :meth:`run` and then have your UI layer regularly call :meth:`pulse` to refresh the job status in the UI. It is advised that you call :meth:`pulse` in the main thread because GUI toolkit usually only support calling UI-related functions from the main thread. We subclass :class:`.GUIObject` and :class:`.ThreadedJobPerformer`. Expected view: :class:`ProgressWindowView`. :param finish_func: A function ``f(jobid)`` that is called when a job is completed. ``jobid`` is an arbitrary id passed to :meth:`run`. :param error_func: A function ``f(jobid, err)`` that is called when an exception is raised and unhandled during the job. If not specified, the error will be raised in the main thread. If it's specified, it's your responsibility to raise the error if you want to. If the function returns ``True``, ``finish_func()`` will be called as if the job terminated normally. """ def __init__( self, finish_func: Callable[[Union[str, None]], None], error_func: Callable[[Union[str, None], Exception], bool] = None, ) -> None: # finish_func(jobid) is the function that is called when a job is completed. GUIObject.__init__(self) ThreadedJobPerformer.__init__(self) self._finish_func = finish_func self._error_func = error_func #: :class:`.TextField`. It contains that title you gave the job on :meth:`run`. self.jobdesc_textfield = TextField() #: :class:`.TextField`. It contains the job textual update that the function might yield #: during its course. self.progressdesc_textfield = TextField() self.jobid: Union[str, None] = None def cancel(self) -> None: """Call for a user-initiated job cancellation.""" # The UI is sometimes a bit buggy and calls cancel() on self.view.close(). We just want to # make sure that this doesn't lead us to think that the user acually cancelled the task, so # we verify that the job is still running. if self._job_running: self.job_cancelled = True def pulse(self) -> None: """Update progress reports in the GUI. Call this regularly from the GUI main run loop. The values might change before :meth:`ProgressWindowView.set_progress` happens. If the job is finished, ``pulse()`` will take care of closing the window and re-raising any exception that might have been raised during the job (in the main thread this time). If there was no exception, ``finish_func(jobid)`` is called to let you take appropriate action. """ last_progress = self.last_progress last_desc = self.last_desc if not self._job_running or last_progress is None: self.view.close() should_continue = True if self.last_error is not None: err = self.last_error.with_traceback(self.last_traceback) if self._error_func is not None: should_continue = self._error_func(self.jobid, err) else: raise err if not self.job_cancelled and should_continue: self._finish_func(self.jobid) return if self.job_cancelled: return if last_desc: self.progressdesc_textfield.text = last_desc self.view.set_progress(last_progress) def run(self, jobid: str, title: str, target: Callable, args: Tuple = ()): """Starts a threaded job. The ``target`` function will be sent, as its first argument, a :class:`.Job` instance which it can use to report on its progress. :param jobid: Arbitrary identifier which will be passed to ``finish_func()`` at the end. :param title: A title for the task you're starting. :param target: The function that does your famous long running job. :param args: additional arguments that you want to send to ``target``. """ # target is a function with its first argument being a Job. It can then be followed by other # arguments which are passed as `args`. self.jobid = jobid self.progressdesc_textfield.text = "" j = self.create_job() args = tuple([j] + list(args)) self.run_threaded(target, args) self.jobdesc_textfield.text = title self.view.show() dupeguru-4.3.1/hscommon/gui/selectable_list.py000066400000000000000000000151021426171743600215100ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2011-09-06 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from collections.abc import Sequence, MutableSequence from hscommon.gui.base import GUIObject class Selectable(Sequence): """Mix-in for a ``Sequence`` that manages its selection status. When mixed in with a ``Sequence``, we enable it to manage its selection status. The selection is held as a list of ``int`` indexes. Multiple selection is supported. """ def __init__(self): self._selected_indexes = [] # --- Private def _check_selection_range(self): if not self: self._selected_indexes = [] if not self._selected_indexes: return self._selected_indexes = [index for index in self._selected_indexes if index < len(self)] if not self._selected_indexes: self._selected_indexes = [len(self) - 1] # --- Virtual def _update_selection(self): """(Virtual) Updates the model's selection appropriately. Called after selection has been updated. Takes the table's selection and does appropriates updates on the view and/or model. Common sense would dictate that when the selection doesn't change, we don't update anything (and thus don't call ``_update_selection()`` at all), but there are cases where it's false. For example, if our list updates its items but doesn't change its selection, we probably want to update the model's selection. By default, does nothing. Important note: This is only called on :meth:`select`, not on changes to :attr:`selected_indexes`. """ # A redesign of how this whole thing works is probably in order, but not now, there's too # much breakage at once involved. # --- Public def select(self, indexes): """Update selection to ``indexes``. :meth:`_update_selection` is called afterwards. :param list indexes: List of ``int`` that is to become the new selection. """ if isinstance(indexes, int): indexes = [indexes] self.selected_indexes = indexes self._update_selection() # --- Properties @property def selected_index(self): """Points to the first selected index. *int*. *get/set*. Thin wrapper around :attr:`selected_indexes`. ``None`` if selection is empty. Using this property only makes sense if your selectable sequence supports single selection only. """ return self._selected_indexes[0] if self._selected_indexes else None @selected_index.setter def selected_index(self, value): self.selected_indexes = [value] @property def selected_indexes(self): """List of selected indexes. *list of int*. *get/set*. When setting the value, automatically removes out-of-bounds indexes. The list is kept sorted. """ return self._selected_indexes @selected_indexes.setter def selected_indexes(self, value): self._selected_indexes = value self._selected_indexes.sort() self._check_selection_range() class SelectableList(MutableSequence, Selectable): """A list that can manage selection of its items. Subclasses :class:`Selectable`. Behaves like a ``list``. """ def __init__(self, items=None): Selectable.__init__(self) if items: self._items = list(items) else: self._items = [] def __delitem__(self, key): self._items.__delitem__(key) self._check_selection_range() self._on_change() def __getitem__(self, key): return self._items.__getitem__(key) def __len__(self): return len(self._items) def __setitem__(self, key, value): self._items.__setitem__(key, value) self._on_change() # --- Override def append(self, item): self._items.append(item) self._on_change() def insert(self, index, item): self._items.insert(index, item) self._on_change() def remove(self, row): self._items.remove(row) self._check_selection_range() self._on_change() # --- Virtual def _on_change(self): """(Virtual) Called whenever the contents of the list changes. By default, does nothing. """ # --- Public def search_by_prefix(self, prefix): # XXX Why the heck is this method here? prefix = prefix.lower() for index, s in enumerate(self): if s.lower().startswith(prefix): return index return -1 class GUISelectableListView: """Expected interface for :class:`GUISelectableList`'s view. *Not actually used in the code. For documentation purposes only.* Our view, some kind of list view or combobox, is expected to sync with the list's contents by appropriately behave to all callbacks in this interface. """ def refresh(self): """Refreshes the contents of the list widget. Ensures that the contents of the list widget is synced with the model. """ def update_selection(self): """Update selection status. Ensures that the list widget's selection is in sync with the model. """ class GUISelectableList(SelectableList, GUIObject): """Cross-toolkit GUI-enabled list view. Represents a UI element presenting the user with a selectable list of items. Subclasses :class:`SelectableList` and :class:`.GUIObject`. Expected view: :class:`GUISelectableListView`. :param iterable items: If specified, items to fill the list with initially. """ def __init__(self, items=None): SelectableList.__init__(self, items) GUIObject.__init__(self) def _view_updated(self): """Refreshes the view contents with :meth:`GUISelectableListView.refresh`. Overrides :meth:`~hscommon.gui.base.GUIObject._view_updated`. """ self.view.refresh() def _update_selection(self): """Refreshes the view selection with :meth:`GUISelectableListView.update_selection`. Overrides :meth:`Selectable._update_selection`. """ self.view.update_selection() def _on_change(self): """Refreshes the view contents with :meth:`GUISelectableListView.refresh`. Overrides :meth:`SelectableList._on_change`. """ self.view.refresh() dupeguru-4.3.1/hscommon/gui/table.py000066400000000000000000000514101426171743600174430ustar00rootroot00000000000000# Created By: Eric Mc Sween # Created On: 2008-05-29 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from collections.abc import MutableSequence from collections import namedtuple from typing import Any, List, Tuple, Union from hscommon.gui.base import GUIObject from hscommon.gui.selectable_list import Selectable # We used to directly subclass list, but it caused problems at some point with deepcopy class Table(MutableSequence, Selectable): """Sortable and selectable sequence of :class:`Row`. In fact, the Table is very similar to :class:`.SelectableList` in practice and differs mostly in principle. Their difference lies in the nature of their items they manage. With the Table, rows usually have many properties, presented in columns, and they have to subclass :class:`Row`. Usually used with :class:`~hscommon.gui.column.Column`. Subclasses :class:`.Selectable`. """ # Should be List[Column], but have circular import... COLUMNS: List = [] def __init__(self) -> None: Selectable.__init__(self) self._rows: List["Row"] = [] self._header: Union["Row", None] = None self._footer: Union["Row", None] = None # TODO type hint for key def __delitem__(self, key): self._rows.__delitem__(key) if self._header is not None and ((not self) or (self[0] is not self._header)): self._header = None if self._footer is not None and ((not self) or (self[-1] is not self._footer)): self._footer = None self._check_selection_range() # TODO type hint for key def __getitem__(self, key) -> Any: return self._rows.__getitem__(key) def __len__(self) -> int: return len(self._rows) # TODO type hint for key def __setitem__(self, key, value: Any) -> None: self._rows.__setitem__(key, value) def append(self, item: "Row") -> None: """Appends ``item`` at the end of the table. If there's a footer, the item is inserted before it. """ if self._footer is not None: self._rows.insert(-1, item) else: self._rows.append(item) def insert(self, index: int, item: "Row") -> None: """Inserts ``item`` at ``index`` in the table. If there's a header, will make sure we don't insert before it, and if there's a footer, will make sure that we don't insert after it. """ if (self._header is not None) and (index == 0): index = 1 if (self._footer is not None) and (index >= len(self)): index = len(self) - 1 self._rows.insert(index, item) def remove(self, row: "Row") -> None: """Removes ``row`` from table. If ``row`` is a header or footer, that header or footer will be set to ``None``. """ if row is self._header: self._header = None if row is self._footer: self._footer = None self._rows.remove(row) self._check_selection_range() def sort_by(self, column_name: str, desc: bool = False) -> None: """Sort table by ``column_name``. Sort key for each row is computed from :meth:`Row.sort_key_for_column`. If ``desc`` is ``True``, sort order is reversed. If present, header and footer will always be first and last, respectively. """ if self._header is not None: self._rows.pop(0) if self._footer is not None: self._rows.pop() self._rows.sort(key=lambda row: row.sort_key_for_column(column_name), reverse=desc) if self._header is not None: self._rows.insert(0, self._header) if self._footer is not None: self._rows.append(self._footer) # --- Properties @property def footer(self) -> Union["Row", None]: """If set, a row that always stay at the bottom of the table. :class:`Row`. *get/set*. When set to something else than ``None``, ``header`` and ``footer`` represent rows that will always be kept in first and/or last position, regardless of sorting. ``len()`` and indexing will include them, which means that if there's a header, ``table[0]`` returns it and if there's a footer, ``table[-1]`` returns it. To make things short, all list-like functions work with header and footer "on". But things get fuzzy for ``append()`` and ``insert()`` because these will ensure that no "normal" row gets inserted before the header or after the footer. Adding and removing footer here and there might seem (and is) hackish, but it's much simpler than the alternative (when, of course, you need such a feature), which is to override magic methods and adjust the results. When we do that, there the slice stuff that we have to implement and it gets quite complex. Moreover, the most frequent operation on a table is ``__getitem__``, and making checks to know whether the key is a header or footer at each call would make that operation, which is the most used, slower. """ return self._footer @footer.setter def footer(self, value: Union["Row", None]) -> None: if self._footer is not None: self._rows.pop() if value is not None: self._rows.append(value) self._footer = value @property def header(self) -> Union["Row", None]: """If set, a row that always stay at the bottom of the table. See :attr:`footer` for details. """ return self._header @header.setter def header(self, value: Union["Row", None]) -> None: if self._header is not None: self._rows.pop(0) if value is not None: self._rows.insert(0, value) self._header = value @property def row_count(self) -> int: """Number or rows in the table (without counting header and footer). *int*. *read-only*. """ result = len(self) if self._footer is not None: result -= 1 if self._header is not None: result -= 1 return result @property def rows(self) -> List["Row"]: """List of rows in the table, excluding header and footer. List of :class:`Row`. *read-only*. """ start = None end = None if self._footer is not None: end = -1 if self._header is not None: start = 1 return self[start:end] @property def selected_row(self) -> "Row": """Selected row according to :attr:`Selectable.selected_index`. :class:`Row`. *get/set*. When setting this attribute, we look up the index of the row and set the selected index from there. If the row isn't in the list, selection isn't changed. """ return self[self.selected_index] if self.selected_index is not None else None @selected_row.setter def selected_row(self, value: int) -> None: try: self.selected_index = self.index(value) except ValueError: pass @property def selected_rows(self) -> List["Row"]: """List of selected rows based on :attr:`.selected_indexes`. List of :class:`Row`. *read-only*. """ return [self[index] for index in self.selected_indexes] class GUITableView: """Expected interface for :class:`GUITable`'s view. *Not actually used in the code. For documentation purposes only.* Our view, some kind of table view, is expected to sync with the table's contents by appropriately behave to all callbacks in this interface. When in edit mode, the content types by the user is expected to be sent as soon as possible to the :class:`Row`. Whenever the user changes the selection, we expect the view to call :meth:`Table.select`. """ def refresh(self) -> None: """Refreshes the contents of the table widget. Ensures that the contents of the table widget is synced with the model. This includes selection. """ def start_editing(self) -> None: """Start editing the currently selected row. Begin whatever inline editing support that the view supports. """ def stop_editing(self) -> None: """Stop editing if there's an inline editing in effect. There's no "aborting" implied in this call, so it's appropriate to send whatever the user has typed and might not have been sent down to the :class:`Row` yet. After you've done that, stop the editing mechanism. """ SortDescriptor = namedtuple("SortDescriptor", "column desc") class GUITable(Table, GUIObject): """Cross-toolkit GUI-enabled table view. Represents a UI element presenting the user with a sortable, selectable, possibly editable, table view. Behaves like the :class:`Table` which it subclasses, but is more focused on being the presenter of some model data to its :attr:`.GUIObject.view`. There's a :meth:`refresh` mechanism which ensures fresh data while preserving sorting order and selection. There's also an editing mechanism which tracks whether (and which) row is being edited (or added) and save/cancel edits when appropriate. Subclasses :class:`Table` and :class:`.GUIObject`. Expected view: :class:`GUITableView`. """ def __init__(self) -> None: GUIObject.__init__(self) Table.__init__(self) #: The row being currently edited by the user. ``None`` if no edit is taking place. self.edited: Union["Row", None] = None self._sort_descriptor: Union[SortDescriptor, None] = None # --- Virtual def _do_add(self) -> Tuple["Row", int]: """(Virtual) Creates a new row, adds it in the table. Returns ``(row, insert_index)``. """ raise NotImplementedError() def _do_delete(self) -> None: """(Virtual) Delete the selected rows.""" pass def _fill(self) -> None: """(Virtual/Required) Fills the table with all the rows that this table is supposed to have. Called by :meth:`refresh`. Does nothing by default. """ pass def _is_edited_new(self) -> bool: """(Virtual) Returns whether the currently edited row should be considered "new". This is used in :meth:`cancel_edits` to know whether the cancellation of the edit means a revert of the row's value or the removal of the row. By default, always false. """ return False def _restore_selection(self, previous_selection): """(Virtual) Restores row selection after a contents-changing operation. Before each contents changing operation, we store our previously selected indexes because in many cases, such as in :meth:`refresh`, our selection will be lost. After the operation is over, we call this method with our previously selected indexes (in ``previous_selection``). The default behavior is (if we indeed have an empty :attr:`.selected_indexes`) to re-select ``previous_selection``. If it was empty, we select the last row of the table. This behavior can, of course, be overriden. """ if not self.selected_indexes: if previous_selection: self.select(previous_selection) else: self.select([len(self) - 1]) # --- Public def add(self) -> None: """Add a new row in edit mode. Requires :meth:`do_add` to be implemented. The newly added row will be selected and in edit mode. """ self.view.stop_editing() if self.edited is not None: self.save_edits() row, insert_index = self._do_add() self.insert(insert_index, row) self.select([insert_index]) self.view.refresh() # We have to set "edited" after calling refresh() because some UI are trigger-happy # about calling save_edits() and they do so during calls to refresh(). We don't want # a call to save_edits() during refresh prematurely mess with our newly added item. self.edited = row self.view.start_editing() def can_edit_cell(self, column_name: str, row_index: int) -> bool: """Returns whether the cell at ``row_index`` and ``column_name`` can be edited. A row is, by default, editable as soon as it has an attr with the same name as `column`. If :meth:`Row.can_edit` returns False, the row is not editable at all. You can set editability of rows at the attribute level with can_edit_* properties. Mostly just a shortcut to :meth:`Row.can_edit_cell`. """ row = self[row_index] return row.can_edit_cell(column_name) def cancel_edits(self) -> None: """Cancels the current edit operation. If there's an :attr:`edited` row, it will be re-initialized (with :meth:`Row.load`). """ if self.edited is None: return self.view.stop_editing() if self._is_edited_new(): previous_selection = self.selected_indexes self.remove(self.edited) self._restore_selection(previous_selection) self._update_selection() else: self.edited.load() self.edited = None self.view.refresh() def delete(self) -> None: """Delete the currently selected rows. Requires :meth:`_do_delete` for this to have any effect on the model. Cancels editing if relevant. """ self.view.stop_editing() if self.edited is not None: self.cancel_edits() return if self: self._do_delete() def refresh(self, refresh_view: bool = True) -> None: """Empty the table and re-create its rows. :meth:`_fill` is called after we emptied the table to create our rows. Previous sort order will be preserved, regardless of the order in which the rows were filled. If there was any edit operation taking place, it's cancelled. :param bool refresh_view: Whether we tell our view to refresh after our refill operation. Most of the time, it's what we want, but there's some cases where we don't. """ self.cancel_edits() previous_selection = self.selected_indexes del self[:] self._fill() sd = self._sort_descriptor if sd is not None: Table.sort_by(self, column_name=sd.column, desc=sd.desc) self._restore_selection(previous_selection) if refresh_view: self.view.refresh() def save_edits(self) -> None: """Commit user edits to the model. This is done by calling :meth:`Row.save`. """ if self.edited is None: return row = self.edited self.edited = None row.save() def sort_by(self, column_name: str, desc: bool = False) -> None: """Sort table by ``column_name``. Overrides :meth:`Table.sort_by`. After having performed sorting, calls :meth:`~.Selectable._update_selection` to give you the chance, if appropriate, to update your selected indexes according to, maybe, the selection that you have in your model. Then, we refresh our view. """ Table.sort_by(self, column_name=column_name, desc=desc) self._sort_descriptor = SortDescriptor(column_name, desc) self._update_selection() self.view.refresh() class Row: """Represents a row in a :class:`Table`. It holds multiple values to be represented through columns. It's its role to prepare data fetched from model instances into ready-to-present-in-a-table fashion. You will do this in :meth:`load`. When you do this, you'll put the result into arbitrary attributes, which will later be fetched by your table for presentation to the user. You can organize your attributes in whatever way you want, but there's a convention you can follow if you want to minimize subclassing and use default behavior: 1. Attribute name = column name. If your attribute is ``foobar``, whenever we refer to ``column_name``, you refer to that attribute with the column name ``foobar``. 2. Public attributes are for *formatted* value, that is, user readable strings. 3. Underscore prefix is the unformatted (computable) value. For example, you could have ``_foobar`` at ``42`` and ``foobar`` at ``"42 seconds"`` (what you present to the user). 4. Unformatted values are used for sorting. 5. If your column name is a python keyword, add an underscore suffix (``from_``). Of course, this is only default behavior. This can be overriden. """ def __init__(self, table: GUITable) -> None: super().__init__() self.table = table def _edit(self) -> None: if self.table.edited is self: return assert self.table.edited is None self.table.edited = self # --- Virtual def can_edit(self) -> bool: """(Virtual) Whether the whole row can be edited. By default, always returns ``True``. This is for the *whole* row. For individual cells, it's :meth:`can_edit_cell`. """ return True def load(self) -> None: """(Virtual/Required) Loads up values from the model to be presented in the table. Usually, our model instances contain values that are not quite ready for display. If you have number formatting, display calculations and other whatnots to perform, you do it here and then you put the result in an arbitrary attribute of the row. """ raise NotImplementedError() def save(self) -> None: """(Virtual/Required) Saves user edits into your model. If your table is editable, this is called when the user commits his changes. Usually, these are typed up stuff, or selected indexes. You have to do proper parsing and reference linking, and save that stuff into your model. """ raise NotImplementedError() def sort_key_for_column(self, column_name: str) -> Any: """(Virtual) Return the value that is to be used to sort by column ``column_name``. By default, looks for an attribute with the same name as ``column_name``, but with an underscore prefix ("unformatted value"). If there's none, tries without the underscore. If there's none, raises ``AttributeError``. """ try: return getattr(self, "_" + column_name) except AttributeError: return getattr(self, column_name) # --- Public def can_edit_cell(self, column_name: str) -> bool: """Returns whether cell for column ``column_name`` can be edited. By the default, the check is done in many steps: 1. We check whether the whole row can be edited with :meth:`can_edit`. If it can't, the cell can't either. 2. If the column doesn't exist as an attribute, we can't edit. 3. If we have an attribute ``can_edit_``, return that. 4. Check if our attribute is a property. If it's not, it's not editable. 5. If our attribute is in fact a property, check whether the property is "settable" (has a ``fset`` method). The cell is editable only if the property is "settable". """ if not self.can_edit(): return False # '_' is in case column is a python keyword if not hasattr(self, column_name): if hasattr(self, column_name + "_"): column_name = column_name + "_" else: return False if hasattr(self, "can_edit_" + column_name): return getattr(self, "can_edit_" + column_name) # If the row has a settable property, we can edit the cell rowclass = self.__class__ prop = getattr(rowclass, column_name, None) if prop is None: return False return bool(getattr(prop, "fset", None)) def get_cell_value(self, attrname: str) -> Any: """Get cell value for ``attrname``. By default, does a simple ``getattr()``, but it is used to allow subclasses to have alternative value storage mechanisms. """ if attrname == "from": attrname = "from_" return getattr(self, attrname) def set_cell_value(self, attrname: str, value: Any) -> None: """Set cell value to ``value`` for ``attrname``. By default, does a simple ``setattr()``, but it is used to allow subclasses to have alternative value storage mechanisms. """ if attrname == "from": attrname = "from_" setattr(self, attrname, value) dupeguru-4.3.1/hscommon/gui/text_field.py000066400000000000000000000066111426171743600205060ustar00rootroot00000000000000# Created On: 2012/01/23 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.gui.base import GUIObject from hscommon.util import nonone class TextFieldView: """Expected interface for :class:`TextField`'s view. *Not actually used in the code. For documentation purposes only.* Our view is expected to sync with :attr:`TextField.text` "both ways", that is, update the model's text when the user types something, but also update the text field when :meth:`refresh` is called. """ def refresh(self): """Refreshes the contents of the input widget. Ensures that the contents of the input widget is actually :attr:`TextField.text`. """ class TextField(GUIObject): """Cross-toolkit text field. Represents a UI element allowing the user to input a text value. Its main attribute is :attr:`text` which acts as the store of the said value. When our model value isn't a string, we have a built-in parsing/formatting mechanism allowing us to directly retrieve/set our non-string value through :attr:`value`. Subclasses :class:`.GUIObject`. Expected view: :class:`TextFieldView`. """ def __init__(self): GUIObject.__init__(self) self._text = "" self._value = None # --- Virtual def _parse(self, text): """(Virtual) Parses ``text`` to put into :attr:`value`. Returns the parsed version of ``text``. Called whenever :attr:`text` changes. """ return text def _format(self, value): """(Virtual) Formats ``value`` to put into :attr:`text`. Returns the formatted version of ``value``. Called whenever :attr:`value` changes. """ return value def _update(self, newvalue): """(Virtual) Called whenever we have a new value. Whenever our text/value store changes to a new value (different from the old one), this method is called. By default, it does nothing but you can override it if you want. """ # --- Override def _view_updated(self): self.view.refresh() # --- Public def refresh(self): """Triggers a view :meth:`~TextFieldView.refresh`.""" self.view.refresh() @property def text(self): """The text that is currently displayed in the widget. *str*. *get/set*. This property can be set. When it is, :meth:`refresh` is called and the view is synced with our value. Always in sync with :attr:`value`. """ return self._text @text.setter def text(self, newtext): self.value = self._parse(nonone(newtext, "")) @property def value(self): """The "parsed" representation of :attr:`text`. *arbitrary type*. *get/set*. By default, it's a mirror of :attr:`text`, but a subclass can override :meth:`_parse` and :meth:`_format` to have anything else. Always in sync with :attr:`text`. """ return self._value @value.setter def value(self, newvalue): if newvalue == self._value: return self._value = newvalue self._text = self._format(newvalue) self._update(self._value) self.refresh() dupeguru-4.3.1/hscommon/gui/tree.py000066400000000000000000000164601426171743600173210ustar00rootroot00000000000000# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from collections.abc import MutableSequence from hscommon.gui.base import GUIObject class Node(MutableSequence): """Pretty bland node implementation to be used in a :class:`Tree`. It has a :attr:`parent`, behaves like a list, its content being its children. Link integrity is somewhat enforced (adding a child to a node will set the child's :attr:`parent`, but that's pretty much as far as we go, integrity-wise. Nodes don't tend to move around much in a GUI tree). We don't even check for infinite node loops. Don't play around these grounds too much. Nodes are designed to be subclassed and given meaningful attributes (those you'll want to display in your tree view), but they all have a :attr:`name`, which is given on initialization. """ def __init__(self, name): self._name = name self._parent = None self._path = None self._children = [] def __repr__(self): return "" % self.name # --- MutableSequence overrides def __delitem__(self, key): self._children.__delitem__(key) def __getitem__(self, key): return self._children.__getitem__(key) def __len__(self): return len(self._children) def __setitem__(self, key, value): self._children.__setitem__(key, value) def append(self, node): self._children.append(node) node._parent = self node._path = None def insert(self, index, node): self._children.insert(index, node) node._parent = self node._path = None # --- Public def clear(self): """Clears the node of all its children.""" del self[:] def find(self, predicate, include_self=True): """Return the first child to match ``predicate``. See :meth:`findall`. """ try: return next(self.findall(predicate, include_self=include_self)) except StopIteration: return None def findall(self, predicate, include_self=True): """Yield all children matching ``predicate``. :param predicate: ``f(node) --> bool`` :param include_self: Whether we can return ``self`` or we return only children. """ if include_self and predicate(self): yield self for child in self: yield from child.findall(predicate, include_self=True) def get_node(self, index_path): """Returns the node at ``index_path``. :param index_path: a list of int indexes leading to our node. See :attr:`path`. """ result = self if index_path: for index in index_path: result = result[index] return result def get_path(self, target_node): """Returns the :attr:`path` of ``target_node``. If ``target_node`` is ``None``, returns ``None``. """ if target_node is None: return None return target_node.path @property def children_count(self): """Same as ``len(self)``.""" return len(self) @property def name(self): """Name for the node, supplied on init.""" return self._name @property def parent(self): """Parent of the node. If ``None``, we have a root node. """ return self._parent @property def path(self): """A list of node indexes leading from the root node to ``self``. The path of a node is always related to its :attr:`root`. It's the sequences of index that we have to take to get to our node, starting from the root. For example, if ``node.path == [1, 2, 3, 4]``, it means that ``node.root[1][2][3][4] is node``. """ if self._path is None: if self._parent is None: self._path = [] else: self._path = self._parent.path + [self._parent.index(self)] return self._path @property def root(self): """Root node of current node. To get it, we recursively follow our :attr:`parent` chain until we have ``None``. """ if self._parent is None: return self else: return self._parent.root class Tree(Node, GUIObject): """Cross-toolkit GUI-enabled tree view. This class is a bit too thin to be used as a tree view controller out of the box and HS apps that subclasses it each add quite a bit of logic to it to make it workable. Making this more usable out of the box is a work in progress. This class is here (in addition to being a :class:`Node`) mostly to handle selection. Subclasses :class:`Node` (it is the root node of all its children) and :class:`.GUIObject`. """ def __init__(self): Node.__init__(self, "") GUIObject.__init__(self) #: Where we store selected nodes (as a list of :class:`Node`) self._selected_nodes = [] # --- Virtual def _select_nodes(self, nodes): """(Virtual) Customize node selection behavior. By default, simply set :attr:`_selected_nodes`. """ self._selected_nodes = nodes # --- Override def _view_updated(self): self.view.refresh() def clear(self): self._selected_nodes = [] Node.clear(self) # --- Public @property def selected_node(self): """Currently selected node. *:class:`Node`*. *get/set*. First of :attr:`selected_nodes`. ``None`` if empty. """ return self._selected_nodes[0] if self._selected_nodes else None @selected_node.setter def selected_node(self, node): if node is not None: self._select_nodes([node]) else: self._select_nodes([]) @property def selected_nodes(self): """List of selected nodes in the tree. *List of :class:`Node`*. *get/set*. We use nodes instead of indexes to store selection because it's simpler when it's time to manage selection of multiple node levels. """ return self._selected_nodes @selected_nodes.setter def selected_nodes(self, nodes): self._select_nodes(nodes) @property def selected_path(self): """Currently selected path. *:attr:`Node.path`*. *get/set*. First of :attr:`selected_paths`. ``None`` if empty. """ return self.get_path(self.selected_node) @selected_path.setter def selected_path(self, index_path): if index_path is not None: self.selected_paths = [index_path] else: self._select_nodes([]) @property def selected_paths(self): """List of selected paths in the tree. *List of :attr:`Node.path`*. *get/set* Computed from :attr:`selected_nodes`. """ return list(map(self.get_path, self._selected_nodes)) @selected_paths.setter def selected_paths(self, index_paths): nodes = [] for path in index_paths: try: nodes.append(self.get_node(path)) except IndexError: pass self._select_nodes(nodes) dupeguru-4.3.1/hscommon/jobprogress/000077500000000000000000000000001426171743600175545ustar00rootroot00000000000000dupeguru-4.3.1/hscommon/jobprogress/__init__.py000066400000000000000000000000001426171743600216530ustar00rootroot00000000000000dupeguru-4.3.1/hscommon/jobprogress/job.py000066400000000000000000000154111426171743600207020ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2004/12/20 # Copyright 2011 Hardcoded Software (http://www.hardcoded.net) # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from typing import Any, Callable, Generator, Iterator, List, Union class JobCancelled(Exception): "The user has cancelled the job" class JobInProgressError(Exception): "A job is already being performed, you can't perform more than one at the same time." class JobCountError(Exception): "The number of jobs started have exceeded the number of jobs allowed" class Job: """Manages a job's progression and return it's progression through a callback. Note that this class is not foolproof. For example, you could call start_subjob, and then call add_progress from the parent job, and nothing would stop you from doing it. However, it would mess your progression because it is the sub job that is supposed to drive the progression. Another example would be to start a subjob, then start another, and call add_progress from the old subjob. Once again, it would mess your progression. There are no stops because it would remove the lightweight aspect of the class (A Job would need to have a Parent instead of just a callback, and the parent could be None. A lot of checks for nothing.). Another one is that nothing stops you from calling add_progress right after SkipJob. """ # ---Magic functions def __init__(self, job_proportions: Union[List[int], int], callback: Callable) -> None: """Initialize the Job with 'jobcount' jobs. Start every job with start_job(). Every time the job progress is updated, 'callback' is called 'callback' takes a 'progress' int param, and a optional 'desc' parameter. Callback must return false if the job must be cancelled. """ if not hasattr(callback, "__call__"): raise TypeError("'callback' MUST be set when creating a Job") if isinstance(job_proportions, int): job_proportions = [1] * job_proportions self._job_proportions = list(job_proportions) self._jobcount = sum(job_proportions) self._callback = callback self._current_job = 0 self._passed_jobs = 0 self._progress = 0 self._currmax = 1 # ---Private def _subjob_callback(self, progress: int, desc: str = "") -> bool: """This is the callback passed to children jobs.""" self.set_progress(progress, desc) return True # if JobCancelled has to be raised, it will be at the highest level def _do_update(self, desc: str) -> None: """Calls the callback function with a % progress as a parameter. The parameter is a int in the 0-100 range. """ if self._current_job: passed_progress = self._passed_jobs * self._currmax current_progress = self._current_job * self._progress total_progress = self._jobcount * self._currmax progress = ((passed_progress + current_progress) * 100) // total_progress else: progress = -1 # indeterminate # It's possible that callback doesn't support a desc arg result = self._callback(progress, desc) if desc else self._callback(progress) if not result: raise JobCancelled() # ---Public def add_progress(self, progress: int = 1, desc: str = "") -> None: self.set_progress(self._progress + progress, desc) def check_if_cancelled(self) -> None: self._do_update("") # TODO type hint iterable def iter_with_progress( self, iterable, desc_format: Union[str, None] = None, every: int = 1, count: Union[int, None] = None ) -> Generator[Any, None, None]: """Iterate through ``iterable`` while automatically adding progress. WARNING: We need our iterable's length. If ``iterable`` is not a sequence (that is, something we can call ``len()`` on), you *have* to specify a count through the ``count`` argument. If ``count`` is ``None``, ``len(iterable)`` is used. """ if count is None: count = len(iterable) desc = "" if desc_format: desc = desc_format % (0, count) self.start_job(count, desc) for i, element in enumerate(iterable, start=1): yield element if i % every == 0: if desc_format: desc = desc_format % (i, count) self.add_progress(progress=every, desc=desc) if desc_format: desc = desc_format % (count, count) self.set_progress(100, desc) def start_job(self, max_progress: int = 100, desc: str = "") -> None: """Begin work on the next job. You must not call start_job more than 'jobcount' (in __init__) times. 'max' is the job units you are to perform. 'desc' is the description of the job. """ self._passed_jobs += self._current_job try: self._current_job = self._job_proportions.pop(0) except IndexError: raise JobCountError() self._progress = 0 self._currmax = max(1, max_progress) self._do_update(desc) def start_subjob(self, job_proportions: Union[List[int], int], desc: str = "") -> "Job": """Starts a sub job. Use this when you want to split a job into multiple smaller jobs. Pretty handy when starting a process where you know how many subjobs you will have, but don't know the work unit count for every of them. returns the Job object """ self.start_job(100, desc) return Job(job_proportions, self._subjob_callback) def set_progress(self, progress: int, desc: str = "") -> None: """Sets the progress of the current job to 'progress', and call the callback """ self._progress = progress if self._progress > self._currmax: self._progress = self._currmax self._do_update(desc) class NullJob: def __init__(self, *args, **kwargs) -> None: # Null job does nothing pass def add_progress(self, *args, **kwargs) -> None: # Null job does nothing pass def check_if_cancelled(self) -> None: # Null job does nothing pass def iter_with_progress(self, sequence, *args, **kwargs) -> Iterator: return iter(sequence) def start_job(self, *args, **kwargs) -> None: # Null job does nothing pass def start_subjob(self, *args, **kwargs) -> "NullJob": return NullJob() def set_progress(self, *args, **kwargs) -> None: # Null job does nothing pass nulljob = NullJob() dupeguru-4.3.1/hscommon/jobprogress/performer.py000066400000000000000000000045051426171743600221330ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2010-11-19 # Copyright 2011 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from threading import Thread import sys from typing import Callable, Tuple, Union from hscommon.jobprogress.job import Job, JobInProgressError, JobCancelled class ThreadedJobPerformer: """Run threaded jobs and track progress. To run a threaded job, first create a job with _create_job(), then call _run_threaded(), with your work function as a parameter. Example: j = self._create_job() self._run_threaded(self.some_work_func, (arg1, arg2, j)) """ _job_running = False last_error = None # --- Protected def create_job(self) -> Job: if self._job_running: raise JobInProgressError() self.last_progress: Union[int, None] = -1 self.last_desc = "" self.job_cancelled = False return Job(1, self._update_progress) def _async_run(self, *args) -> None: target = args[0] args = tuple(args[1:]) self._job_running = True self.last_error = None try: target(*args) except JobCancelled: pass except Exception as e: self.last_error = e self.last_traceback = sys.exc_info()[2] finally: self._job_running = False self.last_progress = None def reraise_if_error(self) -> None: """Reraises the error that happened in the thread if any. Call this after the caller of run_threaded detected that self._job_running returned to False """ if self.last_error is not None: raise self.last_error.with_traceback(self.last_traceback) def _update_progress(self, newprogress: int, newdesc: str = "") -> bool: self.last_progress = newprogress if newdesc: self.last_desc = newdesc return not self.job_cancelled def run_threaded(self, target: Callable, args: Tuple = ()) -> None: if self._job_running: raise JobInProgressError() args = (target,) + args Thread(target=self._async_run, args=args).start() dupeguru-4.3.1/hscommon/loc.py000066400000000000000000000070221426171743600163450ustar00rootroot00000000000000import os import os.path as op import shutil import tempfile from typing import Any, List import polib from hscommon import pygettext LC_MESSAGES = "LC_MESSAGES" def get_langs(folder: str) -> List[str]: return [name for name in os.listdir(folder) if op.isdir(op.join(folder, name))] def files_with_ext(folder: str, ext: str) -> List[str]: return [op.join(folder, fn) for fn in os.listdir(folder) if fn.endswith(ext)] def generate_pot(folders: List[str], outpath: str, keywords: Any, merge: bool = False) -> None: if merge and not op.exists(outpath): merge = False if merge: _, genpath = tempfile.mkstemp() else: genpath = outpath pyfiles = [] for folder in folders: for root, dirs, filenames in os.walk(folder): keep = [fn for fn in filenames if fn.endswith(".py")] pyfiles += [op.join(root, fn) for fn in keep] pygettext.main(pyfiles, outpath=genpath, keywords=keywords) if merge: merge_po_and_preserve(genpath, outpath) try: os.remove(genpath) except Exception: print("Exception while removing temporary folder %s\n", genpath) def compile_all_po(base_folder: str) -> None: langs = get_langs(base_folder) for lang in langs: pofolder = op.join(base_folder, lang, LC_MESSAGES) pofiles = files_with_ext(pofolder, ".po") for pofile in pofiles: p = polib.pofile(pofile) p.save_as_mofile(pofile[:-3] + ".mo") def merge_locale_dir(target: str, mergeinto: str) -> None: langs = get_langs(target) for lang in langs: if not op.exists(op.join(mergeinto, lang)): continue mofolder = op.join(target, lang, LC_MESSAGES) mofiles = files_with_ext(mofolder, ".mo") for mofile in mofiles: shutil.copy(mofile, op.join(mergeinto, lang, LC_MESSAGES)) def merge_pots_into_pos(folder: str) -> None: # We're going to take all pot files in `folder` and for each lang, merge it with the po file # with the same name. potfiles = files_with_ext(folder, ".pot") for potfile in potfiles: refpot = polib.pofile(potfile) refname = op.splitext(op.basename(potfile))[0] for lang in get_langs(folder): po = polib.pofile(op.join(folder, lang, LC_MESSAGES, refname + ".po")) po.merge(refpot) po.save() def merge_po_and_preserve(source: str, dest: str) -> None: # Merges source entries into dest, but keep old entries intact sourcepo = polib.pofile(source) destpo = polib.pofile(dest) for entry in sourcepo: if destpo.find(entry.msgid) is not None: # The entry is already there continue destpo.append(entry) destpo.save() def normalize_all_pos(base_folder: str) -> None: """Normalize the format of .po files in base_folder. When getting POs from external sources, such as Transifex, we end up with spurious diffs because of a difference in the way line wrapping is handled. It wouldn't be a big deal if it happened once, but these spurious diffs keep overwriting each other, and it's annoying. Our PO files will keep polib's format. Call this function to ensure that freshly pulled POs are of the right format before committing them. """ langs = get_langs(base_folder) for lang in langs: pofolder = op.join(base_folder, lang, LC_MESSAGES) pofiles = files_with_ext(pofolder, ".po") for pofile in pofiles: p = polib.pofile(pofile) p.save() dupeguru-4.3.1/hscommon/notify.py000066400000000000000000000064171426171743600171070ustar00rootroot00000000000000# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html """Very simple inter-object notification system. This module is a brain-dead simple notification system involving a :class:`Broadcaster` and a :class:`Listener`. A listener can only listen to one broadcaster. A broadcaster can have multiple listeners. If the listener is connected, whenever the broadcaster calls :meth:`~Broadcaster.notify`, the method with the same name as the broadcasted message is called on the listener. """ from collections import defaultdict from typing import Callable, DefaultDict, List class Broadcaster: """Broadcasts messages that are received by all listeners.""" def __init__(self): self.listeners = set() def add_listener(self, listener: "Listener") -> None: self.listeners.add(listener) def notify(self, msg: str) -> None: """Notify all connected listeners of ``msg``. That means that each listeners will have their method with the same name as ``msg`` called. """ for listener in self.listeners.copy(): # listeners can change during iteration if listener in self.listeners: # disconnected during notification listener.dispatch(msg) def remove_listener(self, listener: "Listener") -> None: self.listeners.discard(listener) class Listener: """A listener is initialized with the broadcaster it's going to listen to. Initially, it is not connected.""" def __init__(self, broadcaster: Broadcaster) -> None: self.broadcaster = broadcaster self._bound_notifications: DefaultDict[str, List[Callable]] = defaultdict(list) def bind_messages(self, messages: str, func: Callable) -> None: """Binds multiple message to the same function. Often, we perform the same thing on multiple messages. Instead of having the same function repeated again and agin in our class, we can use this method to bind multiple messages to the same function. """ for message in messages: self._bound_notifications[message].append(func) def connect(self) -> None: """Connects the listener to its broadcaster.""" self.broadcaster.add_listener(self) def disconnect(self) -> None: """Disconnects the listener from its broadcaster.""" self.broadcaster.remove_listener(self) def dispatch(self, msg: str) -> None: if msg in self._bound_notifications: for func in self._bound_notifications[msg]: func() if hasattr(self, msg): method = getattr(self, msg) method() class Repeater(Broadcaster, Listener): REPEATED_NOTIFICATIONS = None def __init__(self, broadcaster: Broadcaster) -> None: Broadcaster.__init__(self) Listener.__init__(self, broadcaster) def _repeat_message(self, msg: str) -> None: if not self.REPEATED_NOTIFICATIONS or msg in self.REPEATED_NOTIFICATIONS: self.notify(msg) def dispatch(self, msg: str) -> None: Listener.dispatch(self, msg) self._repeat_message(msg) dupeguru-4.3.1/hscommon/path.py000066400000000000000000000035441426171743600165310ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2006/02/21 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import logging from functools import wraps from inspect import signature from pathlib import Path def pathify(f): """Ensure that every annotated :class:`Path` arguments are actually paths. When a function is decorated with ``@pathify``, every argument with annotated as Path will be converted to a Path if it wasn't already. Example:: @pathify def foo(path: Path, otherarg): return path.listdir() Calling ``foo('/bar', 0)`` will convert ``'/bar'`` to ``Path('/bar')``. """ sig = signature(f) pindexes = {i for i, p in enumerate(sig.parameters.values()) if p.annotation is Path} pkeys = {k: v for k, v in sig.parameters.items() if v.annotation is Path} def path_or_none(p): return None if p is None else Path(p) @wraps(f) def wrapped(*args, **kwargs): args = tuple((path_or_none(a) if i in pindexes else a) for i, a in enumerate(args)) kwargs = {k: (path_or_none(v) if k in pkeys else v) for k, v in kwargs.items()} return f(*args, **kwargs) return wrapped def log_io_error(func): """Catches OSError, IOError and WindowsError and log them""" @wraps(func) def wrapper(path, *args, **kwargs): try: return func(path, *args, **kwargs) except OSError as e: msg = 'Error "{0}" during operation "{1}" on "{2}": "{3}"' classname = e.__class__.__name__ funcname = func.__name__ logging.warning(msg.format(classname, funcname, str(path), str(e))) return wrapper dupeguru-4.3.1/hscommon/plat.py000066400000000000000000000012431426171743600165270ustar00rootroot00000000000000# Created On: 2011/09/22 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html # Yes, I know, there's the 'platform' unit for this kind of stuff, but the thing is that I got a # crash on startup once simply for importing this module and since then I don't trust it. One day, # I'll investigate the cause of that crash further. import sys ISWINDOWS = sys.platform == "win32" ISOSX = sys.platform == "darwin" ISLINUX = sys.platform.startswith("linux") dupeguru-4.3.1/hscommon/pygettext.py000066400000000000000000000325541426171743600176350ustar00rootroot00000000000000# This module was taken from CPython's Tools/i18n and dirtily hacked to bypass the need for cmdline # invocation. # Originally written by Barry Warsaw # # Minimally patched to make it even more xgettext compatible # by Peter Funk # # 2002-11-22 Jürgen Hermann # Added checks that _() only contains string literals, and # command line args are resolved to module lists, i.e. you # can now pass a filename, a module or package name, or a # directory (including globbing chars, important for Win32). # Made docstring fit in 80 chars wide displays using pydoc. # import os import imp import sys import glob import token import tokenize __version__ = "1.5" default_keywords = ["_"] DEFAULTKEYWORDS = ", ".join(default_keywords) EMPTYSTRING = "" # The normal pot-file header. msgmerge and Emacs's po-mode work better if it's # there. pot_header = """ msgid "" msgstr "" "Content-Type: text/plain; charset=utf-8\\n" "Content-Transfer-Encoding: utf-8\\n" """ def usage(code, msg=""): print(__doc__ % globals(), file=sys.stderr) if msg: print(msg, file=sys.stderr) sys.exit(code) escapes = [] def make_escapes(pass_iso8859): global escapes if pass_iso8859: # Allow iso-8859 characters to pass through so that e.g. 'msgid # "H?he"' would result not result in 'msgid "H\366he"'. Otherwise we # escape any character outside the 32..126 range. mod = 128 else: mod = 256 for i in range(256): if 32 <= (i % mod) <= 126: escapes.append(chr(i)) else: escapes.append("\\%03o" % i) escapes[ord("\\")] = "\\\\" escapes[ord("\t")] = "\\t" escapes[ord("\r")] = "\\r" escapes[ord("\n")] = "\\n" escapes[ord('"')] = '\\"' def escape(s): global escapes s = list(s) for i in range(len(s)): s[i] = escapes[ord(s[i])] return EMPTYSTRING.join(s) def safe_eval(s): # unwrap quotes, safely return eval(s, {"__builtins__": {}}, {}) def normalize(s): # This converts the various Python string types into a format that is # appropriate for .po files, namely much closer to C style. lines = s.split("\n") if len(lines) == 1: s = '"' + escape(s) + '"' else: if not lines[-1]: del lines[-1] lines[-1] = lines[-1] + "\n" for i in range(len(lines)): lines[i] = escape(lines[i]) lineterm = '\\n"\n"' s = '""\n"' + lineterm.join(lines) + '"' return s def containsAny(str, set): """Check whether 'str' contains ANY of the chars in 'set'""" return 1 in [c in str for c in set] def _visit_pyfiles(list, dirname, names): """Helper for getFilesForName().""" # get extension for python source files if "_py_ext" not in globals(): global _py_ext _py_ext = [triple[0] for triple in imp.get_suffixes() if triple[2] == imp.PY_SOURCE][0] # don't recurse into CVS directories if "CVS" in names: names.remove("CVS") # add all *.py files to list list.extend([os.path.join(dirname, file) for file in names if os.path.splitext(file)[1] == _py_ext]) def _get_modpkg_path(dotted_name, pathlist=None): """Get the filesystem path for a module or a package. Return the file system path to a file for a module, and to a directory for a package. Return None if the name is not found, or is a builtin or extension module. """ # split off top-most name parts = dotted_name.split(".", 1) if len(parts) > 1: # we have a dotted path, import top-level package try: file, pathname, description = imp.find_module(parts[0], pathlist) if file: file.close() except ImportError: return None # check if it's indeed a package if description[2] == imp.PKG_DIRECTORY: # recursively handle the remaining name parts pathname = _get_modpkg_path(parts[1], [pathname]) else: pathname = None else: # plain name try: file, pathname, description = imp.find_module(dotted_name, pathlist) if file: file.close() if description[2] not in [imp.PY_SOURCE, imp.PKG_DIRECTORY]: pathname = None except ImportError: pathname = None return pathname def getFilesForName(name): """Get a list of module files for a filename, a module or package name, or a directory. """ if not os.path.exists(name): # check for glob chars if containsAny(name, "*?[]"): files = glob.glob(name) file_list = [] for file in files: file_list.extend(getFilesForName(file)) return file_list # try to find module or package name = _get_modpkg_path(name) if not name: return [] if os.path.isdir(name): # find all python files in directory file_list = [] os.walk(name, _visit_pyfiles, file_list) return file_list elif os.path.exists(name): # a single file return [name] return [] class TokenEater: def __init__(self, options): self.__options = options self.__messages = {} self.__state = self.__waiting self.__data = [] self.__lineno = -1 self.__freshmodule = 1 self.__curfile = None def __call__(self, ttype, tstring, stup, etup, line): # dispatch # import token # print >> sys.stderr, 'ttype:', token.tok_name[ttype], \ # 'tstring:', tstring self.__state(ttype, tstring, stup[0]) def __waiting(self, ttype, tstring, lineno): opts = self.__options # Do docstring extractions, if enabled if opts.docstrings and not opts.nodocstrings.get(self.__curfile): # module docstring? if self.__freshmodule: if ttype == tokenize.STRING: self.__addentry(safe_eval(tstring), lineno, isdocstring=1) self.__freshmodule = 0 elif ttype not in (tokenize.COMMENT, tokenize.NL): self.__freshmodule = 0 return # class docstring? if ttype == tokenize.NAME and tstring in ("class", "def"): self.__state = self.__suiteseen return if ttype == tokenize.NAME and tstring in opts.keywords: self.__state = self.__keywordseen def __suiteseen(self, ttype, tstring, lineno): # ignore anything until we see the colon if ttype == tokenize.OP and tstring == ":": self.__state = self.__suitedocstring def __suitedocstring(self, ttype, tstring, lineno): # ignore any intervening noise if ttype == tokenize.STRING: self.__addentry(safe_eval(tstring), lineno, isdocstring=1) self.__state = self.__waiting elif ttype not in (tokenize.NEWLINE, tokenize.INDENT, tokenize.COMMENT): # there was no class docstring self.__state = self.__waiting def __keywordseen(self, ttype, tstring, lineno): if ttype == tokenize.OP and tstring == "(": self.__data = [] self.__lineno = lineno self.__state = self.__openseen else: self.__state = self.__waiting def __openseen(self, ttype, tstring, lineno): if ttype == tokenize.OP and tstring == ")": # We've seen the last of the translatable strings. Record the # line number of the first line of the strings and update the list # of messages seen. Reset state for the next batch. If there # were no strings inside _(), then just ignore this entry. if self.__data: self.__addentry(EMPTYSTRING.join(self.__data)) self.__state = self.__waiting elif ttype == tokenize.STRING: self.__data.append(safe_eval(tstring)) elif ttype not in [ tokenize.COMMENT, token.INDENT, token.DEDENT, token.NEWLINE, tokenize.NL, ]: # warn if we see anything else than STRING or whitespace print( '*** %(file)s:%(lineno)s: Seen unexpected token "%(token)s"' % {"token": tstring, "file": self.__curfile, "lineno": self.__lineno}, file=sys.stderr, ) self.__state = self.__waiting def __addentry(self, msg, lineno=None, isdocstring=0): if lineno is None: lineno = self.__lineno if msg not in self.__options.toexclude: entry = (self.__curfile, lineno) self.__messages.setdefault(msg, {})[entry] = isdocstring def set_filename(self, filename): self.__curfile = filename self.__freshmodule = 1 def write(self, fp): options = self.__options # The time stamp in the header doesn't have the same format as that # generated by xgettext... print(pot_header, file=fp) # Sort the entries. First sort each particular entry's keys, then # sort all the entries by their first item. reverse = {} for k, v in self.__messages.items(): keys = sorted(v.keys()) reverse.setdefault(tuple(keys), []).append((k, v)) rkeys = sorted(reverse.keys()) for rkey in rkeys: rentries = reverse[rkey] rentries.sort() for k, v in rentries: # If the entry was gleaned out of a docstring, then add a # comment stating so. This is to aid translators who may wish # to skip translating some unimportant docstrings. isdocstring = any(v.values()) # k is the message string, v is a dictionary-set of (filename, # lineno) tuples. We want to sort the entries in v first by # file name and then by line number. v = sorted(v.keys()) if not options.writelocations: pass # location comments are different b/w Solaris and GNU: elif options.locationstyle == options.SOLARIS: for filename, lineno in v: d = {"filename": filename, "lineno": lineno} print("# File: %(filename)s, line: %(lineno)d" % d, file=fp) elif options.locationstyle == options.GNU: # fit as many locations on one line, as long as the # resulting line length doesn't exceeds 'options.width' locline = "#:" for filename, lineno in v: d = {"filename": filename, "lineno": lineno} s = " %(filename)s:%(lineno)d" % d if len(locline) + len(s) <= options.width: locline = locline + s else: print(locline, file=fp) locline = "#:" + s if len(locline) > 2: print(locline, file=fp) if isdocstring: print("#, docstring", file=fp) print("msgid", normalize(k), file=fp) print('msgstr ""\n', file=fp) def main(source_files, outpath, keywords=None): global default_keywords # for holding option values class Options: # constants GNU = 1 SOLARIS = 2 # defaults extractall = 0 # FIXME: currently this option has no effect at all. escape = 0 keywords = [] outfile = "messages.pot" writelocations = 1 locationstyle = GNU verbose = 0 width = 78 excludefilename = "" docstrings = 0 nodocstrings = {} options = Options() options.outfile = outpath if keywords: options.keywords = keywords # calculate escapes make_escapes(options.escape) # calculate all keywords options.keywords.extend(default_keywords) # initialize list of strings to exclude if options.excludefilename: try: fp = open(options.excludefilename, encoding="utf-8") options.toexclude = fp.readlines() fp.close() except OSError: print( "Can't read --exclude-file: %s" % options.excludefilename, file=sys.stderr, ) sys.exit(1) else: options.toexclude = [] # slurp through all the files eater = TokenEater(options) for filename in source_files: if options.verbose: print("Working on %s" % filename) fp = open(filename, encoding="utf-8") closep = 1 try: eater.set_filename(filename) try: tokens = tokenize.generate_tokens(fp.readline) for _token in tokens: eater(*_token) except tokenize.TokenError as e: print( "%s: %s, line %d, column %d" % (e.args[0], filename, e.args[1][0], e.args[1][1]), file=sys.stderr, ) finally: if closep: fp.close() fp = open(options.outfile, "w", encoding="utf-8") closep = 1 try: eater.write(fp) finally: if closep: fp.close() dupeguru-4.3.1/hscommon/sphinxgen.py000066400000000000000000000056311426171743600175770ustar00rootroot00000000000000# Copyright 2018 Virgil Dupras # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from pathlib import Path import re from typing import Callable, Dict, Union from hscommon.build import read_changelog_file, filereplace from sphinx.cmd.build import build_main as sphinx_build CHANGELOG_FORMAT = """ {version} ({date}) ---------------------- {description} """ def tixgen(tixurl: str) -> Callable[[str], str]: """This is a filter *generator*. tixurl is a url pattern for the tix with a {0} placeholder for the tix # """ urlpattern = tixurl.format("\\1") # will be replaced buy the content of the first group in re R = re.compile(r"#(\d+)") repl = f"`#\\1 <{urlpattern}>`__" return lambda text: R.sub(repl, text) def gen( basepath: Path, destpath: Path, changelogpath: Path, tixurl: str, confrepl: Union[Dict[str, str], None] = None, confpath: Union[Path, None] = None, changelogtmpl: Union[Path, None] = None, ) -> None: """Generate sphinx docs with all bells and whistles. basepath: The base sphinx source path. destpath: The final path of html files changelogpath: The path to the changelog file to insert in changelog.rst. tixurl: The URL (with one formattable argument for the tix number) to the ticket system. confrepl: Dictionary containing replacements that have to be made in conf.py. {name: replacement} """ if confrepl is None: confrepl = {} if confpath is None: confpath = Path(basepath, "conf.tmpl") if changelogtmpl is None: changelogtmpl = Path(basepath, "changelog.tmpl") changelog = read_changelog_file(changelogpath) tix = tixgen(tixurl) rendered_logs = [] for log in changelog: description = tix(log["description"]) # The format of the changelog descriptions is in markdown, but since we only use bulled list # and links, it's not worth depending on the markdown package. A simple regexp suffice. description = re.sub(r"\[(.*?)\]\((.*?)\)", "`\\1 <\\2>`__", description) rendered = CHANGELOG_FORMAT.format(version=log["version"], date=log["date_str"], description=description) rendered_logs.append(rendered) confrepl["version"] = changelog[0]["version"] changelog_out = Path(basepath, "changelog.rst") filereplace(changelogtmpl, changelog_out, changelog="\n".join(rendered_logs)) if Path(confpath).exists(): conf_out = Path(basepath, "conf.py") filereplace(confpath, conf_out, **confrepl) # Call the sphinx_build function, which is the same as doing sphinx-build from cli try: sphinx_build([str(basepath), str(destpath)]) except SystemExit: print("Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit") dupeguru-4.3.1/hscommon/tests/000077500000000000000000000000001426171743600163575ustar00rootroot00000000000000dupeguru-4.3.1/hscommon/tests/__init__.py000066400000000000000000000000001426171743600204560ustar00rootroot00000000000000dupeguru-4.3.1/hscommon/tests/conflict_test.py000066400000000000000000000105431426171743600215740ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2008-01-08 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import pytest from hscommon.conflict import ( get_conflicted_name, get_unconflicted_name, is_conflicted, smart_copy, smart_move, ) from pathlib import Path from hscommon.testutil import eq_ class TestCaseGetConflictedName: def test_simple(self): name = get_conflicted_name(["bar"], "bar") eq_("[000] bar", name) name = get_conflicted_name(["bar", "[000] bar"], "bar") eq_("[001] bar", name) def test_no_conflict(self): name = get_conflicted_name(["bar"], "foobar") eq_("foobar", name) def test_fourth_digit(self): # This test is long because every time we have to add a conflicted name, # a test must be made for every other conflicted name existing... # Anyway, this has very few chances to happen. names = ["bar"] + ["[%03d] bar" % i for i in range(1000)] name = get_conflicted_name(names, "bar") eq_("[1000] bar", name) def test_auto_unconflict(self): # Automatically unconflict the name if it's already conflicted. name = get_conflicted_name([], "[000] foobar") eq_("foobar", name) name = get_conflicted_name(["bar"], "[001] bar") eq_("[000] bar", name) class TestCaseGetUnconflictedName: def test_main(self): eq_("foobar", get_unconflicted_name("[000] foobar")) eq_("foobar", get_unconflicted_name("[9999] foobar")) eq_("[000]foobar", get_unconflicted_name("[000]foobar")) eq_("[000a] foobar", get_unconflicted_name("[000a] foobar")) eq_("foobar", get_unconflicted_name("foobar")) eq_("foo [000] bar", get_unconflicted_name("foo [000] bar")) class TestCaseIsConflicted: def test_main(self): assert is_conflicted("[000] foobar") assert is_conflicted("[9999] foobar") assert not is_conflicted("[000]foobar") assert not is_conflicted("[000a] foobar") assert not is_conflicted("foobar") assert not is_conflicted("foo [000] bar") class TestCaseMoveCopy: @pytest.fixture def do_setup(self, request): tmpdir = request.getfixturevalue("tmpdir") self.path = Path(str(tmpdir)) self.path.joinpath("foo").touch() self.path.joinpath("bar").touch() self.path.joinpath("dir").mkdir() def test_move_no_conflict(self, do_setup): smart_move(self.path.joinpath("foo"), self.path.joinpath("baz")) assert self.path.joinpath("baz").exists() assert not self.path.joinpath("foo").exists() def test_copy_no_conflict(self, do_setup): # No need to duplicate the rest of the tests... Let's just test on move smart_copy(self.path.joinpath("foo"), self.path.joinpath("baz")) assert self.path.joinpath("baz").exists() assert self.path.joinpath("foo").exists() def test_move_no_conflict_dest_is_dir(self, do_setup): smart_move(self.path.joinpath("foo"), self.path.joinpath("dir")) assert self.path.joinpath("dir", "foo").exists() assert not self.path.joinpath("foo").exists() def test_move_conflict(self, do_setup): smart_move(self.path.joinpath("foo"), self.path.joinpath("bar")) assert self.path.joinpath("[000] bar").exists() assert not self.path.joinpath("foo").exists() def test_move_conflict_dest_is_dir(self, do_setup): smart_move(self.path.joinpath("foo"), self.path.joinpath("dir")) smart_move(self.path.joinpath("bar"), self.path.joinpath("foo")) smart_move(self.path.joinpath("foo"), self.path.joinpath("dir")) assert self.path.joinpath("dir", "foo").exists() assert self.path.joinpath("dir", "[000] foo").exists() assert not self.path.joinpath("foo").exists() assert not self.path.joinpath("bar").exists() def test_copy_folder(self, tmpdir): # smart_copy also works on folders path = Path(str(tmpdir)) path.joinpath("foo").mkdir() path.joinpath("bar").mkdir() smart_copy(path.joinpath("foo"), path.joinpath("bar")) # no crash assert path.joinpath("[000] bar").exists() dupeguru-4.3.1/hscommon/tests/notify_test.py000066400000000000000000000105341426171743600213030ustar00rootroot00000000000000# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.testutil import eq_ from hscommon.notify import Broadcaster, Listener, Repeater class HelloListener(Listener): def __init__(self, broadcaster): Listener.__init__(self, broadcaster) self.hello_count = 0 def hello(self): self.hello_count += 1 class HelloRepeater(Repeater): def __init__(self, broadcaster): Repeater.__init__(self, broadcaster) self.hello_count = 0 def hello(self): self.hello_count += 1 def create_pair(): b = Broadcaster() listener = HelloListener(b) return b, listener def test_disconnect_during_notification(): # When a listener disconnects another listener the other listener will not receive a # notification. # This whole complication scheme below is because the order of the notification is not # guaranteed. We could disconnect everything from self.broadcaster.listeners, but this # member is supposed to be private. Hence, the '.other' scheme class Disconnecter(Listener): def __init__(self, broadcaster): Listener.__init__(self, broadcaster) self.hello_count = 0 def hello(self): self.hello_count += 1 self.other.disconnect() broadcaster = Broadcaster() first = Disconnecter(broadcaster) second = Disconnecter(broadcaster) first.other, second.other = second, first first.connect() second.connect() broadcaster.notify("hello") # only one of them was notified eq_(first.hello_count + second.hello_count, 1) def test_disconnect(): # After a disconnect, the listener doesn't hear anything. b, listener = create_pair() listener.connect() listener.disconnect() b.notify("hello") eq_(listener.hello_count, 0) def test_disconnect_when_not_connected(): # When disconnecting an already disconnected listener, nothing happens. b, listener = create_pair() listener.disconnect() def test_not_connected_on_init(): # A listener is not initialized connected. b, listener = create_pair() b.notify("hello") eq_(listener.hello_count, 0) def test_notify(): # The listener listens to the broadcaster. b, listener = create_pair() listener.connect() b.notify("hello") eq_(listener.hello_count, 1) def test_reconnect(): # It's possible to reconnect a listener after disconnection. b, listener = create_pair() listener.connect() listener.disconnect() listener.connect() b.notify("hello") eq_(listener.hello_count, 1) def test_repeater(): b = Broadcaster() r = HelloRepeater(b) listener = HelloListener(r) r.connect() listener.connect() b.notify("hello") eq_(r.hello_count, 1) eq_(listener.hello_count, 1) def test_repeater_with_repeated_notifications(): # If REPEATED_NOTIFICATIONS is not empty, only notifs in this set are repeated (but they're # still dispatched locally). class MyRepeater(HelloRepeater): REPEATED_NOTIFICATIONS = {"hello"} def __init__(self, broadcaster): HelloRepeater.__init__(self, broadcaster) self.foo_count = 0 def foo(self): self.foo_count += 1 b = Broadcaster() r = MyRepeater(b) listener = HelloListener(r) r.connect() listener.connect() b.notify("hello") b.notify("foo") # if the repeater repeated this notif, we'd get a crash on HelloListener eq_(r.hello_count, 1) eq_(listener.hello_count, 1) eq_(r.foo_count, 1) def test_repeater_doesnt_try_to_dispatch_to_self_if_it_cant(): # if a repeater doesn't handle a particular message, it doesn't crash and simply repeats it. b = Broadcaster() r = Repeater(b) # doesnt handle hello listener = HelloListener(r) r.connect() listener.connect() b.notify("hello") # no crash eq_(listener.hello_count, 1) def test_bind_messages(): b, listener = create_pair() listener.bind_messages({"foo", "bar"}, listener.hello) listener.connect() b.notify("foo") b.notify("bar") b.notify("hello") # Normal dispatching still work eq_(listener.hello_count, 3) dupeguru-4.3.1/hscommon/tests/path_test.py000066400000000000000000000015311426171743600207240ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2006/02/21 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.path import pathify from pathlib import Path def test_pathify(): @pathify def foo(a: Path, b, c: Path): return a, b, c a, b, c = foo("foo", 0, c=Path("bar")) assert isinstance(a, Path) assert a == Path("foo") assert b == 0 assert isinstance(c, Path) assert c == Path("bar") def test_pathify_preserve_none(): # @pathify preserves None value and doesn't try to return a Path @pathify def foo(a: Path): return a a = foo(None) assert a is None dupeguru-4.3.1/hscommon/tests/selectable_list_test.py000066400000000000000000000050211426171743600231240ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2011-09-06 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.testutil import eq_, callcounter, CallLogger from hscommon.gui.selectable_list import SelectableList, GUISelectableList def test_in(): # When a SelectableList is in a list, doing "in list" with another instance returns false, even # if they're the same as lists. sl = SelectableList() some_list = [sl] assert SelectableList() not in some_list def test_selection_range(): # selection is correctly adjusted on deletion sl = SelectableList(["foo", "bar", "baz"]) sl.selected_index = 3 eq_(sl.selected_index, 2) del sl[2] eq_(sl.selected_index, 1) def test_update_selection_called(): # _update_selection_is called after a change in selection. However, we only do so on select() # calls. I follow the old behavior of the Table class. At the moment, I don't quite remember # why there was a specific select() method for triggering _update_selection(), but I think I # remember there was a reason, so I keep it that way. sl = SelectableList(["foo", "bar"]) sl._update_selection = callcounter() sl.select(1) eq_(sl._update_selection.callcount, 1) sl.selected_index = 0 eq_(sl._update_selection.callcount, 1) # no call def test_guicalls(): # A GUISelectableList appropriately calls its view. sl = GUISelectableList(["foo", "bar"]) sl.view = CallLogger() sl.view.check_gui_calls(["refresh"]) # Upon setting the view, we get a call to refresh() sl[1] = "baz" sl.view.check_gui_calls(["refresh"]) sl.append("foo") sl.view.check_gui_calls(["refresh"]) del sl[2] sl.view.check_gui_calls(["refresh"]) sl.remove("baz") sl.view.check_gui_calls(["refresh"]) sl.insert(0, "foo") sl.view.check_gui_calls(["refresh"]) sl.select(1) sl.view.check_gui_calls(["update_selection"]) # XXX We have to give up on this for now because of a breakage it causes in the tables. # sl.select(1) # don't update when selection stays the same # gui.check_gui_calls([]) def test_search_by_prefix(): sl = SelectableList(["foo", "bAr", "baZ"]) eq_(sl.search_by_prefix("b"), 1) eq_(sl.search_by_prefix("BA"), 1) eq_(sl.search_by_prefix("BAZ"), 2) eq_(sl.search_by_prefix("BAZZ"), -1) dupeguru-4.3.1/hscommon/tests/table_test.py000066400000000000000000000235011426171743600210600ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2008-08-12 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.testutil import CallLogger, eq_ from hscommon.gui.table import Table, GUITable, Row class TestRow(Row): __test__ = False def __init__(self, table, index, is_new=False): Row.__init__(self, table) self.is_new = is_new self._index = index def load(self): # Does nothing for test pass def save(self): self.is_new = False @property def index(self): return self._index class TestGUITable(GUITable): __test__ = False def __init__(self, rowcount, viewclass=CallLogger): GUITable.__init__(self) self.view = viewclass() self.view.model = self self.rowcount = rowcount self.updated_rows = None def _do_add(self): return TestRow(self, len(self), is_new=True), len(self) def _is_edited_new(self): return self.edited is not None and self.edited.is_new def _fill(self): for i in range(self.rowcount): self.append(TestRow(self, i)) def _update_selection(self): self.updated_rows = self.selected_rows[:] def table_with_footer(): table = Table() table.append(TestRow(table, 0)) footer = TestRow(table, 1) table.footer = footer return table, footer def table_with_header(): table = Table() table.append(TestRow(table, 1)) header = TestRow(table, 0) table.header = header return table, header # --- Tests def test_allow_edit_when_attr_is_property_with_fset(): # When a row has a property that has a fset, by default, make that cell editable. class TestRow(Row): @property def foo(self): # property only for existence checks pass @property def bar(self): # property only for existence checks pass @bar.setter def bar(self, value): # setter only for existence checks pass row = TestRow(Table()) assert row.can_edit_cell("bar") assert not row.can_edit_cell("foo") assert not row.can_edit_cell("baz") # doesn't exist, can't edit def test_can_edit_prop_has_priority_over_fset_checks(): # When a row has a cen_edit_* property, it's the result of that property that is used, not the # result of a fset check. class TestRow(Row): @property def bar(self): # property only for existence checks pass @bar.setter def bar(self, value): # setter only for existence checks pass can_edit_bar = False row = TestRow(Table()) assert not row.can_edit_cell("bar") def test_in(): # When a table is in a list, doing "in list" with another instance returns false, even if # they're the same as lists. table = Table() some_list = [table] assert Table() not in some_list def test_footer_del_all(): # Removing all rows doesn't crash when doing the footer check. table, footer = table_with_footer() del table[:] assert table.footer is None def test_footer_del_row(): # Removing the footer row sets it to None table, footer = table_with_footer() del table[-1] assert table.footer is None eq_(len(table), 1) def test_footer_is_appened_to_table(): # A footer is appended at the table's bottom table, footer = table_with_footer() eq_(len(table), 2) assert table[1] is footer def test_footer_remove(): # remove() on footer sets it to None table, footer = table_with_footer() table.remove(footer) assert table.footer is None def test_footer_replaces_old_footer(): table, footer = table_with_footer() other = Row(table) table.footer = other assert table.footer is other eq_(len(table), 2) assert table[1] is other def test_footer_rows_and_row_count(): # rows() and row_count() ignore footer. table, footer = table_with_footer() eq_(table.row_count, 1) eq_(table.rows, table[:-1]) def test_footer_setting_to_none_removes_old_one(): table, footer = table_with_footer() table.footer = None assert table.footer is None eq_(len(table), 1) def test_footer_stays_there_on_append(): # Appending another row puts it above the footer table, footer = table_with_footer() table.append(Row(table)) eq_(len(table), 3) assert table[2] is footer def test_footer_stays_there_on_insert(): # Inserting another row puts it above the footer table, footer = table_with_footer() table.insert(3, Row(table)) eq_(len(table), 3) assert table[2] is footer def test_header_del_all(): # Removing all rows doesn't crash when doing the header check. table, header = table_with_header() del table[:] assert table.header is None def test_header_del_row(): # Removing the header row sets it to None table, header = table_with_header() del table[0] assert table.header is None eq_(len(table), 1) def test_header_is_inserted_in_table(): # A header is inserted at the table's top table, header = table_with_header() eq_(len(table), 2) assert table[0] is header def test_header_remove(): # remove() on header sets it to None table, header = table_with_header() table.remove(header) assert table.header is None def test_header_replaces_old_header(): table, header = table_with_header() other = Row(table) table.header = other assert table.header is other eq_(len(table), 2) assert table[0] is other def test_header_rows_and_row_count(): # rows() and row_count() ignore header. table, header = table_with_header() eq_(table.row_count, 1) eq_(table.rows, table[1:]) def test_header_setting_to_none_removes_old_one(): table, header = table_with_header() table.header = None assert table.header is None eq_(len(table), 1) def test_header_stays_there_on_insert(): # Inserting another row at the top puts it below the header table, header = table_with_header() table.insert(0, Row(table)) eq_(len(table), 3) assert table[0] is header def test_refresh_view_on_refresh(): # If refresh_view is not False, we refresh the table's view on refresh() table = TestGUITable(1) table.refresh() table.view.check_gui_calls(["refresh"]) table.view.clear_calls() table.refresh(refresh_view=False) table.view.check_gui_calls([]) def test_restore_selection(): # By default, after a refresh, selection goes on the last row table = TestGUITable(10) table.refresh() eq_(table.selected_indexes, [9]) def test_restore_selection_after_cancel_edits(): # _restore_selection() is called after cancel_edits(). Previously, only _update_selection would # be called. class MyTable(TestGUITable): def _restore_selection(self, previous_selection): self.selected_indexes = [6] table = MyTable(10) table.refresh() table.add() table.cancel_edits() eq_(table.selected_indexes, [6]) def test_restore_selection_with_previous_selection(): # By default, we try to restore the selection that was there before a refresh table = TestGUITable(10) table.refresh() table.selected_indexes = [2, 4] table.refresh() eq_(table.selected_indexes, [2, 4]) def test_restore_selection_custom(): # After a _fill() called, the virtual _restore_selection() is called so that it's possible for a # GUITable subclass to customize its post-refresh selection behavior. class MyTable(TestGUITable): def _restore_selection(self, previous_selection): self.selected_indexes = [6] table = MyTable(10) table.refresh() eq_(table.selected_indexes, [6]) def test_row_cell_value(): # *_cell_value() correctly mangles attrnames that are Python reserved words. row = Row(Table()) row.from_ = "foo" eq_(row.get_cell_value("from"), "foo") row.set_cell_value("from", "bar") eq_(row.get_cell_value("from"), "bar") def test_sort_table_also_tries_attributes_without_underscores(): # When determining a sort key, after having unsuccessfully tried the attribute with the, # underscore, try the one without one. table = Table() row1 = Row(table) row1._foo = "a" # underscored attr must be checked first row1.foo = "b" row1.bar = "c" row2 = Row(table) row2._foo = "b" row2.foo = "a" row2.bar = "b" table.append(row1) table.append(row2) table.sort_by("foo") assert table[0] is row1 assert table[1] is row2 table.sort_by("bar") assert table[0] is row2 assert table[1] is row1 def test_sort_table_updates_selection(): table = TestGUITable(10) table.refresh() table.select([2, 4]) table.sort_by("index", desc=True) # Now, the updated rows should be 7 and 5 eq_(len(table.updated_rows), 2) r1, r2 = table.updated_rows eq_(r1.index, 7) eq_(r2.index, 5) def test_sort_table_with_footer(): # Sorting a table with a footer keeps it at the bottom table, footer = table_with_footer() table.sort_by("index", desc=True) assert table[-1] is footer def test_sort_table_with_header(): # Sorting a table with a header keeps it at the top table, header = table_with_header() table.sort_by("index", desc=True) assert table[0] is header def test_add_with_view_that_saves_during_refresh(): # Calling save_edits during refresh() called by add() is ignored. class TableView(CallLogger): def refresh(self): self.model.save_edits() table = TestGUITable(10, viewclass=TableView) table.add() assert table.edited is not None # still in edit mode dupeguru-4.3.1/hscommon/tests/tree_test.py000066400000000000000000000065631426171743600207410ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2010-02-12 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from hscommon.testutil import eq_ from hscommon.gui.tree import Tree, Node def tree_with_some_nodes(): t = Tree() t.append(Node("foo")) t.append(Node("bar")) t.append(Node("baz")) t[0].append(Node("sub1")) t[0].append(Node("sub2")) return t def test_selection(): t = tree_with_some_nodes() assert t.selected_node is None eq_(t.selected_nodes, []) assert t.selected_path is None eq_(t.selected_paths, []) def test_select_one_node(): t = tree_with_some_nodes() t.selected_node = t[0][0] assert t.selected_node is t[0][0] eq_(t.selected_nodes, [t[0][0]]) eq_(t.selected_path, [0, 0]) eq_(t.selected_paths, [[0, 0]]) def test_select_one_path(): t = tree_with_some_nodes() t.selected_path = [0, 1] assert t.selected_node is t[0][1] def test_select_multiple_nodes(): t = tree_with_some_nodes() t.selected_nodes = [t[0], t[1]] eq_(t.selected_paths, [[0], [1]]) def test_select_multiple_paths(): t = tree_with_some_nodes() t.selected_paths = [[0], [1]] eq_(t.selected_nodes, [t[0], t[1]]) def test_select_none_path(): # setting selected_path to None clears the selection t = Tree() t.selected_path = None assert t.selected_path is None def test_select_none_node(): # setting selected_node to None clears the selection t = Tree() t.selected_node = None eq_(t.selected_nodes, []) def test_clear_removes_selection(): # When clearing a tree, we want to clear the selection as well or else we end up with a crash # when calling selected_paths. t = tree_with_some_nodes() t.selected_path = [0] t.clear() assert t.selected_node is None def test_selection_override(): # All selection changed pass through the _select_node() method so it's easy for subclasses to # customize the tree's behavior. class MyTree(Tree): called = False def _select_nodes(self, nodes): self.called = True t = MyTree() t.selected_paths = [] assert t.called t.called = False t.selected_node = None assert t.called def test_findall(): t = tree_with_some_nodes() r = t.findall(lambda n: n.name.startswith("sub")) eq_(set(r), {t[0][0], t[0][1]}) def test_findall_dont_include_self(): # When calling findall with include_self=False, the node itself is never evaluated. t = tree_with_some_nodes() del t._name # so that if the predicate is called on `t`, we crash r = t.findall(lambda n: not n.name.startswith("sub"), include_self=False) # no crash eq_(set(r), {t[0], t[1], t[2]}) def test_find_dont_include_self(): # When calling find with include_self=False, the node itself is never evaluated. t = tree_with_some_nodes() del t._name # so that if the predicate is called on `t`, we crash r = t.find(lambda n: not n.name.startswith("sub"), include_self=False) # no crash assert r is t[0] def test_find_none(): # when find() yields no result, return None t = Tree() assert t.find(lambda n: False) is None # no StopIteration exception dupeguru-4.3.1/hscommon/tests/util_test.py000066400000000000000000000227371426171743600207600ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2011-01-11 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from io import StringIO from pytest import raises from hscommon.testutil import eq_ from pathlib import Path from hscommon.util import ( nonone, tryint, first, flatten, dedupe, extract, allsame, format_time, format_time_decimal, format_size, multi_replace, delete_if_empty, open_if_filename, FileOrPath, iterconsume, escape, get_file_ext, rem_file_ext, pluralize, ) def test_nonone(): eq_("foo", nonone("foo", "bar")) eq_("bar", nonone(None, "bar")) def test_tryint(): eq_(42, tryint("42")) eq_(0, tryint("abc")) eq_(0, tryint(None)) eq_(42, tryint(None, 42)) # --- Sequence def test_first(): eq_(first([3, 2, 1]), 3) eq_(first(i for i in [3, 2, 1] if i < 3), 2) def test_flatten(): eq_([1, 2, 3, 4], flatten([[1, 2], [3, 4]])) eq_([], flatten([])) def test_dedupe(): reflist = [0, 7, 1, 2, 3, 4, 4, 5, 6, 7, 1, 2, 3] eq_(dedupe(reflist), [0, 7, 1, 2, 3, 4, 5, 6]) def test_extract(): wheat, shaft = extract(lambda n: n % 2 == 0, list(range(10))) eq_(wheat, [0, 2, 4, 6, 8]) eq_(shaft, [1, 3, 5, 7, 9]) def test_allsame(): assert allsame([42, 42, 42]) assert not allsame([42, 43, 42]) assert not allsame([43, 42, 42]) # Works on non-sequence as well assert allsame(iter([42, 42, 42])) def test_iterconsume(): # We just want to make sure that we return *all* items and that we're not mistakenly skipping # one. eq_(list(range(2500)), list(iterconsume(list(range(2500))))) eq_(list(reversed(range(2500))), list(iterconsume(list(range(2500)), reverse=False))) # --- String def test_escape(): eq_("f\\o\\ob\\ar", escape("foobar", "oa")) eq_("f*o*ob*ar", escape("foobar", "oa", "*")) eq_("f*o*ob*ar", escape("foobar", set("oa"), "*")) def test_get_file_ext(): eq_(get_file_ext("foobar"), "") eq_(get_file_ext("foo.bar"), "bar") eq_(get_file_ext("foobar."), "") eq_(get_file_ext(".foobar"), "foobar") def test_rem_file_ext(): eq_(rem_file_ext("foobar"), "foobar") eq_(rem_file_ext("foo.bar"), "foo") eq_(rem_file_ext("foobar."), "foobar") eq_(rem_file_ext(".foobar"), "") def test_pluralize(): eq_("0 song", pluralize(0, "song")) eq_("1 song", pluralize(1, "song")) eq_("2 songs", pluralize(2, "song")) eq_("1 song", pluralize(1.1, "song")) eq_("2 songs", pluralize(1.5, "song")) eq_("1.1 songs", pluralize(1.1, "song", 1)) eq_("1.5 songs", pluralize(1.5, "song", 1)) eq_("2 entries", pluralize(2, "entry", plural_word="entries")) def test_format_time(): eq_(format_time(0), "00:00:00") eq_(format_time(1), "00:00:01") eq_(format_time(23), "00:00:23") eq_(format_time(60), "00:01:00") eq_(format_time(101), "00:01:41") eq_(format_time(683), "00:11:23") eq_(format_time(3600), "01:00:00") eq_(format_time(3754), "01:02:34") eq_(format_time(36000), "10:00:00") eq_(format_time(366666), "101:51:06") eq_(format_time(0, with_hours=False), "00:00") eq_(format_time(1, with_hours=False), "00:01") eq_(format_time(23, with_hours=False), "00:23") eq_(format_time(60, with_hours=False), "01:00") eq_(format_time(101, with_hours=False), "01:41") eq_(format_time(683, with_hours=False), "11:23") eq_(format_time(3600, with_hours=False), "60:00") eq_(format_time(6036, with_hours=False), "100:36") eq_(format_time(60360, with_hours=False), "1006:00") def test_format_time_decimal(): eq_(format_time_decimal(0), "0.0 second") eq_(format_time_decimal(1), "1.0 second") eq_(format_time_decimal(23), "23.0 seconds") eq_(format_time_decimal(60), "1.0 minute") eq_(format_time_decimal(101), "1.7 minutes") eq_(format_time_decimal(683), "11.4 minutes") eq_(format_time_decimal(3600), "1.0 hour") eq_(format_time_decimal(6036), "1.7 hours") eq_(format_time_decimal(86400), "1.0 day") eq_(format_time_decimal(160360), "1.9 days") def test_format_size(): eq_(format_size(1024), "1 KB") eq_(format_size(1024, 2), "1.00 KB") eq_(format_size(1024, 0, 2), "1 MB") eq_(format_size(1024, 2, 2), "0.01 MB") eq_(format_size(1024, 3, 2), "0.001 MB") eq_(format_size(1024, 3, 2, False), "0.001") eq_(format_size(1023), "1023 B") eq_(format_size(1023, 0, 1), "1 KB") eq_(format_size(511, 0, 1), "1 KB") eq_(format_size(9), "9 B") eq_(format_size(99), "99 B") eq_(format_size(999), "999 B") eq_(format_size(9999), "10 KB") eq_(format_size(99999), "98 KB") eq_(format_size(999999), "977 KB") eq_(format_size(9999999), "10 MB") eq_(format_size(99999999), "96 MB") eq_(format_size(999999999), "954 MB") eq_(format_size(9999999999), "10 GB") eq_(format_size(99999999999), "94 GB") eq_(format_size(999999999999), "932 GB") eq_(format_size(9999999999999), "10 TB") eq_(format_size(99999999999999), "91 TB") eq_(format_size(999999999999999), "910 TB") eq_(format_size(9999999999999999), "9 PB") eq_(format_size(99999999999999999), "89 PB") eq_(format_size(999999999999999999), "889 PB") eq_(format_size(9999999999999999999), "9 EB") eq_(format_size(99999999999999999999), "87 EB") eq_(format_size(999999999999999999999), "868 EB") eq_(format_size(9999999999999999999999), "9 ZB") eq_(format_size(99999999999999999999999), "85 ZB") eq_(format_size(999999999999999999999999), "848 ZB") def test_multi_replace(): eq_("136", multi_replace("123456", ("2", "45"))) eq_("1 3 6", multi_replace("123456", ("2", "45"), " ")) eq_("1 3 6", multi_replace("123456", "245", " ")) eq_("173896", multi_replace("123456", "245", "789")) eq_("173896", multi_replace("123456", "245", ("7", "8", "9"))) eq_("17386", multi_replace("123456", ("2", "45"), "78")) eq_("17386", multi_replace("123456", ("2", "45"), ("7", "8"))) with raises(ValueError): multi_replace("123456", ("2", "45"), ("7", "8", "9")) eq_("17346", multi_replace("12346", ("2", "45"), "78")) # --- Files class TestCaseDeleteIfEmpty: def test_is_empty(self, tmpdir): testpath = Path(str(tmpdir)) assert delete_if_empty(testpath) assert not testpath.exists() def test_not_empty(self, tmpdir): testpath = Path(str(tmpdir)) testpath.joinpath("foo").mkdir() assert not delete_if_empty(testpath) assert testpath.exists() def test_with_files_to_delete(self, tmpdir): testpath = Path(str(tmpdir)) testpath.joinpath("foo").touch() testpath.joinpath("bar").touch() assert delete_if_empty(testpath, ["foo", "bar"]) assert not testpath.exists() def test_directory_in_files_to_delete(self, tmpdir): testpath = Path(str(tmpdir)) testpath.joinpath("foo").mkdir() assert not delete_if_empty(testpath, ["foo"]) assert testpath.exists() def test_delete_files_to_delete_only_if_dir_is_empty(self, tmpdir): testpath = Path(str(tmpdir)) testpath.joinpath("foo").touch() testpath.joinpath("bar").touch() assert not delete_if_empty(testpath, ["foo"]) assert testpath.exists() assert testpath.joinpath("foo").exists() def test_doesnt_exist(self): # When the 'path' doesn't exist, just do nothing. delete_if_empty(Path("does_not_exist")) # no crash def test_is_file(self, tmpdir): # When 'path' is a file, do nothing. p = Path(str(tmpdir)).joinpath("filename") p.touch() delete_if_empty(p) # no crash def test_ioerror(self, tmpdir, monkeypatch): # if an IO error happens during the operation, ignore it. def do_raise(*args, **kw): raise OSError() monkeypatch.setattr(Path, "rmdir", do_raise) delete_if_empty(Path(str(tmpdir))) # no crash class TestCaseOpenIfFilename: FILE_NAME = "test.txt" def test_file_name(self, tmpdir): filepath = str(tmpdir.join(self.FILE_NAME)) open(filepath, "wb").write(b"test_data") file, close = open_if_filename(filepath) assert close eq_(b"test_data", file.read()) file.close() def test_opened_file(self): sio = StringIO() sio.write("test_data") sio.seek(0) file, close = open_if_filename(sio) assert not close eq_("test_data", file.read()) def test_mode_is_passed_to_open(self, tmpdir): filepath = str(tmpdir.join(self.FILE_NAME)) open(filepath, "w").close() file, close = open_if_filename(filepath, "a") eq_("a", file.mode) file.close() class TestCaseFileOrPath: FILE_NAME = "test.txt" def test_path(self, tmpdir): filepath = str(tmpdir.join(self.FILE_NAME)) open(filepath, "wb").write(b"test_data") with FileOrPath(filepath) as fp: eq_(b"test_data", fp.read()) def test_opened_file(self): sio = StringIO() sio.write("test_data") sio.seek(0) with FileOrPath(sio) as fp: eq_("test_data", fp.read()) def test_mode_is_passed_to_open(self, tmpdir): filepath = str(tmpdir.join(self.FILE_NAME)) open(filepath, "w").close() with FileOrPath(filepath, "a") as fp: eq_("a", fp.mode) dupeguru-4.3.1/hscommon/testutil.py000066400000000000000000000143031426171743600174450ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2010-11-14 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import pytest def eq_(a, b, msg=None): __tracebackhide__ = True assert a == b, msg or "{!r} != {!r}".format(a, b) def callcounter(): def f(*args, **kwargs): f.callcount += 1 f.callcount = 0 return f class CallLogger: """This is a dummy object that logs all calls made to it. It is used to simulate the GUI layer. """ def __init__(self): self.calls = [] def __getattr__(self, func_name): def func(*args, **kw): self.calls.append(func_name) return func def clear_calls(self): del self.calls[:] def check_gui_calls(self, expected, verify_order=False): """Checks that the expected calls have been made to 'self', then clears the log. `expected` is an iterable of strings representing method names. If `verify_order` is True, the order of the calls matters. """ __tracebackhide__ = True if verify_order: eq_(self.calls, expected) else: eq_(set(self.calls), set(expected)) self.clear_calls() def check_gui_calls_partial(self, expected=None, not_expected=None, verify_order=False): """Checks that the expected calls have been made to 'self', then clears the log. `expected` is an iterable of strings representing method names. Order doesn't matter. Moreover, if calls have been made that are not in expected, no failure occur. `not_expected` can be used for a more explicit check (rather than calling `check_gui_calls` with an empty `expected`) to assert that calls have *not* been made. """ __tracebackhide__ = True if expected is not None: not_called = set(expected) - set(self.calls) assert not not_called, f"These calls haven't been made: {not_called}" if verify_order: max_index = 0 for call in expected: index = self.calls.index(call) if index < max_index: raise AssertionError(f"The call {call} hasn't been made in the correct order") max_index = index if not_expected is not None: called = set(not_expected) & set(self.calls) assert not called, f"These calls shouldn't have been made: {called}" self.clear_calls() class TestApp: def __init__(self): self._call_loggers = [] def clear_gui_calls(self): for logger in self._call_loggers: logger.clear_calls() def make_logger(self, logger=None): if logger is None: logger = CallLogger() self._call_loggers.append(logger) return logger def make_gui(self, name, class_, view=None, parent=None, holder=None): if view is None: view = self.make_logger() if parent is None: # The attribute "default_parent" has to be set for this to work correctly parent = self.default_parent if holder is None: holder = self setattr(holder, f"{name}_gui", view) gui = class_(parent) gui.view = view setattr(holder, name, gui) return gui # To use @with_app, you have to import app in your conftest.py file. def with_app(setupfunc): def decorator(func): func.setupfunc = setupfunc return func return decorator @pytest.fixture def app(request): setupfunc = request.function.setupfunc if hasattr(setupfunc, "__code__"): argnames = setupfunc.__code__.co_varnames[: setupfunc.__code__.co_argcount] def getarg(name): if name == "self": return request.function.__self__ else: return request.getfixturevalue(name) args = [getarg(argname) for argname in argnames] else: args = [] app = setupfunc(*args) return app def _unify_args(func, args, kwargs, args_to_ignore=None): """Unify args and kwargs in the same dictionary. The result is kwargs with args added to it. func.func_code.co_varnames is used to determine under what key each elements of arg will be mapped in kwargs. if you want some arguments not to be in the results, supply a list of arg names in args_to_ignore. if f is a function that takes *args, func_code.co_varnames is empty, so args will be put under 'args' in kwargs. def foo(bar, baz) _unifyArgs(foo, (42,), {'baz': 23}) --> {'bar': 42, 'baz': 23} _unifyArgs(foo, (42,), {'baz': 23}, ['bar']) --> {'baz': 23} """ result = kwargs.copy() if hasattr(func, "__code__"): # built-in functions don't have func_code args = list(args) if getattr(func, "__self__", None) is not None: # bound method, we have to add self to args list args = [func.__self__] + args defaults = list(func.__defaults__) if func.__defaults__ is not None else [] arg_count = func.__code__.co_argcount arg_names = list(func.__code__.co_varnames) if len(args) < arg_count: # We have default values required_arg_count = arg_count - len(args) args = args + defaults[-required_arg_count:] for arg_name, arg in zip(arg_names, args): # setdefault is used because if the arg is already in kwargs, we don't want to use default values result.setdefault(arg_name, arg) else: # 'func' has a *args argument result["args"] = args if args_to_ignore: for kw in args_to_ignore: del result[kw] return result def log_calls(func): """Logs all func calls' arguments under func.calls. func.calls is a list of _unify_args() result (dict). Mostly used for unit testing. """ def wrapper(*args, **kwargs): unified_args = _unify_args(func, args, kwargs) wrapper.calls.append(unified_args) return func(*args, **kwargs) wrapper.calls = [] return wrapper dupeguru-4.3.1/hscommon/trans.py000066400000000000000000000116221426171743600167200ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2010-06-23 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html # Doing i18n with GNU gettext for the core text gets complicated, so what I do is that I make the # GUI layer responsible for supplying a tr() function. import locale import logging import os import os.path as op from typing import Callable, Union from hscommon.plat import ISLINUX _trfunc = None _trget = None installed_lang = None def tr(s: str, context: Union[str, None] = None) -> str: if _trfunc is None: return s else: if context: return _trfunc(s, context) else: return _trfunc(s) def trget(domain: str) -> Callable[[str], str]: # Returns a tr() function for the specified domain. if _trget is None: return lambda s: tr(s, domain) else: return _trget(domain) def set_tr( new_tr: Callable[[str, Union[str, None]], str], new_trget: Union[Callable[[str], Callable[[str], str]], None] = None ) -> None: global _trfunc, _trget _trfunc = new_tr if new_trget is not None: _trget = new_trget def get_locale_name(lang: str) -> Union[str, None]: # Removed old conversion code as windows seems to support these LANG2LOCALENAME = { "cs": "cs_CZ", "de": "de_DE", "el": "el_GR", "en": "en", "es": "es_ES", "fr": "fr_FR", "hy": "hy_AM", "it": "it_IT", "ja": "ja_JP", "ko": "ko_KR", "ms": "ms_MY", "nl": "nl_NL", "pl_PL": "pl_PL", "pt_BR": "pt_BR", "ru": "ru_RU", "tr": "tr_TR", "uk": "uk_UA", "vi": "vi_VN", "zh_CN": "zh_CN", } if lang not in LANG2LOCALENAME: return None result = LANG2LOCALENAME[lang] if ISLINUX: result += ".UTF-8" return result # --- Qt def install_qt_trans(lang: str = None) -> None: from PyQt5.QtCore import QCoreApplication, QTranslator, QLocale if not lang: lang = str(QLocale.system().name())[:2] localename = get_locale_name(lang) if localename is not None: try: locale.setlocale(locale.LC_ALL, localename) except locale.Error: logging.warning("Couldn't set locale %s", localename) else: lang = "en" qtr1 = QTranslator(QCoreApplication.instance()) qtr1.load(":/qt_%s" % lang) QCoreApplication.installTranslator(qtr1) qtr2 = QTranslator(QCoreApplication.instance()) qtr2.load(":/%s" % lang) QCoreApplication.installTranslator(qtr2) def qt_tr(s: str, context: Union[str, None] = "core") -> str: if context is None: context = "core" return str(QCoreApplication.translate(context, s, None)) set_tr(qt_tr) # --- gettext def install_gettext_trans(base_folder: os.PathLike, lang: str) -> None: import gettext def gettext_trget(domain: str) -> Callable[[str], str]: if not lang: return lambda s: s try: return gettext.translation(domain, localedir=base_folder, languages=[lang]).gettext except OSError: return lambda s: s default_gettext = gettext_trget("core") def gettext_tr(s: str, context: Union[str, None] = None) -> str: if not context: return default_gettext(s) else: trfunc = gettext_trget(context) return trfunc(s) set_tr(gettext_tr, gettext_trget) global installed_lang installed_lang = lang def install_gettext_trans_under_qt(base_folder: os.PathLike, lang: str = None) -> None: # So, we install the gettext locale, great, but we also should try to install qt_*.qm if # available so that strings that are inside Qt itself over which I have no control are in the # right language. from PyQt5.QtCore import QCoreApplication, QTranslator, QLocale, QLibraryInfo if not lang: lang = str(QLocale.system().name())[:2] localename = get_locale_name(lang) if localename is None: lang = "en" localename = get_locale_name(lang) try: locale.setlocale(locale.LC_ALL, localename) except locale.Error: logging.warning("Couldn't set locale %s", localename) qmname = "qt_%s" % lang if ISLINUX: # Under linux, a full Qt installation is already available in the system, we didn't bundle # up the qm files in our package, so we have to load translations from the system. qmpath = op.join(QLibraryInfo.location(QLibraryInfo.TranslationsPath), qmname) else: qmpath = op.join(base_folder, qmname) qtr = QTranslator(QCoreApplication.instance()) qtr.load(qmpath) QCoreApplication.installTranslator(qtr) install_gettext_trans(base_folder, lang) dupeguru-4.3.1/hscommon/util.py000066400000000000000000000243031426171743600165460ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2011-01-11 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from math import ceil from pathlib import Path from hscommon.path import pathify, log_io_error from typing import IO, Any, Callable, Generator, Iterable, List, Tuple, Union def nonone(value: Any, replace_value: Any) -> Any: """Returns ``value`` if ``value`` is not ``None``. Returns ``replace_value`` otherwise.""" if value is None: return replace_value else: return value def tryint(value: Any, default: int = 0) -> int: """Tries to convert ``value`` to in ``int`` and returns ``default`` if it fails.""" try: return int(value) except (TypeError, ValueError): return default # --- Sequence related def dedupe(iterable: Iterable[Any]) -> List[Any]: """Returns a list of elements in ``iterable`` with all dupes removed. The order of the elements is preserved. """ result = [] seen = {} for item in iterable: if item in seen: continue seen[item] = 1 result.append(item) return result def flatten(iterables: Iterable[Iterable], start_with: Iterable[Any] = None) -> List[Any]: """Takes a list of lists ``iterables`` and returns a list containing elements of every list. If ``start_with`` is not ``None``, the result will start with ``start_with`` items, exactly as if ``start_with`` would be the first item of lists. """ result: List[Any] = [] if start_with: result.extend(start_with) for iterable in iterables: result.extend(iterable) return result def first(iterable: Iterable[Any]): """Returns the first item of ``iterable``.""" try: return next(iter(iterable)) except StopIteration: return None def extract(predicate: Callable[[Any], bool], iterable: Iterable[Any]) -> Tuple[List[Any], List[Any]]: """Separates the wheat from the shaft (`predicate` defines what's the wheat), and returns both.""" wheat = [] shaft = [] for item in iterable: if predicate(item): wheat.append(item) else: shaft.append(item) return wheat, shaft def allsame(iterable: Iterable[Any]) -> bool: """Returns whether all elements of 'iterable' are the same.""" it = iter(iterable) try: first_item = next(it) except StopIteration: raise ValueError("iterable cannot be empty") return all(element == first_item for element in it) def iterconsume(seq: List[Any], reverse: bool = True) -> Generator[Any, None, None]: """Iterate over ``seq`` and pops yielded objects. Because we use the ``pop()`` method, we reverse ``seq`` before proceeding. If you don't need to do that, set ``reverse`` to ``False``. This is useful in tight memory situation where you are looping over a sequence of objects that are going to be discarded afterwards. If you're creating other objects during that iteration you might want to use this to avoid ``MemoryError``. """ if reverse: seq.reverse() while seq: yield seq.pop() # --- String related def escape(s: str, to_escape: str, escape_with: str = "\\") -> str: """Returns ``s`` with characters in ``to_escape`` all prepended with ``escape_with``.""" return "".join((escape_with + c if c in to_escape else c) for c in s) def get_file_ext(filename: str) -> str: """Returns the lowercase extension part of filename, without the dot.""" pos = filename.rfind(".") if pos > -1: return filename[pos + 1 :].lower() else: return "" def rem_file_ext(filename: str) -> str: """Returns the filename without extension.""" pos = filename.rfind(".") if pos > -1: return filename[:pos] else: return filename # TODO type hint number def pluralize(number, word: str, decimals: int = 0, plural_word: Union[str, None] = None) -> str: """Returns a pluralized string with ``number`` in front of ``word``. Adds a 's' to s if ``number`` > 1. ``number``: The number to go in front of s ``word``: The word to go after number ``decimals``: The number of digits after the dot ``plural_word``: If the plural rule for word is more complex than adding a 's', specify a plural """ number = round(number, decimals) plural_format = "%%1.%df %%s" % decimals if number > 1: if plural_word is None: word += "s" else: word = plural_word return plural_format % (number, word) def format_time(seconds: int, with_hours: bool = True) -> str: """Transforms seconds in a hh:mm:ss string. If ``with_hours`` if false, the format is mm:ss. """ minus = seconds < 0 if minus: seconds *= -1 m, s = divmod(seconds, 60) if with_hours: h, m = divmod(m, 60) r = "%02d:%02d:%02d" % (h, m, s) else: r = "%02d:%02d" % (m, s) if minus: return "-" + r else: return r def format_time_decimal(seconds: int) -> str: """Transforms seconds in a strings like '3.4 minutes'.""" minus = seconds < 0 if minus: seconds *= -1 if seconds < 60: r = pluralize(seconds, "second", 1) elif seconds < 3600: r = pluralize(seconds / 60.0, "minute", 1) elif seconds < 86400: r = pluralize(seconds / 3600.0, "hour", 1) else: r = pluralize(seconds / 86400.0, "day", 1) if minus: return "-" + r else: return r SIZE_DESC = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") SIZE_VALS = tuple(1024**i for i in range(1, 9)) def format_size(size: int, decimal: int = 0, forcepower: int = -1, showdesc: bool = True) -> str: """Transform a byte count in a formatted string (KB, MB etc..). ``size`` is the number of bytes to format. ``decimal`` is the number digits after the dot. ``forcepower`` is the desired suffix. 0 is B, 1 is KB, 2 is MB etc.. if kept at -1, the suffix will be automatically chosen (so the resulting number is always below 1024). if ``showdesc`` is ``True``, the suffix will be shown after the number. Usage example:: >>> format_size(1234, decimal=2, showdesc=True) '1.21 KB' """ if forcepower < 0: i = 0 while size >= SIZE_VALS[i]: i += 1 else: i = forcepower if i > 0: div = SIZE_VALS[i - 1] else: div = 1 size_format = "%%%d.%df" % (decimal, decimal) negative = size < 0 divided_size = (0.0 + abs(size)) / div if decimal == 0: divided_size = ceil(divided_size) else: divided_size = ceil(divided_size * (10**decimal)) / (10**decimal) if negative: divided_size *= -1 result = size_format % divided_size if showdesc: result += " " + SIZE_DESC[i] return result def multi_replace(s: str, replace_from: Union[str, List[str]], replace_to: Union[str, List[str]] = "") -> str: """A function like str.replace() with multiple replacements. ``replace_from`` is a list of things you want to replace. Ex: ['a','bc','d'] ``replace_to`` is a list of what you want to replace to. If ``replace_to`` is a list and has the same length as ``replace_from``, ``replace_from`` items will be translated to corresponding ``replace_to``. A ``replace_to`` list must have the same length as ``replace_from`` If ``replace_to`` is a string, all ``replace_from`` occurence will be replaced by that string. ``replace_from`` can also be a str. If it is, every char in it will be translated as if ``replace_from`` would be a list of chars. If ``replace_to`` is a str and has the same length as ``replace_from``, it will be transformed into a list. """ if isinstance(replace_to, str) and (len(replace_from) != len(replace_to)): replace_to = [replace_to for _ in replace_from] if len(replace_from) != len(replace_to): raise ValueError("len(replace_from) must be equal to len(replace_to)") replace = list(zip(replace_from, replace_to)) for r_from, r_to in [r for r in replace if r[0] in s]: s = s.replace(r_from, r_to) return s # --- Files related @log_io_error @pathify def delete_if_empty(path: Path, files_to_delete: List[str] = []) -> bool: """Deletes the directory at 'path' if it is empty or if it only contains files_to_delete.""" if not path.exists() or not path.is_dir(): return False contents = list(path.glob("*")) if any(p for p in contents if (p.name not in files_to_delete) or p.is_dir()): return False for p in contents: p.unlink() path.rmdir() return True def open_if_filename( infile: Union[Path, str, IO], mode: str = "rb", ) -> Tuple[IO, bool]: """If ``infile`` is a string, it opens and returns it. If it's already a file object, it simply returns it. This function returns ``(file, should_close_flag)``. The should_close_flag is True is a file has effectively been opened (if we already pass a file object, we assume that the responsibility for closing the file has already been taken). Example usage:: fp, shouldclose = open_if_filename(infile) dostuff() if shouldclose: fp.close() """ if isinstance(infile, Path): return (infile.open(mode), True) if isinstance(infile, str): return (open(infile, mode), True) else: return (infile, False) class FileOrPath: """Does the same as :func:`open_if_filename`, but it can be used with a ``with`` statement. Example:: with FileOrPath(infile): dostuff() """ def __init__(self, file_or_path: Union[Path, str], mode: str = "rb") -> None: self.file_or_path = file_or_path self.mode = mode self.mustclose = False self.fp: Union[IO, None] = None def __enter__(self) -> IO: self.fp, self.mustclose = open_if_filename(self.file_or_path, self.mode) return self.fp def __exit__(self, exc_type, exc_value, traceback) -> None: if self.fp and self.mustclose: self.fp.close() dupeguru-4.3.1/images/000077500000000000000000000000001426171743600146375ustar00rootroot00000000000000dupeguru-4.3.1/images/dgme_logo.ico000066400000000000000000000422061426171743600172730ustar00rootroot0000000000000000 %F  % 6 h@(0` ! #      ! # 1!$R#KB;2 ! #"! /000,! "#'D/6}+1{!$gZOL5s>9@>>>B2v !3V6>m^ R GF="[> 7 5 5 5;7!"3T7@sdW KG@#l? 7 6 6 6;6!"4T9Bwi\ OIB%@ 6 6 6 6;6!#4T:C{ma SKF ! #'@ 6 6 6 6;7"#5T;E~rfXN!I")? 6 6 6 6;6"#5T 5 6 6 6;6"#5T=Gzo`W#J(9; 6 6 6 6;6"#6T>H~se]#J R> 8 6 6 6 6;7"#6T?Iykd#Iu$A 7 6 6 6 6;6"#6T@Jq"&n"FV-A 6 6 6 6 6;6"#5PAL y*0y!@/'<< 6 6 6 6 6;6 !4kDO 193!$zE 8 6 6 6 6 6:9$?/e-c,_.u8A?H'(4<" 4E 6 6 6 6 6 6 7?* !a!b!gN #!N sCMAKDOBK"&%(>N52 4 6 6 6 6 6 6 6;C+b;)-{* $G;FAW>N_iB3\P-"'X<N#,TG13 6 6 6 6 8A+e7>HFR8@)*H;aBwer7B2C^$*\Ndqv{YjzE0 6 6 8A,d8@K38>G:D**8GU  \5>L ;0AOqҢs˗c̙f̙fΛgYm8GJ:EO6;$3EVkL02?%H!GH 8 57̧͚h˘d̙f̙f͙f{WK]Z%CFU8@"'0=L\k6E ^2I!7!G5A 70"LšΛj̙f̙fΙf~XO_\/MFe8R/26<CNZhs6G Wu4Bk5j.4 ! !G3@ 7.&Pȣաnʑ\˓_f;MMt9YEt8bCFJOV_it}1H!]~-8Y8AO*.sA! "J3@ 71#Ihx~Ī˲亂"24E@`E8nRUY^clt~&I&r %E0Gft KD6! "I3AA) c1=Cʔ| GhE8w\`dhnt|D2K#aJ# Y ? 7@6! "F2,d§pui$//JjF:dfkotz1D?T*31=6@k I : 6 6@5! #ET;P]Wzø.>?< $ Nx9PGB(wqw|F*|&%Y~GnY @ 7;??B2! #w3BAeYy|q)1Ccz!Ddz%Fax$F_x0pH%}{/C:M 5g9Ak L ;;<&$~$w$y{!'٭|&'8⭅]Rih!';7}A/<*88A]('] D 9@(:Ͷ܏~ pԭf+<>\3x:~#G"Lk"-JoCOuS ? 8?( !#Ƹlwl ʕfi!#63y9~/?*7)-U;Cg L = 7?) !%HFTP`Z|{j "3z9~{=1fO+/`28] G ; 7?) !%쁭}!,-X إvWwyj 3z9|~zpF(?h%-3h,2 V D ; 7?) !%z $կНjVrxk 4y9y{wo#dEm!&?  /6l(,{ R B : 7?) !% Ȱ냌~h+έϚhϛgVmyp4x9uuo`)WDW* -4h%(t O B : 7?) !%.ѽvƘj͖b͚g̘d`Xpn4w9pmcO0I?I!z.5k!#m M A : 7?) !%]£Ǒ]Ȑ\̗d̙fΛgWy!0273u9jdV:7@6<^,3g"j M A : 7?) !%͙ܶe͙f̙fΛgƐ]hbzw4s9aXE%;A-2kH )/]!$j L B ; 7?) !%ت˗d͚f͚fčZi  4p9XK2>F',X7).\!$i M B < 8?) !%Ԣs̗bȑ]W} 4m8xJ8>G$'L*%*U!%j L C < 8@) !&DħǏ\b}ăQ.cF.T/>-..1>I="I)/qVKD@C%! "Mһ»䆺62GX2`A_AN?H;D$(_!h "%V#O#)S#H @<,i! #"%-!/!. / . / ./ ???ݐ?$ ???h???????@ ?@ 0q??????????????? ( @ ',[*`&a!C! # "M%a%a"U " %(Q&*oWJ4C$e9;;7#-/5ru WJ@h,= 6 7=' .06sz^MA!m/; 5 7;& !.17veRF#r0; 5 6;'!!.29ylXJ!n3 : 5 7;& !.3:{r` Lc: 8 6 7;& !/4;} zi"O N$N= 6 6 7;&!!. 5496>!_-Qz:83y,UagӱŘflFOb5 :6 n0&)N:A!269M"+`,s2*>)*c Kw͖`ИaeCOa 1m'*P;B!$2]7t2?e2>%8W+-mY/ɒ]Λh͕_i(19)C)-U=C"-D_B(p'39A0#*Sóުuʖbқg\O_[j(U$1+5BVi71~ Mn+=z!b(=/4>bǽޯ}Е^cDVT_@^(Ls>l#OIS^m{26G[;f(*~7a(=4%.J}{é*+1(T};y"`]fo{(<;Gm8t). D<3`)7 nG ,:;,<#eJ2~gpx;%Sj'Mjv7TZ : 8B5 m>heV>%01).>9$\3d7t6y)8%)?7\#" F :7-.!!=:1 ʰrjW1B+49%7)Wt#,59Ie =<&!#WdŢ|CJD,a6'!: ,5_((CZ05 S :;(7!,,(ȶd-63g4~7&)5h&%,0a&)~ J 9;(smcR^د_&/.Z4w8&zs/q1Cq).S"$u E 8<(ΨozpMžӘbg **J4v8&p`5V+-O)-S k C 8<(мõtơvΕ`Ϙbh'*3u7&xcH7A%%=(,Rf C 8<($ʹГ\ΗbЗakTb]4q8z&mN ,6: !/y&*Kd C 9<(IٺΔ]ˑ[jlvl 5o9x%_26<'j$(He E ;>(}ӣxfܒn.@B'Qz0a;g5@59.3tC7u"'dOC=$SŽR/C)(3Mk*-Go')Cq)K $-` .p*p#b  1!`6#~~C~ | | | | |? (0 +- ":1&o "  %d*(";'+W!sR>?!#9 :<*,0cv SB!J&79 5 :+-1e\G#J'Q: 5 :+.3heLF)w : 5 :+/3j pQC0 9 5 :+/4k! !%X>7 8 6 :*$6C!#2A5:  '+U"b= 5 6 90!G"C!-1k?G'342H&';481 4 69=) 7+18|&(F3C(^(3j-/3]f_fACX ;3;/!7*28'(38'+F 4}7"2LT񞔏mĘfiec>+ 8)4:%$ #5a'If6A5R$$b?ߪsЙbÖeOST *G+6=''*D*|5+=_:37>aֽ١jѕ]{qZ >[*6W(F<L`%5(Z~/A(04?Feѵml]Mq-4k'c\jy6$Uk5X^9&1'6?߄EC:p 6L27}; ou/+b~.Q"' 9:;$ɧ/3-SYb[7L,%MpL)Mfa4 5 2>V3D X9,&G"J %11$,52M3 '/f$#7N)- E9%|.{yh撞_ #!5 4 /(9R)(J\ "s >9'efne'ܽwV4x 5 |n3\#!)M&(Bgh <:'Ѝȳ֙bw_ 4u 5pQ3>/%'Dra <:'ƶЕ`є\ĘiOYSr5r 5yY.13$&@o^ =:'̍Sšnntg /d6q+M+,.2o !#9ecE=&,éջL3I#;ZW/:f,/Z 3]&!84*!"  I?(  18 G0 (g0(w'*SwpD!:3 ;1),VqyK#H4 90*-YoU#]6 80%+.Xv  b'} 9 80!,0b.8"4*0c -6481!B(+_,.k#!3R-_/+McYgue_B 5%**]&#$$,J!IXC%FmPfxa"/}+:n)1)N0%Hi!G 1LMhԭdERM/=U+])^Zt+/i$/~. Q +DLU )(4EE!FdT6w$v$-t*< M5( zm ´$$7N(Pk-*+>Zx! p 9%` %lyqvQr\5|.-c'%?_` 7$WLɦgU?5x .~a,6t"!0SW 8 Yuʙi̘eFLEa5t /q#9)(Z !.IX ;^ӽ˞ox{l "Ca+Hz-2w#"7Y&B2+9$w{B  !aa agdupeguru-4.3.1/images/dgme_logo_128.png000066400000000000000000000432141426171743600176770ustar00rootroot00000000000000PNG  IHDR>agAMA a cHRMz&u0`:pQ<bKGDFIDATxy\Wy';ܽ}WKd66H!G !d@H $g2cHf& dBB Ę 6%[޵/w9ǽU]ՋԶ$[J"2JAJ" @D:d2JX815_j\9Twnd ( (H<*bBJ\ẃ{P}ٮ{՟֒z Ԫ_JSOO~>M﬿*0 f: ?C@@(!pL*dD[8wvV ~0ihIGJH g:%$R-dԋ-ljSqˇ1:((! i4ÂKfP#I|vE'[~=?Gmo5~.엀H-MhmA̧ Xe*Om) %d@"6b|2n5`jF1''M0 ;X2t6Kp_Ng]ݟ~_1tZ@棦wJ=Z f"`@ѨEϷ^_FoK9[zQl'!20a8 S]er8<_EM7ẁsqSg-AEuHՒ 6 [9!p&,S* T7 ( پ|z߀0u`nwBt$3HzjIѧg?~\l.-EEJ`66`8 PRؖ])K &K@)E QJBNNө{mj>ш,[t OG(eLN"TZEF 4_=m|Zq6@?aHkhI3< ʃf1L̆-#lGJHP pPhJ>^\<ljT@Qmp4+4%x*xC ѲlMjЭb4jyi[nhvLL4ÃMlPRQL\AK)fEPA<1P_sxvN/T'-\Jnixi2<!(vSM3`:qhvz ^w۵x>wsntl [cMP78(d@1K 1B$+Q^ /@$kyֱDTž^wW~x%"!zvǻ_S =ͅL(4h3eҩD靜; tnR7 t6}(7T 7c!T"g,4YVI"鸷 )~e>)>t,hnq y퇻j i`YJnJ"`דR鄐!T¦o=nh@ aMN !´UJ Pdhd]p,@'`58>]c(=[WK`jIh%1 `1ʴNTh=960 54I>=Q>= Pr9tB`h C۰lR^%S Nֻ;ɭ @\948FH J4GG{{wz'Pƴvm-m #MhiF\ǾIZAThRM 0B n h V  e9$% ^pJn}߰ CgK((c9\_12r;9@ǬJ$3`l0{B/e`^ ]0شm 0/lߎcd"װZ8p`l/Ofotv$g4Ly;p>Hm ڔB<@z.\Jrh(c .WuƠ)tLhBC ׽鹤 R1K&:6#?tzF  ,MR(IF:5TGM G[)l-42%L,M`J1 *$R00ul# :uD&m;ץC89[KƓ 4&'noЅ.'!P=iv zn@+`Cװ.q`{{q0 fgZ?Õ&d A$  rDGH ]gn }j&flC `iA0(tnUfvk%Al Rl.[ }ʣ A`LSF3Yԥ3ͭ@ D$TBye0 qـ;%'F((.E x&=A_`=bټ.i[T4CY/&I6b05b|oatl@ܗ&5tT7/{4wrgs`G2}@'4A4PC/L< VyRtr&XG5M|Bn* B`r\woOzGe;z )8cZg:Z=FJ˂fbƺWFr0mNn'3R0:4Y37F ցv]="ÆK$DuFdlǗ]phbtl@uU-@CSqT+uW%$?h`t.Xt~L673/(j7uO u?15=5޷3gFCrtކny+Z}Gw+tۆE ˲QJM,f/p_s^> zw/ǠF@Fdv[6$@A尹cЍn9~oxIF1 `mf޷a_̦PXX/tBC P U=7;fC̯,=i~ _sSY1fS{ VsꉞT Ą'urɾ =LJ<3<ܼݎNLWpJs֗U?6JHEfRѢP )'F:}h^x E(@Ž@ѡD:6yj |*X^¥)~"TG UB1j"HtX 2Z:tBp86Җo3B:Jd "3!hg]A)+/ x&s]䓺JC+&"t/:Խ~訅N"~ Mgt˽biZp^~i?򘤆 ^*Cr =w=*xÅxLl7r3 M(w!* ПzY:p@?|DJUGؖHЖ> jF Ž 8dAT`bi9\_uVJ=`dGo!$-ZǺ P /ߋz_cӰ4SK,ۿfZjOm{>A`N(nA)%n2Gѣ |s +%!*k )XQ*n@Muh$|8CY،"%ٰlmJ,8@j(r_R3'&; VW>_?%211:?$ lݛAMpU{.a?|wwO\ؗU2>#^O4JC'b58_YX~]w܁T Z sU뙼C3tP*nrq;1MSsOw}{_P}I{5^{`WNׅš9q/:|-pZQm@K :  Y cO@K17k\iZ]Xp#PaiE( =@>XT1/J"W"oj??pE-dU9džy*d 98jmnY.t5HՋR}ßb|E02?w}>X*e+ӷdr DZ<X  s)HJl76,Jl*J (-݅ݹ'/^3LN|?W  =_wi؟j*Eү(P`poumU)ӄ_w~zW~-_}e݀9<4=sÇ` +'fj_2%ɷcBץ?o<ķ8aq _(VZcv'ӷI|֩:_~ͮlV% dZ -l:۝F2mNY D tg\5WG7[wOƦoΦS:\ M)<}rjkN6/m f'We, (ߚ ^=?տ<4XPv|ux"^wuߺ/8KERjgo$u&B4V+Asy'%ǖqXA!pk YzٞQY(CSUW/ ݟrgrg2_3ͷiͯ?7xY-tyj+?`#,Ώ {g&<9 SJAZ nao>X<=@}^)B бd/ TBQ"j3)HWPίYr;6 P.מۯ5 ^%bj5F]-6qhVo !nBKTJta_vvysFq!o|Gآ]S' 22Wܹ'@Ґn[:~~{;./6ëWQʯb׀ |A֪>j.G jQJї}ٻm) Ţ7,3O.z6 ;\WlmzzI' `ؐ@&;%8W(mU\1Zk>q|X)3xxp׾~@] B67ʡߊJAp_/}?>B岇x& Kk,u3'8/0+KgƱc{V௖@+vAYagpX#aFPO< 7z<> jP_7zΓS%'&7><4y>ۆn5 ~DwȾ=x.dVT{;g~ :4`t0SO ٽ"ìmX r 6l6:6wj?*QᶳM _ |)h"5APX|_;UɻW>Fq@} bnO'_=I_-|\mx a^UF' +oP0c"'N/>ZO>G,| =n#w\lt!º4hr%BAr ]QOT{uvbϮ |#_}So ЕKU]0?O`S s^̲P P|~=5pu?NWʁ;12f>)S`+S_2-g06rRD-!*4 vOj:cX[,Ù/=u]S"mgݶ&Xo0'lU<co"?[p0,X*y(p<دP/!? )_y>|Q,.UL(fRzW:JsƟP1_M$ެazWV./"6mǝJ48H_2PeY85>tÙ/{>ETGq57_1wɯ|2Iy>4j_jy#cې '<,X-hv|+(/ ?sxA9OK+nqꔐ*jW:JsR~ФztM.]Cԡ%X]Ike ,^ͭ[ͦJΨ!6CץpW pl{?T4Q_}poʟ<4cڑ}nj}d2Tr iE B ٕ/pK0rC گn 2gN6~^ IB?hLضS2e]@OTgNCX҆XO>KǛnĉu#w`5ضg-T\[r FCg~??ʱqcjf6==mV Wu᩹emXҁWk@З@,~۱|!v(~槐%o\&a 阍VOnL*]J./*)3&ozb0S`:hryH|>5 S3sđznHv )}C}}mW_>qs2x FbHqЬUѨ~оeƒk .!پ4^7cуbZ/ w.<˳,}> >'_D4`$#wB4 q(!j[](r[=5wa -I/UoR]Cb(}#pʰ4>deK1y`\ۮV*S %8^ɔI?=gVu O-7cW Z2s!%tS?+jk.jG×RR{uԊ/L<iWp@AcA cA2_1mJ )/H'x@djȩۯ7=y EfD```+vW |#SsjjC+%`[ESPE;.Xct] "JA>Gڱ|?T6J$hRV;27^=>Tp`bp@ 䐻|<,_.LeAi\bL|ӕܘRi0wxqJгCESR{z=jc?;c8% hh]X+^J x΢i/fobNC +쥣hM=5OLkf=l描^(|mWqg࠺ ZJ0CIDE*ЛBP> /`߳y޾q,n!p}*(,/j$eԪu^[W^.z~YOԃ_Ag'ǝХo.j1uI9f&z#7&,YԞ.Ep5ɩCgn$6m!Al=C^ZccيIb?+՚3q(4u|JeSQ{qP 3t-?dɬ)I-TkG)mH?SxGr5{L R _z?i@ǟe+u#l]6*|CÃ͗>͠(=V*ڨ t$%@5J tK.*/N_0O;IPxֽ#պ (̯^#I:΅kШ5 NcLa%j+s]3J:vehR_Ocks7 Tp>WUHfp  Bz /OacKU()ÎFuJ0X"T&-#ibsjerfYf(FA-~DG~"%TJanlAmghN9x\kw[F)m:@XT}^$j7wpp/Q~W})aV?X W7::$rO42=c8v|59#0um1JR.Mpt B0hH2nm<1E a-"Hl Rs]hV=('ۺdJ:?44DJ/|+n؛LFq0@3تYlGl&(rzS&)^ҩcGW:I:J)P;lQ'q$#gTV( b6TৗK-ۮSJQM΢ 01:Fggvgb!v]IWa$ox"t=TEe[ɶ%|W-q=_p 'gY\Sק24cs_;B rۉls3w:а{H:Z-yeW(E& JEP*zR;O\buzh3Gb2jD[ۚVahx>,BU[if2^-k]6C) &gx~&" Zqfҁ;EPR"e[dM*6H p:ʥ9ݺDή>h:;YC> JauF=_}4(o^+x /zC% dž' ?Q|ry5C(|ӯ ,zP%!RcZrJ w}8C XX1 C.Fs(S'Vj2#fLEӍgʕm}q_6x9g`>tTB0A/O r0t5EBIP-ї)XC#*נ#JJKq޷\r.S˭M>B\J Ht^\Jf J婙Rwӫ] A(V%397o!;w}RbPFi+i%!lY+}zI\o˵$ĐR*ꄐVJ-.>{lI䴰i kTBK=rTDr"xV!dP @PsѨ2u3ňJ|6=[*]qǒ>X :\9a>j!P ϮN?q{ !@$tm@J)Rc$oe1@R',CGJ&@t)^>cBKx<@*; xta/޲eO]u-FAH!v f\QJz*}$}CJM3 n5Ju~H6:007O/U]YCshJZ+ dTW 51:tbjRtwe?X޾+oFjPCJ)4jRRC6x7IzIPZ ĜP%&Qq!e/.D. ø+_k;1 j<2dr}gY2~Q`~nFJZBP >1hp 1H)0qs 34kkF@HcF&3<׿f,tQz9֕P$A PJ«y$lKg<d8fЖi>V/lf P/*]|@8[qֿ-Ƈ",*^{AqFO9S (7P5Pmg9_7(^ޚMu_49CR[c'd$fQՆǕ `ܞ/x d~uhgBHZ琌i TJKJDZnIu@ ѡv"5]tܽGC" D@ ]c0AtTLI2(W J(0]gIsFM_^kF(PLJ#T/vPm&!lS٢) ZB@CFR"aa:x@6xўйlC&M;pzSxqs.:'`S:XrDYE9ؾF"4(']tX7MǯCt5_!aQ&!hp4YOfB ^'jJqZY>PD?#,ز Rڴ>h%<Ѻcf@("z"!&4mI5۞ ΒZYAP3[`B@)Q*?R,,]x>\El{թ͚{CY$9UAca`cڔ ¦Thw$F,#mIJɨ{p9#ڽ|+DAO@dJHi~tKPKrGJy RV@ T3PPNB%@6l!oڢћ";"s8]AEFG6ͳ^FsI`1:^3ЊѵȊ#u[ @򍨕[zsgCKFBuu 6R( %Z *L6C5!(!ݶns^v`$(YCK*!$LJ }BН.:y+/;jVoHށ/$ !0(6ώ[fw; .M^ W,k+iC{J6TM')m7 D2HH"6lD=/<+`"p)T7T<Ϻv=63G aT@Rq_˗dD Q0M\~o ϼrBP3n7`!sKGr߻EP"i ]B,nKe~\P~j wOH|ΪEbݗI+F^҉~fGBw_ IW[6Q=,'Jfy?2T*Yo. w˜/,Z|uz#m]R2&T#&N6d4ۖ>sh_K0 '('Vˣ8W@PEA:cy[k57;҄g%WfzdbI򲉖3u U9yeϮ}7}XgB곡%~jf2,|}=C5o_0V?691̨͟az B>ǩoh,y;{G4`Nq߰Xtvuqn{wKԍ*Mfìmtax#U,'Hqɒh筷jt{׮e#;;ycLW0"X6dkTyѶ8+" C3ʧ~ٶм6A}y;8v|ظq,ɇkC:\eh8)ohޫͿ[wݕ>\E4"WՍt $]`Y82?LO&0뚛J}-f~e]e3 |^l} *f3 O\wN. dm975r,?O>XYwwF:n^듏aq'gǚ,U }ҙ-@?pNm⻹ꟾK?T<7h'7?R0T/ciOG8`&wxh洮o=bu{8GW~pHU^8|HT\"8.Q.DvOijvf3SsuT{l/'ٚᮻ.0$OsUEml,A]"L%Vnqx\X&Xud%L@0, C(J.Qr8xx@,T"oXT ô@'̴g.&:7MM0M(.'2QM4PM"ĉCvz9`(A^3L cș5$I"XlN;ÆeD %,ILٜtECH(I'Mx:ʁ JKM$Ψ,>H'oۃ6."6:L"OK2k+ tM/D;= r)tEXticc:copyrightCopyright Apple, Inc., 2008^tEXticc:descriptionColor LCDX7tEXticc:manufacturerColor LCD}?tEXticc:modelColor LCDIENDB`dupeguru-4.3.1/images/dgpe_logo.ico000066400000000000000000000422061426171743600172760ustar00rootroot0000000000000000 %F  % 6 h@(0`  1"B:41% O;&***( #/e/6~)/v!%c VMF#! " *A>>>A0z$'D2:gZ N DH,  *7< 5 6 6:8&)F29m`S HI/  1(8; 6 6 6:7'*G4;qeW KK/ 6 =;9 6 6 6:7'*H5<vj\ NM3 <!X= 8 6 6 6:7'+I6>zna RO5 <#v? 7 6 6 6:7(+I7?}sfWS2 6'@ 6 6 6 6:7(,J8@wk\ W3 --? 5 6 6 6:7(,J9A{pa$]0  5< 5 6 6 6:7(,K:Bvh#(c-! ! @< 9 6 6 6 6:7)-L;C}q'-h(%A 7 6 6 6 6:7(+H}=F ~*1i!1A 6 6 6 6 6:7 #-4_=E*-)/bR3@< 6 6 6 6 6 8= % ! !! " # $`X,0f-2c3;rFQ*+:B$'L,H 8 6 6 6 6 6 6=9+,, X "QIVGR7>(0,78-BBN+1!GC 7 6 6 6 6 6 6 6;>D6 ""C"{&*QFR+63IWaE/Bx97#S ; 7 6 6 6 6 6 6 6 5>5! " ,).dDP,2qz%0VHk.^^o9>&0&q$*\D 8 7 6 6 6 6 6 6 6?6! " ,-3lAK*+BK/5uy$DgG-1EBZ_'-aR : 7 7 6 6 6 6 6 6?6! " &-3lBM"!#!GQ0:{u$^HG![{f*0g#(b = 8 7 6 6 6 6 6 6?5"!$ !+1fCN#"&'Hc-R %o!!!q i"u.2o*1q C 9 7 7 6 6 6 6 6?4 $&) ,1gDO$#)&JH-q)3y  #%F(,q/6{ F : 7 7 6 6 6 6 6= .&dwv:f|yW -2iEP$#-@Q&oM&&+X > %U? 7 7 6 6 6 6 5 88AGJ~TPB$.7oGT%&#*7GXh4E a.C)f! !&@; 6 6 6 41@BGZ}lzjxVQB$,/IGe&:.38@JVeq4ERm;Lw6b%(ax">;41 =GH]xww]cRR2>=A-0 e D 8 59=}zz{nVV@+YA*W?(s^te_OÖk[UEO:%~sakNV(1$eAGC=!su{B-&(^C`xS < 8><6Z]imcPW?(]F/V>&vٲpdMʝqbXFM9%m^J͵\KKA *5!7NU5GQ 4EM=Rd:=y)C=Q9e14d H 9>1  Jr{wskXV?)]F/W>'zhܺqXϞpteN\UEg[I®ҢsmOd!GahH#;4=*9:@V  W @ 9>"inaMU?*]F/W>&viWյhɺҟnWu_ZUGµ٫}W>D>!?UUG%!F$Su#*Hq>Hn N = 8=#͙k[FU@+[C+_M8ʝpոЛhXg_NQ>(pgnco!AWXG%,E/@(,S7>a H ; 8=""!$δiYDS?+\F/zfѬɾ֧z\oWUI7V=&r0<:r !AXXG%y79m#`+0_/5 X D : 8="#"%Ͳn]FM9&}mYĞuxjTͲўnzV[UFZB+U=&zײ{Vii[ !!AWXG%||ylA|0H{4,2d)- S B : 8=#pr[N@/ӡpenǡyndQVC.]F.V>&ziˬ[dYF!AWXG%~xvm ^Ei%)O,2e%(w O A 9 8=#}~kS}vg޸\gznXwvh_M8\D,]F/X@(xjX̱]PNB!@WXG%{rl^%NCS2 +1d"$p M @ 9 8=#ךsVᾚ^dpiW\K7[C+]F/]F/ZB*l]Iɯ]483!?VXG%vjaL*=>F# ,2f k L @ : 8=$PdYVIӼ`\LG:[B*]F/]F/]F/[C,cQ=ӿ\! h!>UXG%paS6/38@|'-[ "j K A : 8=$QH:ɲz\bPK>XA+\E/]F/]F/\E.\I5įwZ >!=TXG%iVC#243;yf',X #i K A ; 8>$[M<ɱtWiVQBaH0_H0[E.[E.]F.YD.sZ )!$ěmX@{~cyxseO}^>~_?tW:fL3[E.V@*xu]$8OXG._$@%)# :B(,]A!%K&+nQG@=?"χlYB}[;dD_?jLdD`@aAcBaAuX;ZB*le /%. -_CmBXAK>I3;2 +n#(\"(X"LD?-\̔sXlNkMgJdGcEbD~`A~_?_>`?uT4|co L$&4I!/Q+Q+Q#G !8$P#Q"P!P/Cíұ鬘yne}^xZtVoSjNmp!&$+FfƇDzŶ߱{zaFC8 9[] ]&` X@  ?C??Q?@? ??0        000p ( @ 5e!%J=2#l#c..-"m "' $(S$(dMB*  ,978(#&@#-3wp TJ6!9 : 7<0)')B!.3wsYL7%!(9 8 6:/'&)C!/5|z`P;(#A; 7 6:/''*D!17hW< "(a< 6 6:/''+D!18o_=+; 5 6:/'(,I!39vh!>1 ; 6 6:/'#%64:!u #:/: 9 6 6:/% $  #>8?),""0#_@ 6 6 6 :2K " # "1%(d7?8?.3"%4:S8? 5 6 6 6;44$J4z#%=WCM.:8O6l/:c"pK 8 6 6 6 6 5?5"^ !-3h:D&)K$*EM;c&k*9 *w #NB 6 6 6 6 6<2R06k26!;B(+K &JkG=<6G%%PP 6 6 6 6 6;- T06h49$$=_%Da!I_OFf)(\` 8 6 6 6 6 8 .-6=U16j591#Y@#Ut*A#(` @ 5 6 525"=mmc{<2?s7C $/BY):Ux*H  O8 :31E[ZciejD $4T7_?DO]m(;M`4_-4%  !@ 2 H]XclmQaK3YC-n'$\$2^7tW\eq~ < M`2j0> J:$Wa[xxl|iM]I1iTeiq{4-n#G]o9^ a 8 9=yxqsWYB*nXBrX{gG;*wg 1-<3Gj7y9w6 w}#;08V4Z%& E 982aa`]I1R8"u`{lI>.pҮA9-,9 1B.< 2?5q//0o"*46Hf =;!$$'\H2U<$xePǙmxgN}xj޷jM(21(/_3:#:I{%"6L/5 R ::vT>(UB,wȪ̲Ȗeo]Fo`N{h{tq0b2$9y"%6C,0\r&)~ H 8:xN;'qWoߴuSQB0iV@xa&//=0b2zv,s5O '-3e "p D 9: zßeUBơ{ltygWBV>&^L7εa]0a2re0X.2`'+Nk B 8: zͷqΩf^WF[E.\C,YE/¬~^ 20_2eL3?((D&*Of B 9; {}ugSvZN>+]D-]E.VA+wgP 0]2~S/35$&9%(Hc B 9<|A|r^ueUE2_F.[D.T?(ncYG1[4u;37!#1#'De E <=!)psU}c~ay^@}^>rU9YB*y_`XH$@]f;k8I6<.3u"L3^"&\L@5R&`@`@_?]=]<_=tS4tYmcQ "(&3/"%5@!"0@#+*%2&@#@ - [­ñѣ|圊nxXdmQ/'ë9TtŴ*43-  @@@ @ @ @ @ @ (0 5P $K;'w +-0)!'+\jM5 (t;:6I,0ko Q;- : 77 O-1ozY>$0 9 77N.3scA)4 8 77N/4wo E17 6 77M15v"%F"_; 6 77N"#$Co),Gv37''%&;. ; 6 69+(o"N)>/+.\>I 6L6S*yC 7 6 6 6>2 ?!"4T4:.2m-Bu0y2(GaB ? 5 6 6 :1!2!#1N39-.0Av"'.k,i#$OL 5 6 6 60!"%."!0L48!(/k0n :kY 6 635&*Byxj581A#,?Q7H"++?^1,s#6]#'34"!G_[_oXi93(r"5HQ7g"RSdw/+o0Y%){+'))=EQb[bzkWs]B[F1kPTM@%,#@X[<"jhv%2)Qt,< > 5AESllU:v`G~foUsaLj 0A0&W4j1|2%HYu1Hd 9 +4&.7MXfR;kR׶rPUE2tqse 2o&u0j(-Es(+P`g ;5%*1S>yeÞwtobP9S='wW2l&~]1E!$I$&?ha ;5)*1U4wy]M9YB,V?(upbN2i%m9022#%>h] <8 \ߢu{_L6]E-T>({`PI<2c-^&+/2 !#8`b D= \†eGuX}_AzY:gJ/lRJF:"5LG0Cw/2h#%@|, !>7)μrݗh|^tUgGtS]O>5ɴRîoҾ +P``(   4: J.} +z0#?(,^j> 97 ;'+/cuD$Q7 9',1g~M&k 8 8&,-`"#V* 8 8(  #  ".-1d,=%D(.R6 6 69*!1),e-/$#6R-h/x D ; 55."!$*+b$$/`!>E>"-OE2667L?@:W ,Aw&<agAMA a cHRMz&u0`:pQ<bKGDJIDATxwWvy+H݈ 9gdOPW $[g^ײc9jY۲׻eiֲ-3#MHh8̙I 4:Vn4@9Cs~(tuu}I|/zcd#Q{ G%zoo6 P1 ljFbFKuWVOUW׻#KK8O=_3; *}n(q9q`:S).׌lVI6hC{n e9;x Ș9@DHx38L ],׵(6Z^cE$`%!㻔eϦEYXX*иk{yOu1 &ځBю(u{7R--Lw:ǃ0~!m@SZ=N|}}gĜMe$BJl[}f˾r38!$*i,LWL;Mӡ7.֍aFHcTZ{h8|S-x6^>iㅋ?ՄZ+Vњ$Mn"1!7 ٌc//(h@7zGH:O u Jo* KD"c\1π/><=#fݔ1Ҷa@Hz] =YNI.a!-ϵ)r"%!P.8x~,'&F'] d)EDa8D!Fe6\'=( %%7`"H,,V-H/R 08ThB(:8J{@T HRnu &fwཛ rr-8m E/–)Slt] 1 QqP1n@4WHZ%ƣbuC>A Ohh6LuEQo}Vim9Ƥ,:R$CERXM)1OY+chَO xcnA,XGѥۋ6:-i6ϑ86 $Q Gx"! T Q!c0X̖I=6oY H]b V]T 88~j m0QeIroIGߌ\Zk_bُ]Ly?x菈V}݉0q0Rixx!FHd"NIρ8HC )rHiMZt#(UpC|{eD~D_k  27 bi[ Wi]$5),KR4Ի#a7}@!BH׸kt߯B"mR1ka.(e}sW0`%l)z0196 wR#A/NlRИT8 D Mq6:/5\s\!l:ֽiu1I@f\I=}[6XiTUo*uW1P3բIBRخG)ٶJp: @1!Mz ioIw2)` RCP 2h'g.YImN!0J@C7.PI-iK.I#VI 9v ,Bz( IQJ"Ae7}MpHY#cKϧ-;#7}sE*SJlN ,MU`j DD0  Rfve|;o/ YĨ$(8! Ll"1R- 2 s2 eݸ:cr$ lK8. d5@lla&D cT?Li|UJMHbſ흛|}_Xt{8R#unjulfNS(QI& \' za?-o9\.'DcIx؞FG2g(t]t*4 ͖1Ȗ&!;qjaѺO ègKǀڛFLn򙣵 k zݴ!{4粸EHL=\pk#y n>mϰ E7#biL`[ʷl uM]p3="e[ug ^plIֳ]YoK^9:Veɘ/=?\xqK֝! l[2Z-+iưr\l#{p[.dgk"CVc0ƎZ=)aB`apKTlJ[K4+k9h/}ST:Pm|2A FL5Gg9{~M-'? 7i(Z5b EZ!J&Q]ϧ2LNB <@ k@i%Xd_cemg6uqqAJ !'vjo^|-ԣ~r0RHi/viS)88$iͥu~E~=Ι9OiYO*%}--Vz>8a{_Tqk1d|b@C]VW/rB0MlX]M . ,K!.Ne|4DZ,* C&M@b Im J\XKzm0q RHFO>'5#ej*/h3x9~/5S,ǡP*XwˇC]Uhc(|2"Q醊v/&B8(E/H2YsɷWm|`rv5&j=cyNj#Fq=ۀ2=ú؍Rv0I0J-oױx,zىuk8V'_$9pG"E^~L<\\$]Vims$\4 :4V/jiwcI׵Z`8] a!{6i!iIy{d m|u"[ƶ,$l7NBJ8AGH}u'𕯿( z$QH`hq /~/#wcM/Z;*D vK yHm 1TyNFE/Pv{;\b| 0[6aH+M{"섃Fm4s2jlm6ma @ 1 ֪'bbRMgk( 逰T >1#G_ x%p=b CZ:i=Ye( [ݱUhmBD ʶ/ѧ?v8g?m_ۦ!js5s̭Vɸc+q<|BK!.+ZǪ If}㚍f7+Cwwk%fz3cM me ,KCmlOr灲Sڈ)Aۚ]juxK&Ius;Ƽ(:B%1?eI}W.?tD{ĖY±dȗGQVV+|}y݃ƒrH bIB)+r83FpA5pQ'H863`&}Z]B{BG 觋 Qh>sl DZ s)`a)KRr`ئE+Q`I5C_නu(6m˕(Np98  -)K$ K\j][6`sW1 {ÞB*#߯'4 F-i_YbĸR-pH+rƠPr, 명 g/rX|Dz]ּ:ɺ;6#{q%J,{w|:JMcYa d=|z/K;so~__w@ jhzf# -Rav.#~ao%#Qb8?7~4"ER 98!Ug-L,Fiߞ6I6^Nkza]t ÄWo|i>{4 K,~)x LH"27{H:52.C)Mַ+lˇ_~R2*e$ʲw s3r(Xdz5,EHá*%@edSבAH&[8b؍VkP[EƈשB8maٞ(\\'V7ayϱH :Ia waD68X|/-zj..*Ղ6Fktn1,`Ͷ]LAfs|蘙,J=B=>YBb6/ȶ,TUM<9<=K \+SEAgJJ$a$f%h XRb"uh.ū(z)8Fuۨ(Zf}0 $A+Hy灙QJ!:j 84Qų,\&6%C߀cHohyHJy'0xqe[A˺M@wfriWqШ;ϖH*/5VO=Jjs!^!P6ŊDeeZ%D6f{-f=sO6f c4[D#Ԓ B.ѝ?G RPc Psh3lua \K|}{ÇX9<=S:{Bx?BqedL\ #xBn*/雎RbD-K,J&K9Aik)ڝڈШjNH|FnX J(FtCEӣڠ̾J73v7]g.}ןŧzs2Cf X<eKK|K$ P[mCo=#@%$K7?Ce0-d~m5e¨adRI`%ܪ[0ڐtCzkM&QK H'c&$Ckj0+s140 Br!-F' *mMt KTHގh4W/*z^e-?y=SSFAtH/CTo$aVm)6)6Cނ:8.?·,9<c3'/_5bXR'$x"  *VݐL-f{,VcUhچaF٥QM )+<`iT:r51>L޴fwg_xJ)scY?|_}Ft\`(Vծl;^o~ L$J7.18`Ͼz1'B~\^Fm6{Oq AVJ|'V- )dpKYT/v$tb|!\;m6h7O8Ph1`.~6gbecMt$V$(lيiH%ox`G;:Qع*KM("!e~0Ȁ'3(FÅ붻`wWn?:a`]r^n)2Uutz41ft1ͮpaag:DҞü8 aFQnp~_|(Zi>?t\<~2OQ4_7_ a,.OEJsNHX`ؐn?ǐ0.g0}=bW00Z.o+zyo1sG|DJiT $R}RDؖz͎TGo'ӿjO?vZd{yk2o9>TZvܟ2v.m4mQkJ\\Z/r@4f0|kdO8'VOsސG^ot\Spi}TgM#{|?|f+wHP]TUDQv.Ea֊/˶ܪۦV>gۯ?K?ylDytw3HNHvyKͼD)NZ͵Kٗ3e̙SgD3Mږyf\p4N̞rg\_-@_`wT*9O؈LP&PAS;;xI bPvq7kk Y~瞕ۻg1tq++cPRbyH)8RKʧ%|xظ|is֗?֞#E:N.qc?^$=6Ql6HA K+mN,҆Xk~n@6c{ym^y;&;>s_pQoV[NBع}JM "X_m2yg<@Jq{Q6(G*έišr Ty-TbݵAé{%YXr9g]EܿSRξ2ٗ+dj𮻦hBj]H`*nDŦCRË Z,_8ñ Gn~#̿i,Ovk{H# V6O萄ɕ`$HPpm(ĩd1IL,kܾW0:*[tEMC bbhb|Nӱ&b{{ArݛCeh4'BQ?k$v[]m .Xmn6$ DYCh uQ9};=Ww?l3W s pX9=32FmZ$Hq^gW:l{\]bg8$F{Gh2'?*#Z͈ T=H};VBR*_>yq0p>z#M}UoeǶT.am~V^Xg_Z,/[W`XZ;~X i鑱*q|Q-z3FF)i1t!O}Yp>X:yfN]Ҭq*##.13/Q& Ny?rh)c "㏔mT\!FӸV/ pzxص ES(!]bdE8>=fX k_RtWwL HǢBU7* R8-d+\,| ߐu>|>{:ym1&޸TKϾX/ N}NT*2{HrLs “'=s`}{{&Vւ+5Ŝxڝ[7v{VWO_|߱j)}F=l= Ў:U!o}$ d!g/v,N독}[??zp:St`G>B$ȜO{yCok5]ymW٪)쬇VRG*U]iun?=Z{/~vo֏w/UzgNr7Ne]yjkS Vڶʌ]`o+id&(LOmf??]Kr,<.~Hl%]8XwírUܐM`$ (|5%GQ "x_ןU|R^g[Z, p]&&&l_Ūkm^> "fXo !ְi3yq*Gk*9NͿ=Y[|Ls4XK%iIJ#ť:Mkkuz"fF3ئn5xedlūPQ-%K&8WV$V_k{/륷D ib#=;{#wVx~@XMOb@6(Gql8plrDaUʡ"EOQ'5G$I~=~ZTnUxs3m@9ag_8w+Ff}J!$iP *-JP}u\wxO/.Mm[~_N;R1qbbb#έּLzkV D$7mM)ufh'pຘ(:l&Z v;2zbݵwqrZ"R>u,|%>^lä}a|>w{'~a(c>צ:Qkʯ4|\7އ"YnHo1'eR Ce{~/I!R^kߐ:qj郥|,-D+ L_Hסtxʱ}D-s; +]%i1ZBH&Gw={h-W]֞l3U& pf`F' A+Mw1a_mwer)Dhr#iتTgϜ_&G\7V8n>C MU)"Xi|f{1_.^—Lψ&~=o9gI4LH]t^aㅬui⾹ a 1gk3aOH8Dw]u4J6 D҈a1#8\oK!Rq3;N5Uep+7="_jwߑ0_fǍŏ9*cs9NVqP6va#l|a3&Վɱi_]GFeKD'TcM=#q/AFw:{dw︛,ګ T7${/no%W*Ƃ/N. N@,Qѻs$p*DS͏ZX>AOk{ɱ+-ZfED1V'lp]RfՁQ'H Cno&- !@F$w:YU;y~NI^>Wz| Õ2wbc+k#̜Y C(_%DL<{j,z_]### f3oW$b(TqCJVc-)7*rzߑhw'uzhgp&B\4fдqEbGjoh=Y2k/Msjvt-R3Np(eb3J<"\p|덇$<8Hx3,F HaXpt:;LnI0;5ikߵ:|=GﻝDkrfM9ل2\T;1^:kSg*ZB???PT/wj?Z6$pk$u(ݏJ &r e2u)Dz hڭS# /x?8=cINTɏsa`3,?sUl|V ! !l&đD(qk4+..R*zxIDMF1NG+M9DQuվd" l.k:ٹ-w~tw5ܟ#]'#HLK`f?;R#CA5j1vaN3yb>d$,T*\rO6 } :>+HK" ˱oWuYͯL6,XOjz?kk[6鎕h]ZvkW dFRfZaڭmJIXN8 sy~‚/(_OcY8I3\"h'S_6k! +e}9X-$s)XT|PJٮʍ$줃3n>K ?YvKe "JHLfBXubTd6W AKE cӗV]/PkWO*("E3o^s+y,Ng1'H4"-IE(u!eU-aܢ-czv?='?d돩W^ǯBmD6VRǭ9~l Rà!nwz$$뺔0-RN9=;?-?Qf: UnMK+@tch!2-DܯM1($͟H-87͏W*0ft) ~=cLFszyc9yFg\J#x:VT&1$~i z0gv/6ezWr쏛auW-"3'bz/`v|9/}C tOh.Crcx.c\Hg~ӳ#F;!6+>fsT:L[kc#67ٕ&OϬϽR澴d1ٶ}[zhǐM0\ vqlײZ?,!/\ b?~q=Ԝ~s]Nz!n9GvXf߁?Hj0Ob7:K#Zq5p2lpq=l ;(]slK&^kY~85;?ޜCqSzKJgJMnLڒf48Nh˸ؖ>y6y<ƤŝU^F !q81c/cN o6 F 5|k?̦n($Ja,p3lg )t %Pڠ-)Ƙ/Io+BHW}TR ] WN@ "E%AI$Paf? Vh9o6;6 #RcNwHi)ŝM h (&֊FxmKDM"u}0oݪV?ڬb@ lJLaշ M:Pd#KLhY5zaumޯw-c?P V=Ko6D Fe-ZW%|w#םWY%AϟٲG qc@gK*y=&>-6>0T2C>@(B12 aV;x׷pl2N7ܴ̕L v Kʤ{ұvmu&}m ͹;l-ɰ% RpFP*U8c|R ]u[aͧR",u Jor0!3T@5R) E,vJ:^lr}Ύ%6Glf3i R@&oI -zW{qҥMmx4077Bo;lNϐ1` #D7MT;h Z切7­R߀L'D R PU5m0Ttmcnm2uHlv˱9'!,m+R;`.gcR y0a1x 럭 [^VH)H[T \9j)D⥼BAf&=ta0w_wUJ9v2;=]7n [[@ao,A ˲Jc Hqͤ0 E4`6 he3|’UC="1WkeMl /MIf!P$а=Y%<,)0R\'۲.ڬjnk2\tKpE;*f5Q!! ? m_JV;k7?-mɠW!;HJ$p1^86qMCH[D6ffHM0 ߯ f+iiYT~?\@ZH[DÉMIpű}Gs]hS[^)yi33Xݛb` C"-!D Wsg>鋜4ؾүz դ-6_%ܲ C7jBX2z=!bhuwlFum(7p]z`] EZ M:eV芍'd  lHJGf Y@3;.@qh`[[%m2̒OD@ dtHw%q.FJ!-!@Z;"64 ^-Aß7K>}jqSt X#%8&RH)b&֚DID۶huCryXW;8M;ap0iP(Xem)tx9(Gg_9w߶IzZgg`k`PnU7ؐ{3^ηpqq9_RK\0mIq3^!2O Yj4fk$ QL'k aŊ(IࢡVXUܝd_]4>"QA/bbnHly4ow:F8u֍N?cテ]C^rR`y <;?{<0=v~(%ȃx'i ڲlev1ƜunsxA iw*q2iƘ&9:Y682ByOzޡףft{IENDB`dupeguru-4.3.1/images/dgpe_logo_32.png000066400000000000000000000046541426171743600176210ustar00rootroot00000000000000PNG  IHDR szzgAMA a cHRMz&u0`:pQ<bKGDIDATXý[l\G9=gok;u)A*AUA} P^**!PBBT YUIIMq]9{3C4i.ur4:3f9B\<}?2momzR[]M@~< :ʚQ^sG}u]۳OyM}j{WVD\[Y+Fر]1qQ#&l>z  E=b|ǍI? *C!dc\{mg/2wuibn.@BÈ}0r4#!@Pfpu{2~ϙV#JR"̛VBH8~ L8Q`'6=|J8 IhgnlV[hY @aE 2lǩSHрWgAg.XnM:ñWSd(SvR>6`! q00їݙB9 .&Ry$ؐ3kXm,;O}VK*̫2kTm[&R^i4; . l|eW׷γȢSVA2dJV"D2 9LC L˅'Iqu]8+3N%Δk45}B0E7j{K>2_nTzT*^c,L;s[6NOM'FsZzY4< nݲ㕹ҿ9SK}q=vj170؏Ѓџ=={FS'JS㝝9͙jvvetp0d [Wq:d>ocCb ǀF=: I4vȋ,n\ij(9ON6&pCz i~~ΡA.J) QUT>sl=痗?եHO~74,@ ttG݅'̯2:SoKfb2j.qJIgmEef7CO=b3SCvF`|(o wLd{\{јyG/#@xS\޸_0c{vyB~h۾7=p_ƾw?n4֭x~,dW\CdP{xm琧Wj?'c8,j 'bgQsB\0T%d7"S,ؙFs8ν̟;ubuSZOqY&!+!Ǖ+| 4[dN@i3߾wws˹JaڲZ9?ڭ /@,Ybp`58uS#r8fQ|v2"$D jU;yx߿#̸ >9G @S^qm @ẅ́o|! ;dK@S!pH%\-~ 8gJBwKOp )J|AHWPE<.K` a.˜&[X"!P"!Xd?j Dr%I!$q VdY3FfvS3bUwC6#JJٗnu]'lv+ }0,ZF[/@|@TVw_fmD)(2tEXticc:copyrightCopyright Apple Computer, Inc., 2005t|tEXticc:descriptioniMactEXticc:manufactureriMactEXticc:modeliMac IENDB`dupeguru-4.3.1/images/dgse_logo.ico000066400000000000000000000422061426171743600173010ustar00rootroot0000000000000000 %F  % 6 h@(0` "J.+(&" G!#<"###!R! #'O&,j.5t'-c!'W#ME& !<'/?>>>>( X#%E6?k]QGI8ok>: 7 7 7=9'+K4<k_R GG:|!> 7 6 6 6;9 (,L6>pdW KH=#? 7 6 6 6;9 (,M7@ui\ OK>(? 6 6 6 5;9 )-N9Axma SLC-? 6 6 6 6;9 )-O:C|rfWP C 2> 6 6 6 6:9 ).P;Dvj[U C!7< 5 6 6 6;9 *.P 6 6 6 6 6;9 *.PAK{-4y2! TA : 6 6 6 6 6;9 .4b@I##08z$+E 7 6 6 6 6 6 9=! ! # # . Q'*Q(+P.4eGR,.39.5n`,A@ 6 6 6 6 6 6 6?5'(($! # %M*/N\HSBK-4'03 .DP%'N-"O : 7 6 6 6 6 6 6 7>@B? # I ##J&  "E9BBK%0COU6m?Y$A#MH 8 7 6 6 6 6 6 6 6 5;@%AH/5{3:& "E:KCb IXg&yJ%Bf6 B!&X < 8 6 6 6 6 6 6 6 6;?$B L2:|@J?H8@(  +C:fDo ~G2zX7'.dD 9 7 6 6 6 6 6 6 6;?$@G3;}AK BK;C( -SF F5=FP" ,=N](zO#$(Z  E@F 8 7 7 6 6 6 6;@%A%H7FGV"%#'.:GVd#xF49I9b1! @3A 7 6 6 6 6;?$B",H6VFg#;48<CLWco"~G6/:s!TzAf),l! "E2A 7 6 6;?%B&2G7eFt#LGKOU]fpzC9.:rTsF24#(d<! #F2@ 6:@$B*8F7oF#ZUZ^ciqz8B>@ .! #4FC;Wf!y~?4E0@7Ck('a E 9>6!#c#a#ba " JfrG$$F!Vw#;\|EVxU ? 9?"! !*6+:l:Ah L < 8>"! " =RTE(@0kA3:z/3 ] F ; 8?#! "=RTE(!{F%Eg!8@%( V C : 8?#! "=RTE(}~}v)tEz)=&.8A s Q A 9 8?#! "=RTE(zzuh5n=X!~*<5>j M @ 9 8?#! "=RTE(uskX>c4={Z)E2:~d K ? 9 8?#! "% ! &'.d"%gQHA@? ! # !&%PAlE]CNAK:C I<3$*\"(S#I B=)W "-7 *B&B&B#A!5"B!B!B A #???@?ݐ?????@??@???P@x ? @P ?0??6???????????( @ #2a"&D!92$!" ()--'!<!6 ,1n!$tXL9H'><<=%'+I(/5zo V I>[,< 5 6<('+H&17|w^N@a0; 5 6<((,I&29}eSD e4 : 5 6<((-K&4;lYF]8 9 5 6<()-K&5;r` IN ;; 7 6 6<(*/N&6=yk"K6't= 5 6 6<(&(>"8?y"&L1= 6 6 6<(,! M$&B %?;C&)#&A/; 9 6 6 6<) '$! #)W),l5;s:A044;!!-v(xC 6 6 6 5 9901" #!+#M */_HS+34F-\7GQ E? 6 6 6 6 6 8B5 `"(18v4:"_*3]:`[!v?!0@9O 7 6 6 6 6 6<1T'3:u15147> ]-U~=>(\y4!%] ; 7 6 6 6 6=2S&3:v384:8S#,^+t)m!"?'+jC 6 7 6 6 6=2T(5&e&O}+8v_'= 8 6=2U)6&6`6hJPYes<*j#Rq;\ j4_(<>2T-:'4j4u\cks24u{ >;3 ^(1U7M65yI(tjt{#<7Dk1b0= O 7 9C4 m .>Zc*[/[:/{21u&-6;\t @:4+, !#KdN;;!9Ft&-EW4> Y ;;# " AV=:(9.-0[)+ K 9;$!CY@:~01V/5h w E 8;$!CY@:}{q8o(1P.4hj B 8;$!CY@:wo\;R" 1*/cb A 8;$!AW@:n_!B8?(_$(-`] @ 8;$"@U@;bF$*6:!F&+Y\ A 9;$!@UC;O)$"39{7',X] B :=$1A/:s8[3:6<)-_!$F $eMC:!t#!/DM,8X},/M}(*G}$B"" 3v /~,~#h ?@ w   `(0 9d"&M=, ! +10#i$'B+1jqP?9"8; 9=-.3ps SA?&J9 5 :-/5t}]E@(b: 5 :-07vfJ=, : 5 :-28{qP;2 9 5 :-28w"&T=: 7 6 :,(&%'Zg+.Mk49%#'*K%v> 5 6 83(m(f!'$9%17s>H!3@5P""0v> ; 5 6 6 9?) "$=g6=%'C1G*j..]6G 5 6 6 6;*#% M8)%;"?!!CW+3-*Qk'%Ae$& @8$e"E[43~3|#+6Y*+Qm ;9%l"E[43x l5T4(+Ua :9%l"DZ43j#M28%(S[ ;9%l#BY53zO#,02m$'MX ;9%m =Q28t'@-,*.]"&F` C<$j"( )?bz2;k-0\,O'0"=4*! p?`PP(  "AXR5*5*+0fmC ;4 :2-1hyJ!G5 81/4lT#`7 81&##E/2g!#_' 9 71#$  !,16s,9#<*1\4 8 694"G*.l,.n" 0K-h0!(H = 5 78%n ,.j$$)-Q GS>%=cJ 5 78$g$, .;w&/.Z1%AaG5 98$h3C-Z&__{-.b )x,!R3%m%Ort6z"x&-i)8 D5'6J%F],,,)5Otn 7$N!"E\&+-Y%#>j] 6$H"E\(+}!]+1c!!agAMA a cHRMz&u0`:pQ<bKGD7IDATxieu&LwԿ d 1Pj E4vNS3a׶Ȫտkg٣+ȍġq?MOu~3!"qJ.`ـmU6Xk3mSGЌ3<Ӟ%Ͻ_i#GɋLJƀl $8 М8q#9H8RҖY1y=Gl Jy7Eie?y0m{:6 *H#@f] 8ݺlp-333K*# VcƱmJA'z[ȴFf((\  Vm`ƶv? $Vʻ) W[ K.^x}:Ҡ2( >r  ϑF)X8}93'e( udQ2E ` &@ pZU$Cŵu HN+ݖ[ &8Ȥ @}b$2" c8T0S8¤D=+M+q9^FBn+^f"Bi#[!.8? 6 l@:9-`'f(UR OL5ΉcDry ˯̉ܵrMd9ȩ[1׏thdc'2!88c)JY8`4H b ddSWLX_nBX1 ȕ. ,7wF` !8g@ձ\L  ^- ѿr٘ \`b2 ^hǶqB0)sfd,R&tr02Gu rpLܴYXiQA ``P2TAOds,b7-@!`O Ga@,]@ppQ-؀K*N"833%ya6G6bur %Ln D hm2 )l 7EZsc,}~>["x?S$$P D ԅ eeY(%x3hQ`7v Zn2EBe|F#!i@ LjFc%$\׵f%8aHz_ʭ@^q6`ظu4`kM4e7aaqeLUY$ 8AqLm+0I 7@F䝕1L)%`r,nTͪ1ws |P2x_=yMw<,'ftFK ,,̕C䶄t}M' 0)pi:oVQc-(r!aZk ` Far{ASHbHEր`8%L1x36777 lmm%LG38c*a[@_&H 1B r8U,,@: M`: 嚅5Wlb` H!/qD0_Ia?<#&0C ϖ[4 2cC#??Ʃa& #Db 0 49@RSFcV(U@ ZC2#(,.hyzqq`mL/,@H&5WX uLՌTkH0XR%9$Dާ^n} p1\ h4]mIO03K5xAt6C p l e`O&nNNk0p "0*[] G*̓鬆hJ)x@jN泇Ò2w K(GG?N P>nk\O|tׁ~%8OSQ (jC86 Nqdfp܂5 wLL r  '&A1ڲn_{Mn O]hz9`I 7'BC>2kٚ.H/2b[V ^~cJ%gԃH3fp-[[ʼn9T 0n%|lQ@^^E-ϝ6Ԙ`ɠMp׃,)'#cZZk׃e2T.l`02`$<I]6;wYܮJLG9p XG/*~|hTW|ݠ{0@'0:">qo` t_,qڪ `kU -@hXJDPQ^ bkV;7F۳s؛i9M&I"N&ރ$G97gA"-1 CecQ.ٓI {z՗a2X^ϋQTWA%k:/~l\_1@^A qGB4rw`F `7`D\ ?.i2Hֈq`@0V{ =[ Xv{r=MaD)J [m5H f0Z!^d{eg}e7xtQXvގ*^uMvZMߙ0T_-zl,j1i8g G x׶6JR/Ru20Ӎj 2 Wv=hra.3rt~iM-"jojj@מ"뫈֖d^U8#'r810tCVƪegqn6l[u2Dhd4a}^Q:ɠ*JaRH6^/ʵO_i_bg]=nLl~dR ,TÞ"0iS{,E{B?|IIRh1P+Y㯸fiY㝟=(dTqA`w1I9z ’IlwT** y<$OFMk5tN+m6vmo1LCqstdS5ρFߴA*\0)<^Uwc~} eYD>1E@ T?bFBJ-+;HeS>Z?οu-ePV;n7]j L}AX2g[9ĝ(?DFC Ŏ7 m?+F1T@ƠU+1-W^Vϙ6,\񚆭dGOɬu(^*g ·GVssSUPxJٛax)Uv,:ztһ o:]<N[H6, aO@)t,A_f%(0=crh e d}5]zN;WSz/9kbU@fhh°< Ns|nSZ<_6F}-ʲomN^V8S` Z!*X[="ꖫUU-~vwz͇]82,JYN2d~ KЈAk^P1CZTZz(p5RȝM񍇎-G2?@inN PC ^؁־SV}]R{Rrmif[-±Aն X2թ*w;xӟ vGzsOEu/:Ӄp:N2 (; Dh H t PA ʗ?yNj靏קN"iw`UglHd -A1W6vժ/Uʕ?5 _e=>^&uj )Ȳ"ЙUva :F;E$kw'x[߽^/Gs騃Ssw;(=̏"M0YS(-HP3i&=\,K(Mqoo8= 2Lwt+\[J 4.$CBK6T@3P~R1QvO5ǿwnvJA!֠:S\H^p)mەzݿ8.za5o0SH{!idQ,H` ܼCHqfB \CrLZVM6S\wC?WXavj[87%-~ Yl i0ō$Ja"Ԩmn'Ԫ~I;7tA9H(wf+6 m`U a釧, PwTL;ȮN;wjni,ʐv#r%s:Qq+A FeQ iQ<[^eN؟nVW;s;e*pgHYB.m տYT?a1KKKl_ wlYV ) 4c *T}y*rٳKn;6p%PvǼjؽds'g} r茆#_=q%(["(P &K`M/YzSox(c_Xu|GZc7Ѿ؏@!/+OL_N(S?MLCx6n9w9@Ht/R)^k:wR aft/M3:wjnVGvĝZH@PC E гf(aO-BG>,sqS\}÷]Dru ݭkX)y/7kՇG4G_c*m6?iY}1TpcAAJC NPv!v6: Noj; nGl cv|"DH{ T6T&c, %0Y {j:Ufqq&jyW7!Y_S}?~:^ܗ_ӌ=uڵoiz0>?ʣs&35H5YP+F;~pXTm @elK#s q;JhaDe$OȆiu1 A0q30*WƥEqלeW/g{p~KrWSׯ_ocW^7϶l@86n̏oVBJ(80oy.$)AzUmi?{q~Q;DQdAA+gPGIaC 7(K`Q؇[&glmԱmKR/aՌt΄ѬE:?7ō2UdL7R Qz'plHK=W΄mmYswp[ Jƣn(}ұ $D<ѐq$D܏DEF]nㄽU:6ںTmfGŐYluuu5^3-~ґj`esҺ5j 1%|֨' ! #/m>4ةsSw{: Lޮ~OA -bftt x? ,L/oں\ԵafIoZReg}z3ŀ H)X:9nk[uߍd;&>?׿{u|TOä YM$?aP`gD`Ax%OQiB.믛JzeK^V񫗈ta0AE AnXсU)T]kw<$c a;;wl)Ty#nw3<Q=y򤿭xNqwϋm{13(? ؠ$`/_.~kޓ~Y6P0/᳎4{3+p ׁ qG} I)7%'e˂Ժ^JTحTTt Я4ꏾ>{fu|N/@E `MT=ڷ#~ YrQ9yNxozRJ)9k1/.--1gffJG7~c 3ж I T`ܔ;0;-As v7Ŷku_ _yH~9d~,FM.`)E$1Xe3]pNx'GW蹂)SsAo㏸~:{Q$I (#R$+]͝&hɢZW ;6<~'Ej0SG 8{qqQ;@,t\z#)d V\m mo\'g?omĒ{!KGg 6H̾ x Gv `j{v]#5'hAwm9tӉzow> zK' j'R r`.%{OAz6t/ [aG5c˟rsq$N ]w|nl*UH$#lh#bX`4\fǿ.nN\z2П>2[)GiüﹹAz.OB}' _⯴g,n"5<_7gflL-oWm]~e3[.!U$QVoG7Z,p\\eF׿ʱ-z5 noj~g6?8'3sS~];F{U` xMp-' (vC[M!8~7;S_~~}~'Ξm!(-L o73ޅ0󁳐e 3_>IkFO%GL?umcxavlDUƉ5k/]|}[T:=5Zr8ը@%T u ia8 V?'%Ϸ]7}JXKCV3;A|sHz硣Y8v ,9dž)AbRo1^j+!.<, M$kW&n )\WmG[$٥nђ[2${mqEUZ喀knWmaokߌ;Mn57zIz nP EGf0s7cjsd>O}|w>q߃2 3׍VyѠ8:Dp:"ٞY.YWkhdQmn7#u1Pk炫睒Ꮃ"x rԦZ_~]3-ĽSpjeNCL/DǏo{OITRSy{ƞJݏ,Ώ{o DuD++VV@* J&a5硒RU=vZV"\8_'՚8ȳJ$M(x{sL*9d K|8] @j};q=t4+PQ JS|^)xz{?0]{??dحz!\]Q JcNb$ZIٸJWjMX97ݔIAV=a07^߶F%94^rJnHm8VEw qs[e|K+JOHƼwvլ/E Q^E2Q72EvsQi=p.c_ ⏞nV#S`)$v 1pUE%'H5'py=V{-UJܕrN`J_pQ'kj?A; y}-W`Hclڠ@5!$̩BVf!Moku|jY'JZ?(yPE""7+jQN2Nɍm!n Z@EIsjϹOJ2W3j'?\~tMpMTu%1EHybYe@^gzeX0Wj|fժI5XȢ$/ VaeJ )o;r-)WJj@׋nomwfzzg>bK­gȂv o Ei^P+?0!U Q\0gɘuJemuu,W-E?:[@|8Kۭ"~7X X`Bs!Eh0~PڊW$'PȾ}Z3_?~ܩOhݽ, ®apUTg`UJ *0Ų5:Td1F@0D]B(xH3od[Dwݵ۽OΖy)<9́F~\pNa[,p)Ơ,'eN¤&l ˮ('x`RlϿ/ӿJ:_869X/bfn ;]G,v#>TYc @)zcOt<ScQu|XI0@+i(C8Ӭnx8uRōJ}*HQ^qAzt -?ny}٠n_ @'W^|7( #i?]YLfJp͠R!T} v2d Ǧ7wJX RԪ4ȜEyz&[M|Ҭ $ ȲJcBZO2OcԄm?͗ d߫"4Mԍ1R~O>*%0/!醃6*_8C㈍A)|iZ3G蛛bmeQKsɿly|T9;UD6w8 j o(97r[f/FiHFW>핃.$-ΘԽ{r3# ʹPC跖MFRUυc` ٞ$ ]&7w TlڑWf8tt&|MʡC= (,.AeM4K@kzzLXF}H{:$-&kʈwDHV PZ#E aPslh5Ä*{~540 4F@f@0 V #iuaۏ11C9<ھYNt. G> Z7o<LC30c IANEJWL/i2ұn7S;;) p1.@#0Y76]&xu3q@*"[v}0#.`!hA<ߎ66{cΛs֛ܴNb"4"L ֩J8xI'-ciuۖOZ~2 wB)aI?CGH)^L{ CDgge}7&Zͷ<ɝ;tFct(0DZ,&56׷7~? C2# z<{GpČe3iLu\r0~ HY= CB}L\2B[7\h5، ~J> $6P(f3C图(j͵cZayJf^y BnJpU/ `(c`M17f3"p!nx= 9|0[FXhc̠ Fc^ `9ʔ䫏7V``!aĐ@@"dDH^Z)¦)v`Re>y?t OƸ!c@(,aD'czjQ.MF0w : F'OD4}N˩.jW+ʻ AG_nȡ00{R#s361Ƙ[e@|-]Q;&\~oX4uzB!61\}L$o:ߏ)!3 ~o0<@ S}sc]Niuﮫyd\|GAøȻ3ET؞`J-SBdO x$;9;8p##&ιf ]˲S`6FDgRђdШ Pȑw+>E3FZdپ. .! q6X}"Q9aor/Gla?aL'kkafoCFkG |"cFeH Ěo2~蓼oBd}qk N<ʌDc7 Mb**{-|&DюqSHy"̘^V$#`ő9^pSҎĐmnhHEPLcAxv%̐v|uh '*&,JG„U; Π2 fCcJ6:t'Q;wv N3d 36ȌA q4% r`NRJS?mmȲv  dtbESQ46Rꖶ `n e[Ȣ:ZCk?HkidA.(SXKWaC덕!]m-4[<7#ۢyOC`,.E]۶_0J0nyX0 PF+6<椑 !xcVkMD ^odUm)JoSX`qڜhΞ~oWtYk~~'D|SMC$5FW "Cq ǻ8IB0ECTc7)"l sqKFOAg.nFVJRy""0&S,aE{HN<a4ћԜDa{}[l ӗC Bsoriii ovJ"7Ƽs+/hU&o]tIENDB`dupeguru-4.3.1/images/dgse_logo_32.png000066400000000000000000000034761426171743600176250ustar00rootroot00000000000000PNG  IHDR szzgAMAOX2 cHRMz&u0`:pQ<bKGDIDATXŗkl\GܻO'n4 jmР6jQ!QD >$Qj RaQ#UJUP!jC^u]î$ Hϙ9gHDQ01<|s0 #(rFh QD1<'1 3gq= CX0n꠻?sOg&T19z(Udj zӬ}P<ܵ O}OCPZC[ ?lh~sǩ$*2+ ʿяeJW\p&>RUje ` XsDd{Rpd") "ka#j3.ˁ+$%#eh_W ;vn4V(|k 7 t zow?KVen\̀vd aEM,+58!̯c0 `D&D>mM͑S|`-?4 "]Ӗ6Z;9#b@8K mmg 2hlaĚO}!}U;>w*Y!Dh1 ABqR Y{zbHGXOIK/X08Lˈ|{ndzڴE+@PcxSUlQrCbI]{v#֏ƪүH&3*%% υ.WPi0"ƀr~?Hnczds2| omw8KLBOC?8Rt7 0PHs_+k_.߫\\g6 iBWOR1N¡ߎu雺Cg~{|Gam^mwj]L;Dc .ĢFR1FЁÅAݝʊO}}|][׵[Cr=dMá:-v.%1/m?zAFx㥋X_wZN2u]vHDJfQ["$""/5_Z۵6JΠ+n=ۗ[6D ;XcC.D`qljOS^ Mʊ/Yf:zf wMU ЁCP BPlzɯ3_+.v]e݊ oq kL2QҌtk @hHXXdc /mkWMLh- HX3  }$F@BTA9!㈫`C44 Q4C)bD܅sjaοXjiw S ^OƂ d;A81yለ`vv_;ceCk]&r3UgqD2M-EQB(3,X[tEXtSoftwareAdobe ImageReadyqe<IENDB`dupeguru-4.3.1/images/dialog-error.png000066400000000000000000000025661426171743600177440ustar00rootroot00000000000000PNG  IHDR szz=IDATxp,aۚ2m.mViUX۶ŵle6vWJכ}ȩ:7EUR}[¬Sk'nr>%bOxY-TBoxͦtb_B ?x5' |ppzgϒRD OYX E8wSEm x\5XZJʶ֧ruvLv0:"#SL{[i~Fn3:3(#9X =o; 829SnBץ:>^6V vE& UyOFc{8n\FI(٠4 W cWmh@6 B;N #?&7d5G6~)ˇ fkG6&bXR[V:*A)-߭ˢq'.}M^]-&Cs2- ޲ea8r ;8N'uz^4''X[k+bK\[{`fc41A@?)e A:R_MZ 5 qfꗗCOR767cktµPj[c^qiy#u:,+KK^E_|<OUgXZ\l²)Z++noIքC+~ m_}Ymxbgl՞/kU]oltqX,rODϞ;C-E)*`duNjFo1mfQhD&g`XQz/0Z_OGlVkFx.}>NekyUm_ ƀ0|x-!iJqO7#Q%JEGA`UO~g8l@(P&Tw={0ZJ~]\j~N3( m 1m}_ cGϞrIENDB`dupeguru-4.3.1/images/dupeguru.icns000077500000000000000000001505641426171743600173730ustar00rootroot00000000000000icnstics#H?39w{<?ics4̯"̪"̪",", ";, گ˽=  ics83]+:e2޳+4;2޳4;3ݳ454++,4:4]V3;::+2edA4,+2k23޳]9+:4V]de2+2]V2܈+޳22+]VVis32lƣljɞ>2ׅF8مQ7܄ n6 ?@ Һs}G G+3 G423 ޞ9531 沦f05:aӃ /-huj=+liXO)kSGYcЀcUCNkӅRՅv ׄ 닅 Ljtf OZr&g* *L"  |{m |@(KIσ RN.S"-R;IUqLkPAM* ҅2ԅ3k ׄ 0; CT |:h) ;! t yx7Fw  w5J Fσ QM -R",Q:HSpKs8mkU0,%_Q#fM jJ /zX p P߀G{A{P"K܂ [G7NƸ|ɓICN##ϿϿϿ߃??p߀??icl4 ˺˳+= + " "+ """""" ڪ"",˪"""! ""̺˻,˰3" 絛˪̪˫ ,ʪ"ͪ˳ۻ3ʪ  ̪ߪߪߪʪicl8222VV+:ddd2޳߁:@ddV+޳::@d]޳4::@]޳4:::^+ݳ4::::޳44444,d޳44444-]޳,4:44443d޳+,44:::444d޳,::::::43d޳,::::::44 :,^dd@:::4 ,d2ddddd@:4 ,ddjdd@:3޳ddjdd3-2޳ddd]34 ޳d]]::4,V޳^dd@::+]]]dddd2]d]2d܈2d܏++VV܏VVVV܏VV܏V޳VVV+޳il32 ¸֐ 濱ZH8(y ˺_K<0x ƶaK;0y ųfJ;/x ƬmM;/x yR/x lB/zŅK5T`<6<5m׊G6-jvԿl5kN(d˱}N17/dP1767/f yG767/fnD77667/e [>767/d ۠F967/d ߚo;6)\ 򗧨;767?bY Z560xf06/mT56/ xdKC566/ q]EY266/ ~kV@s066/ xcP<.66/ q[J:.56/ n`M@3<<4 yyvӐ f?"! e tK'  d }`E)y e ڄm_Af! e ۈwq^`% e ۊ}}so. e ۋ> e ݊[ h츄 ;~tzI [yedmu#VgmWU^gukY5PX?:DPatF]6 Q@#&5Le7 S :(DtI  S :?;0  Q 8!.y闓$ P 3sl\l P T00FVaQ  F mdgA"%  J>}y W?  /    %  B  a     %% 򖈉vvuӐ )!d -  a .!t c .!` c .J  c .9( c .*8 c ."zO e߄*Cx 88D0! ), [2  !E_ Tg4" &n<W3N4" +~,\4 N4"-l4 Q 2 +hF  Q 1 ,st- O 0!(sR*v  N *mR%+ M K' AN  D m]a: !  H<~r R< ~ +    "   ?   _   }  }   !  l8mk 8qKK)S;QBQDQ89Qd*QK (,+NZ(1k^&wi/3ty-/sȘ-.tԑ1-t -p*/p)Ў/o*/^3$g8Tש  / Jyo2 vxiOich#Ho7????ich4:+""+""""+""""""""""""ꪫ"""++""""!"""""""""""!""*"+33"33*+"";3;몪몪着몪몪몪몪몪몪몪몪몪몪ich8 ^4::ddd^:@djdd޳^::@djd޳:4::ddd޳:4:::dd޳44:::@@޳44:::::d޳44::::::޳44::::::޳44:444:4d޳44:44444:d޳44:444444d޳44:44444^^޳44:444444:d޳_4444::::4444 ^:޳^34::::::444444d޳^4:::::::4444 ^4:ݳ^4::::::::444d::@@::::::444d޳:@dddd@::::4:޳:dddddd@:::4޳:djddddd@::4޳^djddddd@44޳^djddjd::d޳^djjd@^44޳^djdd444ݳ^^d^::::޳dd:@:::4d:ddd@::^޳d:^:ddddd^^djjjd޳djjd޳djj܈d܈d܏޳d܏޳d܏݈d܏d܏d܏޳d܏޳d޳d޳dݬih32/hna]X6"793/+!!Ⱥ+ Ep\QIE9#"ѵ@[eKC<9?%""ԾI`bKB;8>%"#ļVdcKA:8>%"#ĺg!jcJ@:8>%"#ĵ{#sdK@98>%"#¿pjL@97>%"#dz,toN@97=%"#OtxQB97=%"#qVD:7=%"##g^G;8>%""D\jM<8=%"$NzU@9>""-D?>;}+5aE9>0!")usP<8><;=+#%)`֑`B769B6#" wO:6?5#- !ƾ]H٦cCB5"5C:!yпBjאc<"4?6>9!jƵEqp"5@6>:!`Oc76@6=:!U+#BD776>:!RUluB976>: !TĸIZX7c?9776>:!O5QL){Z<876=:!O)@>+kN:876=: !Q)/6cB976>:!O)'cAU;76=;!! (ƣ#OE76?: "%I/L96;@=@5"!'864AfA>6:<%"##! Uq!0D6>. Qxs, #C86?,! R}ol69;6?,! Pvhd;-?6?,! P|pa\='@6?,! Oxl\W>"?76?,! O}thXS=;:6?,! NzobTO? 7;56?,! Mvk^PL<3=6?,! LrfYLJ8/>6?,! KmaTHH7,?6?,!Jh[OEG5)@5565?,! 4veWNK+ ??>A&! 9ND<7-#%/.' # #! !%@A42- ZgRHI? 0%  utE0I",  xzSD)J'.  |^R<G,/  ~g_N0C31  nj^EI:5  trkYVG4  xyuic_7  {~|vm{08%  }xR7.  ~y49  32H  ~d3U !&2/0.Z|$*i* $Xsy~WpQ   !`ihmrw|"G%  [n^cglrxoe  TfTY]bhpyF8A  K{XFKOT\eoz3N3%  >lI27;AIUam{6Pi/  2Z5"&,6DSbt>: (U-)9K[t& 'R/%5K?)5 )Q-'cp4BA;$ &R,JR$=<9  &N-HC/11 &MM@"jzjz}- #1KYDUdt= $ FN%,=JOcR%  &SPR?#)*&M% !!%M882  /I#4  /H2    0E,  /D&  /C   .A .A .@  ->  ,=  ,;  *; 9.%  %!  $..-)'HCC@?6 * !:8?!& !:7@$( !:7?() !:7<.+ !:774. !:70<- !:7)B/ !:7"F#0# !:7B+0* !:7 :8.3 !:7 ,D)? !<4  E$!C "&H'8:A+  5HEG/&I!/?  !%N8 =7F' ! $H-#I&1D !!&I.3E?8 ! &J-==F1"!  %J/ C;C,!  'I.  C9    &J, $N#    $F-B: $.   %E+ B< 3   #F*A=:<1   #C+?;>?@<*   $BC7 >@D5&   !, =?$I%    +!   +<&    +<!   *:   *:   *9  )8  )6  (5  '4   '4   1(! !!!!  h8mk &'2 #" 1( D1 ]4 ~, #  * \  c DBAB](AAC@ \_2*RXR|QVQ]R#PQR NVT5lP{C["XzxuV5ۄvx~0]p8*;8;N;[h;c@;j";i ;b;[;R<w9/څ uit32R!""#$#$##!+31+#$##$##$SxS#$##"&JOKHDA><:864&##]-$##2ongb^XTQNKIGFF>$##Ex!###rqigc^XURNLIGFBD=$#hŻ?##NyofYTOKHEB@>==BAD)$#pѽN##euq\TPLHDA?=;:8<:989CA8$##un$##"vupYTOKFC@><:989BA8$##w{#%yvpYTOKFB@>;:889BA7$##xÿ#&ywpYTOKFB?=;:879CA7$##z¾#&zxqYTOJEB?=;9879CA7$##{¾ !##&}yqZTOJEA?=:9879B@7$##|$ ##'~{qZUOJEA?<:9878B@7$##~4##,|q[UOJEA><:9878B@7$##~:##B~o[UOJEA><:8878B@7$##Q$#Fo\VPJEA><:8879B@8$##ÿd$#Gq]VPKEA><:8778B@7$##¾{#9u^WQKEA><98778B@7$##¼º"##*y_XRKEA><:8778B@7$##(##+}aYRLFA><:8779B@7$##ށƺ;##+c[TMFA><98778B@7$##ĿV$#,d\UMGB><98778B@8$##׀ |##+g]VNGB?<:8778B@8$##˥+!## j`XPHC?<:8779B@7$##׃ F##mbYRIC@<:8778B@7$##؃ k "#qd\SJD@=:8878B@7$##؀ف ֑* #uvg^UKE@=:8878B@8$##ڄ%عE##m|jaWMFB=;9778B@7$## ۂ%l ##VneZOHB>;9878B@7$## ܂$ߓ&!#Hrh]RJC?;9878B@7$## ݂#2##3vk`TKE@<9879B@7$## ނ"X##-}odWNGA=:879B@7$##4""#+th[QIB=:979B@7$## ߀#:#+ym_TKD>;979C@6$##g##(r~rcWNF?;989CA4$##2/)!##cwh\QIA=:89C@8#hZ#O}naULC>;88A%##\!"#;¹tfZOF?<98=C?9$%#?dZVUTSQXH#)νzl_SHA=:89AA@4!##D7""#oŽtfYLD>;97:@AB@41343..,##u݁F#DƬ|n`QG@<9778=BA@=D?6%#;#o# 0ufWKC>:87:=@BFB?A $#"$## $gȀ8k#vЮn^QG@;977669?A@>%#$###cǀ8<3ȟwfXLC=98778?BA>$#<*$##"g̀7"!gѺo`SG?;878?B?>$# >BC-!$## )l€2r!%ͮyiZMB=::@B?@$# >A>?D+ $###X2J7ʢueUG@=BAA?$#!B?B@B@B-$##;"Sƽ5pνn]MFGFB?$#!B?C<8:B@C-$##9"Oº)"ɴyfWTOFA$#B?C=867;B?C-!$##"S̾+*.¯vm`QI$#>@C=86 7;B?C, $##"B̼)$)и|iY $##!B@B<76 7;B@B-$##!;ɹ(/˭s'$# B?B;76 7;B@C-$##!3ɽǴ%߁!CŰ4$#C?C=86 7;A?C-$##5Ż"ވ%C@#>@C=8676 7;C>C,!$##&t 02~q#"!AAC=87676 7;B@B-$## j+.?!# "GBD<87767676 7:B@C,$##r9#$MEG?97676 7:B?C,$##]#"UOMD;8876 7;B?C,!$## kN## ;9887676676 7;B@C+$##wc' #Q2ewjSFA=;9887676 7;B?C* $##nI!#W; SrgRE?<;98876 7:A@B/$##eK#R/!PkdNA>;:987676 7;B?C,$##eM#Br# i|g]G?=;987676 7;B?B*$##iL"#*I$ orbQC><:976 767;B?C*$## m@"#&s9$$jk]LA>;987676676 7667:A@B0$## e=#"jþx%!%+o_YG@<:88776776 ;B?C/$##eK#!\ǼݹB##@gZRC=;9876 7;C?B* $##aJ"#$T÷|"#a]XJ?<:87767667667;B@C*$##\B"#&Oͼ6#$.cWTC<;987767676 7:A@B/$## #'Qs$#D]TK>;98776 7;B?C2 $##!#(Nȹ #$XTOA<:877676 7;B?B) $###,IH$#5ZPG<:87766767676678;<==AF=C&$##A"#$MRL?:877676766768=A @C@A-#! )w!##5LJ@976766767;C?<!"##$$#/$#!LGE:768@AA $$#MI$#=HH=767676766769B@;$##7}{\#(JFA877676;BA2$##9{ywi ##FED8776;BA-$##=~xt~un-##5GE;77676767676AB8676767676676 766!#/=@?@?7"$###%+)'&%#"! "$## $###  !#$#$"#$###  ! %&#  !8FUYULD@?=<;8.   !!    @bvnia[TRQONKKP.   03.,*'&$#! ! 5]w}ysmfb`_]\UBQ%  252641.,*)'&%$   F{kTJA91(%$5[CG84+  JTKE<5,!IOI *5; $!LWPKC;3)?UI#  .5;  #!N[UOHA90$9YH)  16:  # O_YTMG@7+ 4\I.  578  # Qb\XRLE=3(0^J5  786 # Rd`[VPJC9.#+_J;  896  # Sgc`ZUOH?5+ '_L>  896  # Tjfc_ZUOF=4(#]OD   ::6  # Ulifb^ZTLD;0 XSF ;;5  # Vnkifb^YQJB7)SWH <=2  # Wqnlifb^WPH?1$K]H >A-  # Xspoljgc]WPH;.EcH  >C*  # Xtrqomjgb\VOC6CgK"  @C-  # Yvtsqomkfa\TK>DlP,  @B3  # Ywvutrqnkfa[RFEp[7 @@8  # ZxvutrokgbZOJqlH A@;  # [zxyxxwurolgaWPnvY AB=  # [{zyywusplf^Vk|g  BB?  # [{z|{yxvspkd\hp4 CCB  # [}||}|{zwupkdguS @DF  # [}|}~~~}|{yuqkiyk&=DJ  # [}} ~~|yuqk|8 :FL  # [}~ ~}zvpP#3HM  #  [~~'~{vo0,LN$  #  [~ {D!PL-  #  [$["QK5  # ([~v(PL?  #  [~?IRK  # Z~aAYW!  # Y}~!}.$?]_, # 4c}~K$>[_< # 5(t|}q$!>ZYQ # 6Kz}~D9cWb&  !1C||y|}l0hee8 %  ! ! !2IB@?=<=>==AYwxz|~9$bzeQ " !-bvvuxuwy{}~jLni+ !B Uowsqsuwxz|~75vJ $# #! Rosnnoqrtvxz|~])pym! "+  S|}omlkjjlmopqsvwy{}0Y|: "  9(S{zigghhijlmnprtvwz|~i3~a"  "   9!Io}hddeefgijlnpqsvxz}OVI   ! C Fpzdaabcdefhjlmoqtvx{~1+{z/ #   A Ftq_^__`abdeghjmoqsvy}nM`" #     ?$Gon][[\]]_`acegiknqtwz}S!lJ #       =!>`oYUVWXYZ[]^`bdfjlprvz}9-~:  ! "# ; :[ykTQRRSTUVXZ[^`cfiloswz~,S|h,!%  % # 9 6bvcMLLMNOPQSTVY[^adhlosw{r$cx^('* % " 7 4]yu]IFGHHIJKMOPSVY\_dgkotw{j$'kmY+33#!  $ $   -Jvp\C?*@ABCEGIKNQTX\`ejnrv{p $f[bU@BEGKOSWZ`einsx{_(lwJCA3! $ #  "HnbzM3112$4579;>AEHLQV[_djotx]6lwmW8   % "  /Eg^nA,))**+,-/136:>BFKPUZafkpva!6dU+  % %  -/_Zl@$ !"$&(+.159>DINU[`flszr)+U<  # " $WQk< !$(+/49>DKQW]dkpxn&(4 "$ #  ,[Oj;"&+05;BHNT[cjpxt. %' #  4XPf4#(-39@GMT\cipH '"* #    &WOb9%*18>DLU[cr|=7.0 "  "XQg:#*17>ENUd{;  4=7$ #   #TNf9$+18?HX~pC !& #  3UNd5%+19Kt~c= &"P@C  "   %UL^8$+=ryvyO. ]}< I;D& "    "VPd8 ._qhqB(\z. #M7D$ #  "PLd7&Rk^f4'Io+ %J5= #   0ULc6$PgS[%!=k* $G6: #   *QM[5(V^SK@zk/>47 "   "UKb6 $V]NR DKRX]bhknu< -(0 $     RTgK.06!")/4:@DIMRUWVY|xi-  *+ $  %ZJe= "(-27=96[]H *'   *   ' &kAJ\[\\Z]L.$$(*,-..,' 2cP2  "((    !&  )@]URSTUUNad4 NXI "(  # !    ,ROX`( 5QG  "&  %   GP[E$L@3 !' %  JV^'CA< #!!   ! !Ze43H;  ' #!Rc:&J7% "# # Qb:H6.  ! # Sb7>82 " " Ta41;1  ! " Ta4(=.   " S`4 =-   #" " R`3;,$ !% " R_35-& # " R^20-%   " Q^2,/% # " Q]2'/$ $ " P]1"0#  " " P\10"   " P\10" ! " O[10" # " OZ0 /! # " OZ0 .! " " NY0 -!    " NY/ ,#   " NY/ *$  ! " MW. )#  " " MW. +  "  " LV. + " " KU- *  ! " KU- *     " JT, )   " JS, )   " IR+ (   " HP+ '    " FO, &    " @L3 $     "  5=I#(  # !  $B:H?9530.+*((*" $!   #  7@56420-+)'%## !  141.,*(&$"!           !  ! !"!  ! ! *47 65431) !    ! 'OKHIIGFECBAD* !  *,(&%"!! KDNS QPNMH:E#!  +.+.,*'%$"! ! 8LV9)'& %$$0M;= 1-& " ATK  @C? $.2  " CTE  8H? ! '.2 " DTE  4K?%! *.1 " DTE  0M@(! ./0 " CTE -O@.! 00/ " DTE*O@3! 01.  " DTE 'PB6! 01.  " DTE $ND;!  22.  " DTE "KG< 23.  " DTE !GK? 44+  " CTE BO? 57(  " DTE :S@! 69&  " CTE 3WA! 79(  " CTE ,VF(! 78,  " DTE 'TL1! 770  " CTE #MS> 873  " DTE  DUH 884  " DTE ;TM! 996  " CTE 3QM$! ::8  " DTE *NJ4! 7:<  " CTE #JGA 4;?  " DTE EII! 2=@  " DTE B  " DTE 0UH1! 'BB#  " CTE &UGB EA)  " DTE !MJJ F@0  " CTE ?RJ'! EB7  " DTE 0VG<! ?F@  " CTE !$PII 2MI! " DTD  @RG! $PP) " FSA 0XE+! JQ5 ! KO9 $MNB! =LF  ! 6GN,8WP#! 0GR%  " !  KJJ#)PI? %GT4  !" +KC[4"!CHM! KMG   *>:7664! !IFT# $" 'RBW4$!!&OJM! 0JR5  " !  *LIS2  5WE4 !KGK$   "  ! JFY9##ESL!! 'OV= " !  $ODZ9!,TJH!! AHW- " ! ,OEY3 !49"  !  JES2 'HQFG!  "  B08   !  EBS3 'FTFG! AI0 !!"?/4  !  *IBS2 %EVFG! NGBN2 "!=/1  ! &FBM1(JOGA! FIPQMJ9" 5-/  !  HAQ2$IODF" NGX;/LMJ= "!6*.  !  GBR1$BMBF! OCV8" )SKL6 ! %2*(  !  #GAQ2#AREE! NCY@" /VHN%!! *-*  ! %EBM@IJD;! FIU=$!1TIF!! )+*  !  I=JCBC" NHX8" 4XKE!" ,&(  !  ICH@" NCV9#"@NK2 !('!  !  5,! MCY<"'QFN!  '#)  ! ! GHU="5WFB !("%  !  " KFX8""INM(! %$  ! MAT7"/XF> !# "  ! J!  "    !  'FDKQ(1E=!       ! 0 !   ! FR5 E' ! !   ! =D' ! !    ! !"    !  82<50.+)'%#""$! !  17..-+)'%#" !   +,*(&$" !   ! !    ! ! ! "!! ! t8mk@FR.6S/ LX)@v`Zs(; D j[Si\mbnjns$n{)n0n9 nB nKnUnbno"n|*n5nC nQ n`npn&n5nE nVtnp"Tn49nG n_ n|-nGnaRn0'r"N0s U 8EP$ )w|Vԥ:#.O UFo53Մ9YՇE)=0҄ES>$s)v9BR ց7b:0m։Cb:%b"ӅFi=[+v:FN8f;@׊@_90'ԆFf=3&x:Gz3/y9m?{2P؈;\8-T׉G^97Ax>E=>vL5wA= Z7S\~9{9 Dy<E; :ڋ@ :8~?މ<7Cz<I(E}<dK$<܌>TI+5σBtG&@{<=E|=y7>ފ=Ro'.4τArg??|=OPD|>|,B߄=TS 6ʅ;p<7>d=}g C$%|?!CZ`x8a5:V T<Ss L9NVVVVVVU[.ex]WVVVVVVUM0 =E ԈJ" C^J u&U{,4l F eY@@@@@@@@@;0!  dupeguru-4.3.1/images/exchange.icns000066400000000000000000000033561426171743600173060ustar00rootroot00000000000000icnsil32ުHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHl8mk00`P 0jP 0`~`0 @`0p`Pp@` @@`Pp ` 0@ `0P`p0ర00P` Pdupeguru-4.3.1/images/exchange.ico000066400000000000000000000102761426171743600171230ustar00rootroot00000000000000  ( @   HHHHHHHHHHHHHHHHHHPHHHHHHHHHHHHHHHPHHHHHHHHHHHH`HHH HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH0HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH0HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH`HHHHHHHHHHHHHHHHHHpHHH0HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH0HHHPHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH@HHHHHHHHHHHHHHHHHHHHH HHHHHHHHH`HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH0HHHHHHHHHHHHHHH HHHHHH`HHH HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH`HHHHHHHHHHHHPHHHHHHHHHpHHHHHHHHHHHH@HHHHHHHHHHHH@HHHHHHHHH`HHH HHHHHH@HHHHHHHHHHHHHHHHHHHHHPHHHHHHHHHHHHpHHHHHHHHHHHHHHHHHH`HHH0HHHpHHHHHHHHHHHHHHHHHH`HHHHHHHHHHHHHHHHHHHHHHHHHHH HHHHHHHHHHHHHHHHHHHHHHHHHHH@HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH0HHHHHHHHHHHHHHHHHHHHH`HHHHHHHHHHHHHHHHHHHHHHHH~HHHHHHHHHHHHHHHHHHHHHHHH`HHH HHH0HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH0HHHHHHHHHHHHjHHHHHHHHHHHHHHHHHHHHHHHHHHHPHHHHHHHHHHHH0HHH`HHHHHHHHHPHHH HHHHHHHHHHHH0HHH????LJÇ``?dupeguru-4.3.1/images/exchange.png000066400000000000000000000014351426171743600171320ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYs  ~tEXtCreation Time08/18/09:tEXtSoftwareAdobe Fireworks CS4ӠuIDATXŗ1z@K9 P; IMc| I5|('tH#Xٷ3~=q?IDRY` "2~ ` \VH̦jUt跽W` Hd/ ,̀*ͫD$~|[韖Sd+: Q˼eU5Sy:nŦc ' -?ؠ@[}240Ye{+-%T٫IENDB`dupeguru-4.3.1/images/exchange_purple.png000066400000000000000000000012551426171743600205210ustar00rootroot00000000000000PNG  IHDR szzbKGD" Aa3 pHYs  ~MIDATXŗ=r0?k܋9Xr 4 Ӧ|(Ѕ7|QZ4n`uf91H%N"}ayw>y˘ Yd^7oXQ: ~0r`S,T{ݡkk5 '+ ,n{+yk;`e{}X7 2N#g$ӟ-bjYx%ֻzOML׽%Z z 3e4+y)o*EKMZ ] "azTXtRaw profile type exifxڭ8Ê5Z<; KtxU3u/ "3H{S\&Rs[lگ>>9ߏuxyq||D_>/_}oS?gssw=f!?7\|9I k6~vhv;]s{\t˸n8do_x~p߫g_w| -PB4{*W^Cd92Gq+4A9- 1`\nsWnO~5$r`\ 6\vx_W"RAB&6<|{OyR!>)PM `Řȟ+9SH1SI5M9s.Y5K(K)kkڛoL-jkw٣?ˆ#<ʨ>IgyYg}V\iUV]m6dvi]vmCpI'riF_~C5#gx gbF|tD($Wlu1zEN1S3Ȥ,gq|:3v_q3?Cᗸ&jKH8oĞ*Ԝ@Y Cs0gfD{ yX4ݶg5>ioVw>'zLvyN{P}9-qs59iZKa}Uq9J؜i-o;ynnxNMpvxЙIt_uQN̓fX;u{ct욹5Xtt?Ϳ=0C6 *@˻ kJ#1{m9Yc"Bߜb%f Sc髴QX ,bRX5=!٭βK"+;ykPa`n?](Q6OEʹ3Gk>KA.Gd Zot=#m&b%nި}q8cv4n 'L*n˓>ܥ6wWQRT˜7f[-QmbI!P݉2-&9@-wSB{ja-oPVkN3ks|R&$$AG6P[9֏b3L=Q}&3}WSgmZ; In6c3S;FIHJܽ;i1=Fv*Ȯ-9jH%9Fu`ۣpML<8b@oa11b 易E3gaP8+n% Ҋikܴd*!s9wfJ4!2ɜ"Lfxu70KB]16 9e6exL摧\$. kgǃU.]6-5`Ըb`W֏Cґ -(pI qnjo-x!v+BvT9|,w {a,24|"lē/uf].XySFu@#Rglnp\Xv#9mlA|vٴ~czTæydjadpE|W:(T6<#!% DwhY'[8 J3 XP5XCP [ |* =DO Vyf<h1@RU& #|*oV"bHC[]@mFwa 2#ʵqIל.Ó f85Zy67A^#ʋѩX^4`(-.POk5b\Mq8fpyHBxF-PhA2P,mӈOy|4Q)91 +ɴR)z]]T T@mER$az68Z!nl<39( ߉onraᭃ OٽO6 <<fL(%ĀgHZW{uq&6U7" ȃN\e r9_yJc.I.辢J3>~"AoVGau :z-ϟx3y!p,d9L>ߐnL"" gLCYUO I-L Rr7%?uC~مB|R뗱pC>!P(ʋp8Uzd_aS%̸E èHő(5\cj+cL2s %.GUQ"=gqK2n7"nFHPOG,;xQa E|6 kGL൮.r||DL q;P:9o5#˰#nY5+,53'l)E ÛH]&2L|d,/H3J<(46 m0bWHXA [BB3V;Ev9+t$ C"$d4 oMY{l3Ђǭ ãSEz@$@@1V qV{Ao-!_CqPJ "%WVfJj.kPk't+`Ux(qFthq2l%LAhL)1DE3ER{P4LbzBJ觓7Wv!" wD5C@5-b:%L1K|fA\5 ҅-d=&t>ÛBJ@Fdmb zSirQrej"$O1%|C8Ld!-y8(@hf|Q߃2u20[\{䆢֑l/@d<REpə'$n"* D]e `*s ?x. d|n3k>(W>o/NⵎT-@̇Tf#Жo? TdNFŔyBR)T<x*eLA^"a#iHuD5z4T eI.ȪE8Nbgܖ %<: _MXP0 N:J{ؒj ]7Y3 o^v$~UUD;lS/`G7Yi-ؐïEV@D-r\ 0]+% C" ]H?UC;x%r1(t/5ʨt&Ò?˒]Ht!n] -W.$YkbJڔ%\ ejq =in۫Qr bԙ]+jiupJ#4 '4@QЇo-7,$}2@/2bODKa-w|#V8MQI-=>yFᎢI[2l):1zSBH6A`!c:x&/|䒀TbxL@qYVm:;u'T\gĀ:̢O$Y#YT墆 ՙ:M2* KvaQ& &4ͩOP- T"` )$I( 3y'DSCI@)gq.w[ZZ)-2Z^%tl섬ȓɅƖvNEz'<0 Y M=Xji,IYBVUmH~y󑮢R7<ܷG.3lEBU/{cj@A<:Zޘ` eF!ȩNc1hNZh u*Qkj 0-b}lѰ>lV6J"G rS0, 9䄩/,Q*vI NN\ج !!!;fZd)FrM5Ԏ~xZ!0)[*St{1H8(]w)lё"&\65CFI$9I =AlV%"tK`2 )23CKb+Sxt{xP`ó N 1bi{J4N?lVA"TfQw9}%d^k'U 1*"z/Pq` xNDj4ZE2ہ\^";)6YuLZB#`p蝺$ʻl1j3J]۸rS߱j8<8Ohu2ɥ^1q1{BZ-L^PGw>H3 80!@.r+y.u(\)R^cpw3W<)p>K̮ޑXvjB<nhEyЈ-N,r![nPᩀvɼ7֜nK牴Vwd?:kq5D(|j\SWLCkh -rjΗOs3 P/BpĄkP"_X= Q}1V|bZ{p7o-\jNZ,YӀ$iI%0R s! }ezIxN};w-w amwH^5Hs b= z_6㫿_GP_j%x|[ug;[v6hbE0nRkq* (1>Za+&7mcXL<9h:♤*k䪦HPR> jQ8ΪFxs:0|Kh<{kD̀tp f6# jI3^EŁP86BB$-@j ET =y2HHX>L Qխg#fAG/24X`fBHRF4++ҕDKM!;#V=Z%2~W_'Wu9-x4?8 cƪ"(~-y1UstGv ^GisUv{g^_JDw]v;'ɐ6:,=AΓ7:.-,.H2[+Lu#3CnyKG6/DElTg3EQ€hK%J|Wy5־]#ko6auOjR[ƒ }di}NŠY ZGIҊ<0seshW"oLXmu[ CF]8vRd jT`n^RH2oy7A!(,'΃J&? )ƕrbh?d%{udnQrZo??Mk']Umikc ,3j9$ Vv) PL0⎴ a]Q 35\ɥuE"̑6%MmVݵ:^̕W<1Z@L$:U"p.}i j!2>m6(67u1uhK=:`EyDA:4mm.@fΪƷ'ɡt\T+xE5!q;6:ͯ ߶ SDzeA Yi}QIvQt`aA 1E`\*Jܗ{їi{!αr;10c`D wF 6.j~-u\R *]Z a(Ƭń)GrIsmiiS yTU^CH`Q0 j|7hD-ho]\YHG Rl_̽'oz~ڶi{m)SИ'S%:'B ϖlw3\hЃxO>0|x; R1-{'kԳ;60؞f޲5kS#2j!؛A4їg`-Wbg\=n Ah_S0ڹUjœx?\<Cz*SFwK)QCߘq-傜h`nɻ2=z>>_}ҵ?kk44? ax%4,M-&h3i(Z]CuMWS|]Z*?22])^cOU/jѩ\&9;Ń0MK~˗<,_v:9]3,#!({-y AieAj_0M+ZҿjjM"!|h?yUOϼ{!pG/SJ_Zyq #QbhXc~~¤wo4sEC@^]l3 Ӭf$kӬv18iCCPICC profilex}=HPOSR*v鐡v *U(BP+`?hbHR\ׂ?Ug]\AIEJ/)}wYc3hmf)1_XC#BH2f%)N$բŀH< &^'ڴ QVUsQ.Hu7e5s9(XbYԈ'㪦SXYYuZ1EH*j]'BS>a/K!W؀Zq/)z_c|;N>Wzǿ?Iot0 \\w4e ٔ])HK(30x WqhVH){}s=<-rbKGD" Aa3 pHYs  tIME  9a IDATx[p\Ǚ3(C@U ځ/#ҮȫJm^[[UIvJy˶!/JJRk=^nڔmHQ$$ Cq@@:3y83=sH9sN=ܥ n Y L8zBּ D,Fr o CrHᢓ"}t)Y; /]dy?LbxDAQ  6< 79<R>0H2ՠnxk 1$B( oc+F I)Zg4 屜1BK>0+9ˇ!NW-(w )2hGNWsC!~va_9w=FyB;٢ <NwUah \j2$s\waY!)nA`/'z'E y9Q52 ~q`&FH3 Ԅȡ2|XI ~bP jn$js0P!L3EvÝC^PFlϑ}e u DW#GqqiB I!jЙ%;a^,Y +r9/ ȜK*/ȃ/v36қhEYdf ϗ!6,*Y )N/OɧkVQy5" R@>A:V灚qt}5jq h74W|8m@ K",G,y/*RPn0e<2oYhMX&,#G_RH"M& . g {BDnY*~Rj/gsEY+T)vĹ?ߩ 3qX#Æ6A( $k.I}M^f\R ^1w;A(C}DpߎAdE/,,0GŃَؼZUlh0F}T,1,dEoډ"(hInSI~r{eEޓ&ѥ "V hFȐ#؎hDp,-)u}j?p w7j/.> .ɏ;[/q)BA5uQHaq5 )vP&%vq| 5F}Z頓m)GFbM`j KNZTkKD/~K&, pZ"h+O?F~o^y" Y] 0X!GG6;|><6rHJp`9zH‡;2H45w}Եt5|gPeIe+3b+9;,]X!uIJnC0_Vb ӗ,rOA]$/"dێCSb dBO|Dyһ=w֧ '($$6֋Ⱦw:Y$| /0*`A.LhDpwz$?/(^"%;״4> TeG&2\n$]d72MмKf!$;eU$l$ o#M@3dܵ{{LQ2@Nu!&䐈 F~GpDGHbHI>=>'WkԵQ› GaNߖ/.7u.q-鼻VȚV.A"XrYnr ^q;?W5 |?7_/; (pMbTZ笸EYbaDq7Z4JxfaR47=6n}2UB1*'(Z ֌,,~ˊNO_Tz/'VۓyX,xg:11'.Q Y ~>~-z~ ߸j,5S";@oo4^!KCKSz*UT \%J}нғR0~1id_zyTJ2Z-_PC̀ZsZ2\4.ou GBeEXja.[@\'9ۼSְUbe~0s<45CnLv[ɩ=|Fhtq 6ܺůo}kI&#*#RƠ8m h b54Pۼr(AM#r61'!m`H gãE3U6Wx,ׁ7яZ8sgnTihNa50H)kZ!j|` Y?iCrHϪ=<{esљ[OZ߻˸?VzISR$a &Z)8Ԡ!眛)sa7Bk*J<hȯns_y~[v~qK]7D*[>E렉ٻhIwC3̙=7K$=jĒ?ip_*:#|Zy vrJR?!iBPPx[C' Y҄V8]ՠvAj&q XC%\-y5o[p`5 .?.S;(9F[ :W%pߨz{)-{˝IpTsbg/1\9F~~{}L}\\|}>e[KSKa:Y pАJFgAZfg~@:~]J G1}kX `m.ٟ%4i4ɔFcWx8jwմZաK7+\L"yy# U:^y5>{)R`'V)<9UX2tuD%XO ٗQ1y&s/W33SB2ύKgxײu$@N ׸Ve" z.Rs y.߄<u7" 8;G4<]?.@N2jn$@-KpyoNMRTV(VnygBJ"V Ns bQmZ4pVʯ*Xcp ˘t0,V/a.R5gZ/j'm@ _],U#oyk|)ŇUFKLd<0 \VvӔ}Ȋygq!a`U9ݾ`Y:8! }(jQ=Y8*6a_ L^t# 7rñE7rrjPU~9;]Maw!\!. :MPvnr!yOw9F~. Vj.R}5ϟ ,)@7!9ը XYD]A0.F@xB,bjH7Y37]8|޴9J*8,Y|lKn2aT nt0q4[ܫWǺ-X b4-x*벋BW 5P\Az?+ZBq?[G7qx9 o}*=nvJKDԨ ,E7G']N#=$93&R!$/}+oSUeݻ.veYBs[pN@BESؒrP G1MP58;k9[7%4܆Jߕ6Nv ^( ZYZ,ՠe~n%=)OL_b%l D&jH9nĵTݿ]z"*?Q7uA8j"BW#B.g] |n*W#2\ǝ멹Qs(U$]Cm<[{ 58ûPQ[5P/ic&0k} *>0㜟XtC:$vQIp]b޶}\\BYy5ܵI7x[{Ϲ-r*"oRYwv80]η~Q`t)V1$[CrkZ{o ltL͠l6^Ʌ~UTg8nCtŏis3ӵp',[._V%t%Nߌ<| 4\E,%v%:)/e7BH\F=h x8("H:8u%,Vɜ>Y)j;ց!斂'ߒ{WHM7n9̭\nZ_an g!"Wq0r}.E@2gR$M _HV`Fv);(rͦ m0'l0uR(mMÓ&]D3-SM0zxj!.3[\rtk%lܲ]_4A@9m A&N/ pDˬAD̖,ּ,'V!jQ9'hٵZ~!yJ>]~J>]tjY6;cL &82X^_qhZoAs˽ B\ !4@-hMȫSԀ||Qzt֍ZH&k6$\QuYZuez uD_zjQlJzJ>O Ӕz\+7Fx_s,F~9/cYrßk!//TғvNg'.K"FR9ٷbe9t^9Dt\7aHyZg\%3Frc SWYkD( Zz8a=PW)@b=QR '|U4EYeJZ=|^ٟb{wbٝ_Z`S'],_@wQU3TA !-dUWF4!ƻr@ZUՎoΤrlT[ Q8Hm%t0iub oZla"oN>&ߋʨS[%niXА"J'@Ff~@OQr\aVA"IV2by۪se`4|C&1)>%8z^W%K=zQ!}GTTp (t1u5Vʀݎ4 C~KT˧u5.Ys6+[Ȋ^IJF4#3*`ޣ?nG|Q\xO? K@KXZ bKs^0]Sc $vbM97ȭYzJ>]|/%d 2uUM NOGyzU` DR?H#]&ݒm`!hD 7}φZJ廩1ql@*gE!<d;"ۈhz Uv?T ~zmk QvK^Ɋ^/E]z^҇0]þdޤۃUvƐu+[YL=ȏOriflEBw?$ #~'nry9VET;UN\ح6/ql|zXIӥnJTN'xk[9k{U$QN]EVW̍&jlV_x9<§ `x9}dȊBXpヲACo('pa)p̾gy˚oIi(J┰Ds_'KE5H@Xˈ Cݰ{/'ji.˄ǭ̦ 8F]D%Ty}69F'-%NdƗԁQ FEKnW{.2ߔU;||х4@u%y5~wuPX$41}'x]8}/`MH}lۃuzgH೷ 9lDT@"P@Ę^{w99~EPSr/]̵ɔΫ3&J-Y-,YMVt~>BĽsrl%U~e7̰i*F@{](qwW{z!2}{ =! z/zmt-$YQTqo-ݯS&:]d֪[Kum~Sjqcpy5"~+-~1vR)Gk/1>S X۟Ĩ]dUJs|d;2QJWFw >Ru/_yc(Ӑݿ^`J@Kd;QQ )kw9٧r,PU˒Rw_ IN $N;XTp|^pff~[$qYdI;]=s_(ouu_t%Eq-a +'qh5`uMSl)j5Imqdۿ ~W(vE }87WpT¸P?zz[&T ˷PJ(Zr#nI7JY h#S1Rζq l&DLسoZ5tZV݊ {|ASDeҍ9R2˜we B)nukqF'>u%.I1Is [`~oʂP=²GH0D!)=.!DڐZ%':Q_ݢ_vSΡ> .qDZ7M+ (B$lIa 8FcZv?ul ior*5((k ̯~!$1j/I*MwŠ{OP4'%]Lu.W;7mN,kIQv3éQ~s~ zW Ia.: \7)VQJ}ht5]0 ,H7,mc+9mmxR?96p (@0UX&nѷ)]aTk +YQ csmL1s L/  qMr@q_ lXSZZvw'[( g/XXS}'E$X\K蒠Yk:kpG")-r@jT*ᘵ(}}lWڼ^c^p'QQ4p%vȃg]Fc[/ SN{PQr^BVq{nk>d{q쟱ҲTlIENDB`dupeguru-4.3.1/images/exchange_purple_waifu_s4_tta8.png000066400000000000000000000157161426171743600232710ustar00rootroot00000000000000PNG  IHDR>abKGDCIDATxieEu748`F FPIPVXQY%DD;, T\0; NQ{aw:ý{:瞡Nծ]޵+.0@s!qE9Ȁ,GNb#tf:0@" $:#,vk]-YdYFd:6^8̜~itf:tbsؙ/Vl h 39t2g8;`g yVlas4-7"ޱ-{Ľ7#_`G?6@H|x,ЧݡaɺȺ9y$<'Cwۘ]s aAN7fѡK@+V )kg.Rp5:m"s&s͢sy :oɐe-<;Bp~Z[YL5G>өnh}%?؈]"x`oKQNu%wMh#Ͽ򤆂N0j`r Yx,k/;A^U3WJĹ=zow_sg><~ ]b56rO* sqdF ~<j!{7~&SL:p 6gS8xDO< xnH{\ \\ r%+q5@j,=؟ƴ =xP=ˁHj5_>˩D8QYv{kV;KObŐ2m۟>D&syuk_Dd+ ~N[> _sY:Ε !]ݠ |8'5Yi I p%oeNcV@!?оuƣ}p+A-LX BN n"=Nѽ:(c3r*AUT6;Mo&;r@VqvX缢gϤX鉽Ӟ4M_U}E۫o2h$q.G*M2OX0,/^VJ.v8u 3+mmEX叐qsW"=3,TQLj ٌ(|N Q5=Km9s bDVR[VVޟ'*hiKVf*mQ[8B߹8i>뇦"Nܿnٖ[t1gKA8:gxnsFL#5=U5k1N/@:@]%UBԶۑq=?w͹pOUʴiea%qNb/ߟGf );-IrpS>xx(2>(2@w׭͗W2&u6Xg;}C\HD`[پM z9qNktj7LmưgV(N,>UGEr"Nm?2+2U_PE UTl"6] ۥ6~³+a< q ÍꗈyU V!T bDGEJ0 ,e;&y)#amA |(Lͼw!M>MAt|bWZUŭ i [ϙ!Y? r5`C<O֞"| *Ƭc[|\ sp}F+ԠeVρGGz.1G4.8mXF۸~>zcZAGaKautAoLRve| 恈0y "\*@:@eTv7I EUvl:Z kB p.lڀ~6/[ \xvi4Y&@Twd6PYTG WF(w p ^8lwuVn@zzT.P*{Ol'ŠeE?`#aq76b(82(OQ'#>0a_}h`"N\6'(_XAW'5Sr^ %ݚ'Ig0id%k mM;՛&+,wvJȕETvA͸G&p%4Ilo-W˜}>Q\Y,噈.DU< #=b-Mg2H"O,AM̆WW͒*B C£Hk, t!Ij3GLdh_/Qݭj]`L "~7b_PnT?;LIUE-T@86Rfעچ濑h]{zKzs\]|u>zL#2,2 J,ۑP;y>t-UJqW2j.3F-ఘX qp E!qug3!7㞵7pMv żF;[ zh(keCܳ`uֹծL=߯(ٔB$UE_u/UmX9)d/]y%v6WRq_:Ow/KqP8lZ5 <1{B:eMlFG ie/;#Py߸VϼΜD(:E3h-eFЌ*[eˣk̮)[pfo]OtKP"x {/"(J@DsֳuĽqK H֣-^)I+.gto7i&Cxg6_|_*hۨVvnKD D_%eLa :l"޴m`!w"+toB<߈?\@"@C!8,jzhobzO7v3߼c'Kző mvyN7y:O>Atk}d%eVE;sM3RȦIww/ͨTpE/Ҥ8 .Ĩ{|YK3rTO}?>vGljhGRob+]QUq",~XrѶQdc)^6x$UYC.=oKt"e[p+iϡEPG84"S>EQ"k1I.'B-ˉ!z}"CW A^qUw-XozЂO3ߺ=|XNZ(ub8q^hX^UPOK M)Oj*h 2j EX%VlF/{<?'6XI<*F6܃>< kplCT;M>hҾ]ȿ!Bq-DxdˑwcH0] YJmތl41׋f3ɪ5Г%p7!-xk9!?ڶRTZ8v3R"؈@TH !TD`fcY;wD5 h*v{7eO3ކMJFy`JS1U@C0tj MDz:u2SW z\؊x9WwmU⻈վHū4#iey?!bۂwQz#TD[TbI4?G(:3_9WVBBZGU>K1l[0H}ε_!ƞې[PgsDF-y0)W{ [!n "l=_XZ>?]L[{2%u6̫PPq‹meFutIvQtiYV &hV2t}>{L?<6Eݩmm^+\]ۯqS T-ʔV$r)"Jܺ-Lع3=TS"=gu0#4)OY-#zޮH+lr .":2ڥ "bu{jڬKϔ!"i׭"uUx)>%#$)kr{U<ݘDџ@}SRl]-짙EhTeZRϜYğr;MqcT}1]DI eERmQl;TGZ/_karPpmr^CѬ ر ~͎I @y֐3l":fgQUʔ dɲ bҴӿ7#2 [̳vx }S*oP]t 1Y*4aJ!SYFg )ΐ'{nb#TO &фsKIJ^÷af7%6ۀ}lY!RXFjUȎo@A:tHp۰)>";+pg;ցmZlhZU"+a qzH_4E]ވj3/—؃z?Cvf#;yL#@6{s~+ѭj8i[ˢ^Su똹Ǡ?R7 [Өϰ>AZiHQ>jIcx6`< ޯ6Ro,ۨ^NBat+uG^Dn;ZEԹXP)e: l v|F7PbeW}ޯ6&Mjp"%]޿5>,P0=b.[ &Cr/0Uv;7r++i M]$nߪ.zOL)@FR>[+]1#]K~j/&:X*#L Ӕy7M2jk*K|Yu_6+/L'|J琅ڽ%y|.`[5};|6<7oJ 0Ȳl@/3qg'd0Ȗv[绀焣;Bn 2aY[m[*pDZ"o\Xr*m Es2`.a붭lui|h^P߂)rv<`9>V"h¹0o]>ǯ{\0#EGhQIENDB`dupeguru-4.3.1/images/exchange_purple_waifu_s4_tta8.xcf000066400000000000000000000413771426171743600232670ustar00rootroot00000000000000gimp xcf v011BBsgimp-image-grid(style solid) (fgcolor (color-rgba 0 0 0 1)) (bgcolor (color-rgba 1 1 1 1)) (xspacing 10) (yspacing 10) (spacing-unit inches) (xoffset 0) (yoffset 0) (offset-unit inches) gimp-image-metadata 8 8 8 128 128 2 72/1 72/1 1 "exchange_purple_waifu_s4_tta8.png!? "     %$# 0B`$7m7o7s7l-mosl/.!'-6d',GI6*dd!5604II3*dd%2II2*dd'26I16d'1II1*dd'06 H0I d0*d:6;I;*d:6;I;*d:6;I.*c,6,I+*!*d*6F6*6F"I)5F$*d(5H(I(5G+*d&4dF6&5cd#6%*GdDI$6cd *c#6d?*d"6dd &35Id6d; %Gd6dc !-134Id6dd$ $3Id*cd, '4Id GH! 5c0 6dd!" 6cG/*dd!.GE/6dd!.*d>.Id .6cG.*d3.I..*c&.Hd.*dA.6d!)455++-+++.232221110 0 0:;;:;;:;.,,+** ) ( ( & & %$#"       " /./.........)q3s3u3p,qsup,/,'^+=?+]^ 5' 4==/^^2??2^^2'?1'^1??1]^0' <0? ^0^:';?;]:';?;]:';?.],',?+ ^*':'*': ?)'<^(&<?(&=^&&^<'&&]^ '%=]9?$']^]#']3^"']^%&?^'^. ;^'][  !%&?^']^ %?^]^ &?^ =< ']! ']^ " '];/^^ .=:/']^.]2.=^.']=.^#.?.].<^.^6.'^ )߿9߀54/00/.---.-1/.++++ ++- ``U|4 D߀߀6q   ߀ ߀  ",,,& ',.. %! )42& *6IddI65(*6Id dI66**6GcdI*6ddI*I566IcdI6*9d68cI*9dI*:d5:d4:d4:d4:d5;d(;IcC01Gdd6cF3/+&!/3Gdd4`! !0Idd'c#'IdI> *Idd(' 6d6'*IcI06cd!G6c3d'6d III d&d$*d 41I 5G*d 6d#I 6I*d 5d4I 4d&6 4.Hd I6 4%4Idd$(d 4 $(.Fd0I 5 #2Id36 5 #2'5 6 4 5(d 4 I 06d!.II.*d2.5 */6d /6G.6..6d %!     989:::::;;                         ... //..% ! &# '?^^?'''?^ ^?'''=]^?']^??&''?]^?'9^'8]?9^?:^&:^&:^&:^&:^';^;?[7 !;^^']<% %=^^&Z  !?^^] ?^?2 ?^^ '^'?]? ']^ =']#^'] ??? ^^^ $"? '=^ '^? '=^ &^$? &^' &<^ ?' &$?^^^ &<^!? '  "?^#' '  #' ' & &^ $? '^ .=?.^#.' /'^/'=.'.'^y69:1"߿!߿￀￀߿߀￀￀ ߀߀  ߀߀ ߀ ߀   ߀ ߀߀  ߀ Ht  + i 0qp  .--B6I.I6-*d6.I 6.*d 6.6 6.6 6.6 6 6 I 6 d #66* 6 &6dI( 6 ,*dd56 2Id66 66dI66*6 I6dI*6 d&*d dI*6 HI dI*6 d%6 dI*6 I*dd6 d4I6d46*dd(6II*d6d6I*ddI( 6Id6 6*ddI* !66dd6IIdI( 6d*ddI64(*6IdIdI6Id*d;I;*d;4c:4d:4d:2d:'Id96d9'Id9'Id8(Id8(Id8%3Gda?+'%%Cd#.Ba Z36d ! !&% I# 6&616d4*dD4Id#46E56d!5*dA5Id!56D66c!6*d@7'79!.-. . . . .                         ;;;::::999888   #&1444555566779'?.='-^'.? '.^ '.' '.' '.' '' ?' ^'' ' '^? ' ]^&' #?^'' ''^?''' ?'^?' ^^ ]?' <? ]?' ^' ]?' ?^]' ^&?'^$'^^'??^']'?^]? '?^' '^^? ''^^'??^? '^]^?'&'?^?^?'?^];?;^;&]:$]:$^:#^:?]9'^9?^9?^8?^8?^8#=^Z37^ 7Z S#'^   ?#'&'1'^4^94?^ 4':5'^5]65=^ 5'96']6^5779 **++++  ߀ ߀ {߿T￀F￁￀߀ ߀ ߀￀߿         ￀ɗW!! "/.-45412709O:>(6D-6d!/ID0*dd!/6D0Id!0*dA16d!06A1Gd 0*dd+1Id/1*dD16c 2Hd% *cd-IA*daI*Id!dI6**cd$dI*Id'dI6(*dd(d5Id*cI!*d?"+6d"d 6/#D!"d##G#d!$?:d :A:d!:A:d!:C:d!:@:d!:A:d!:A:d!:B:d!:@:d!:B;!;<<==>b&"􇆇!nonmpnmkm"n(-/0/0010101112    ""#"##$::::::::::::::::::;;<<=%$!1 ('9-'^ /?90^^ /':0?^0^41'^0'41=^0]^1=^1]91']2<^ ]^?6^Z??^ ]?']^^??^]?']^^&?^]? ^3"'^"^'#9 "^#;#^ $3:^:6:^:6:^ :7:^:5:^:6:^:4:^:7:^:5:^:6;;<<$!! 󈉇 񆈈}oqsrssq(}o# *+,,0//,-++߀,߀.1 ߀ ￀߀߿￀߿߀߀߀߀߀߀`VZ߀gt`t^2233445566778899::;;<<==>?@@dupeguru-4.3.1/images/exchange_waifu_s4_tta8.png000066400000000000000000000127251426171743600216770ustar00rootroot00000000000000PNG  IHDRi7@bKGD#2IDATx{Uy3{sjԠvDB Jc}65Y.c6RC1i"eFE0ؠbU"E<.޳13{f?ν\ι޳z HHBFz>#G{%H'`ab*.U{g2o4bY6q0S Gͺ)@ ?`&C&$2]}eqhڤdbo) p9PD i)I;oY $ 1@@|U*UIKAۀ&&&A ]oK! X=fdoZzW8$Dή}(`6HG71 >T2=IHHx-wih2I I 8Uz>$f !k<SJMnV_ 1甂5pWq[15~!]OPNH.nB"&r%25WFzobW%sqjhY)Un䫄@?pJդ׊X$@@d;$͓Ie.\0nUIcT Ҽ]b6y/R # !LDH@i'X~|ۚ`@F?.$erI@@@Hl+ȥ%3?Ͷq! fA0]v!HlC"Ji9WZ t`,)_MBBD>yJ2p5V9ܿiV^zH.yS؂%,Z\6wP!PLIu2Um2UʻBI "F=r3m 껧zNOJTzv{"D1 Y`d8|q eV\A"F33m%(!Nw^׉&Fj1Ʋ1'Ƚ}X V{DLDDBBL@ؐD"Ptl-#@6*IR$}JyYg% FUW+|™" MBFH~Cq mtFԃ/ T{kͼ&:8=6qc#3|)H Oitr-լf%+x }L&bbBm 4wv'#@qɖܷ_`qn@KLsG g9El2iG;?2 r?kh6r1[x[Ꙭڳ1h$p a]M?!N=ۧsCFhƅ`q?k ԼpaVp&(Hy,AVӬfxG`aZeJB$ 8?xXmZ"w_XQNKr 0GYJ6TChA!70ȫƅ)T8y i[Of:<̓ n0*;\n"¨HcP 0 tKU)vQ%:ytnH 8f$M!*8Yf:*:_!'6n!2G94].o_Eo4M^I5bb:P WZV1: .$ 1 EByrIN泖̃c3ve;,Nc /s;D|.st`?f⮒#YgEO;t[v 6ˡM$9\e76N5qx iVA) ~y|\|aqiw}J O 6E'ⶀo#p%?bj$tC3К >gueI: 9g `_į-JLPs@%$V5jԨ #Jpa\gydt@lHF:':,dzJZqLi`X Fҁ!뉰uq7CqYÇZU*&OLL CqLa6Ae4v`2IM'f=Lz!H{Ǖ(TIF1 D_ yգMu2"B 5*Hn>sn `?GpgQ2~4RZлnvV/~љ"ZQy@s:h\j;ew#1aJt9U?&'s.mqSNШj^F Kmbu]}75ML''h&u'ױ1~&#Ld A}Mx5Fb+-d&N,r70reSWA*-snM}bXH<y#<,6Q5;Z NXFHY}vo sPߴKDpaܦiEԳ*CȄ +y(=Up O35txghT l[r0\Ո @YHAmۃK\˳z/bAp*#nIT3qA2?[ El~ pjxG9%š[9qHӑ,*ƇS^e;]^͙+VH>φG?@! s%q غ2=xXz'&&g%B#͟&Op"ٟ(sYL/5Q2dX'w:}uXyΟ'8qDV'>d M2 a_Y#;wKJB5itԫHM#EYr~3*-&buN*Bp(umBRs~tZT#~~c渓!mKR7:`. +P!bkVuPЪfHЧ6O Hx;@FIcȗ͘%@1%^d ٌ>;nk]y`DXpkr-~6[c\e2!BkuZ˶5.q)!V/\q:Ee 1=^XQ:a-Bl!S' ]a+O ):o;CZ}}9ih ,D1hIENDB`dupeguru-4.3.1/images/folder32.png000066400000000000000000000030161426171743600167650ustar00rootroot00000000000000PNG  IHDR szzgAMA a cHRMz&u0`:pQ<bKGD pHYs  IDATXV=]EμY71Ѹ懍$ +D!NVuJvV"(),Di-bv{̜Y̽4Fm2޽o̜;gpoCf~Ys-=uc믿gW?yk7ΓVϽʫFgVϜ]+޾|>thiLҸ{--yʕس%npW<';ݴVʱw~0Ma]sm圹}$}捷.~ǟiѽߵ{?IWo×\Y|đe9x'S$IqJHHHjَC"H`v;w;o!A:s0C$A#ږy0" 0m52MP>1Y90GbF5⼂EeLTCnqniP#@Hyٰ#FHyN7.34Ÿ Ќ7"!?wT+i> RJ0o٭$H@MbH)T0ORhf#a9 TAv7A6U#)V"Ģflv:{0Vku{", ӄIr ͦ*{ W@Hp%Dj4" <"ou@"[.)/>UH3ԺύhJP%X ExəP38HRuTz3ANcP3X'ԈlXlcYEMi\^ 2KJc;] Yc-3(`=*ʡޯI#ƛ2t*cr2{ud0Od &$%, ƈNdu`isl0GJjRTo X_蕚#.APVL0bhV44m+)ySc3i\ ׫p2 3=ARapYMӉѫ)ڮuVT_˱^"*:P \o\U`@у`F@lHWr \ }V/ptJmܸG7 bjB9Npr[Ѷc?  ;ۏ )S=tEXticc:copyrightCopyright 2007 Apple Inc., all rights reserved.f)#tEXticc:descriptionGeneric RGB Profile8$tEXticc:manufacturerGeneric RGB Profile=h>tEXticc:modelGeneric RGB ProfileoIENDB`dupeguru-4.3.1/images/minus_8.png000066400000000000000000000003241426171743600167260ustar00rootroot00000000000000PNG  IHDRnvgAMAOX2 cHRMz&u0`:pQ<bKGD̿,IDATו̡ C? bXiw 1"? XotEXtSoftwareAdobe ImageReadyqe<IENDB`dupeguru-4.3.1/images/old_zoom_best_fit.png000066400000000000000000000303231426171743600210470ustar00rootroot00000000000000PNG  IHDR>asRGBbKGD pHYs$$P$tIME % IDATx}yŝZj#>|1 08I 9F$'&c¾<Hl02 l5g$>UjiTӒZ_pQޡAQ@yPa  t 2ilc  `&ck@kux@ ^&1 `0hp>0Y/xӁ@ c``K׊v~~TVVrb L"#!""J!U#0o`x2)? ?ڊZX\4 d]]]رc:;;|?S<nCǼy0uT~B>#J;wL&Kpk WQw(ϛ7v***F^.C>i4 Ң}X,ZZEfzL&Nlڴ 0=<_i (!Rw+M6 >|>ߨI&H&UU~:FGG~mߊ9_tAdՍ D"@2q Y^"={qFDQ[X8zQòҥK1iҤ"NfÁTDxC<"qݠҭKLݎ" pŰm6l޼l#$ ##tO ӧOˡjnHR2cx{wpZ'NĂ*7 2:"\.lMݍ7xR3.<\<P .g!^4D"#r<}CxkwDh5VdaJ8m@UUt99ƍ}vGpK e$FaŊhmm-ҳ(Lz߻ɍNZҪjMVknWSэƼ#;sKӨ;\d֗k#ڄ4QBxh4[bƍ4 ȧ\9C+7_~9jkkpCCC#_7^pz]U>5XP,9!ι\*V9O~?dmmmx7*k=S`0x>X}>J\.c A}zь@ pç`p𫪊k^WkNcg&L+NX٦ZDAS=Sw\vͮZTs9\2KsɌ퉦cTV4ÖvDչ^::l\. 6lg+~M0i$s9&H 㾿7US6WLgt=]1J-숀() rCe?N@@$FT" >k%הxTcq亭]Tc2= NY-#!4 X 7+Ƶ 0DVl6:: cp`KoE6Ϡ'EZOnm"ZLmԮ\r\RW_]\dž Q@F s)ʊËٜNgr^ǔYSS#XtxWDX6g wYgIg2{Tw.d1E!Ɇ**vN'p֯_/..g>Vr_va0ZoD̼@9UUC</2~>%}e#:}«2 ;E H -&h(Dt1N9/z36rڣ3<_( "VPQQ;le  k 2eq^?K[fN|bQ(x--jD-)_8F>}B<^bp9 #PH v3DH1өj Ioey_ҏsaք \.!B"@KK /Wxh.$Q58q"9c so`!=j_z(7փNl l;U^':$Nxap $oǷ8LE)&Bw;P .ۨ.Q}o`E7O5,hThX 3f;`*`9x2H$ \i9^!溷ŗ?3WPԚFk*\*O;("FBzo:Y BpdFa=! BM M1~@^߁SfJ)dČ3{ntb@D7WҏjE^Wy-n ֽVO;);B? W)㠅$8`X͜6As0pΙ=îh,* ap41+ƺ-yD"!Nduuu`0Xq XP;E_$E84[' ab℺yS*k6//-QvR"K mJ/3ޛ.@pe+SpFF#C{&rΕ'-i,l6jjCX6pԩS,d y7\.Ll3_FwE 188 +j#7"祠ZH*r}˞hhJ vf\,2b3o}e^i@N͢ QlBAd952sPPN9@|Aur W ,0PvE!B@E(uޮ9N1l6^_JiQ6=Z-+gWLs8/(jn| N 6-H0W4. -\@o%' :L(A]o(y= P4pa_SSx ܱ*P 'O!d9YQ[V"Z3l0aN8']~C b3nB7 @Hr948 Q <*^ZH+"m)^ۓ@8+s(UU8BE-6sIV<$]/J^-qp5d!Zz&tjQ9ceM!wT  sTb(q,zWUU1ށaիRqpq0@Ho Yl^C^cH5QE" L82 XH 3% kX29s"$T+P*{p-T>QHZQQ!*/VKfrp0N$%yo$!y@"2]wP ZCpS+|& qLNE|_4UH'ߜ"բYL^H"gJ*1 &VAO HIsIM-u(` X<rDcuG@)8"'x@M!@ A?S)'Ϫ XۧXQ@bsؒ76Zy:b"$@&Ǵ^+8CɼKQ-]X\KXKsUpEOpBt '}@]Hs7q{,\Р+y_l$ bS-TZwuP X$J$@03#ٌr@1TMXD4NQg\E93'BlNx&W@D,rD`T)n ٬iNQV *Gch p*#&?y(c$}#~OQ$u"+ 0UC?} ߴohW6rwh)300`L@^R04j-2p-6  8%pBܾQgiIû8dx |Ggl߾ĀGl sp07"P- C}Ɍg3}'$`'pB8' D}AT "mR@fxR7j?B@Pӽ06KHUUaXDja iu 0ZӴٷe (3):A|:6H!GHD`L8;vcԑq!5%;F{BJeԸXL|̟(E"_a3Eh121N"3D7u@@.a缐# )̔l( Eֽ7]S\^s J`ΘBo33]6qYYQ"ͳ|2MBMtI m q @fC'4' 9tD,0IR^>QD a(mP`0)Pds  7W_O4 , vD `@0"LYEʸyжڇ }tJe¨M1 r>u˞.%>רSܻD`)c])Кd$ = 2Z &aLΚjVD@10*ԅ-ҀFt5QKL3l%_nW$2HSJR D8/%>)`^1 YyQ\4)d΅x.hj\]}= aaB7]b**~ ~ {QuP 2O{[x:GIt[JŽk^|nI^T :g0ծG*Ƙ U" R `PIt4MUUeM)xay=M4@?A$=4S%*>:zKfp aZAtL&R;V%4(jn˛Ⓠ`t81 vSS>^(+q |BE^P0 j0.b{"mlDRqӬaLJDS8WT\}}}m^zu~,`qax<y"A(8qj%F/p4)h'ئ9Ce⢿}txVil6Bff9ǻ+8&Q@`0, 1}tl6Ijd2PJq4?:w2Z8ٵkeM%qV:Ps"ŒzdK$f3̜ٙEgL#&`|#͊u;ĺBι:wtsEֆp8,׮]5`X9Wmۆ "BUU444s\tj{=58m`߼6FL[d:S4|J>?L^) n!)[?&(sspѩM1LŲvdG @ 5 > ʦ{fmm-6q_]:=4vvƑR8}dLSnyy >5bbfKlN~t#>PʜDfv^&TK\N{q[cWm~_60aa^f8oEO$cGIDATJuj_*Ma_:hh?(!pm72}Ͼ/Q\XϮ:^z4 [n{)~_[ F3f`Μ9p8hjjG6Jf4|BS3Z=CH9v^s]̙.iތ,ݍL&aHI Xْv >;vCEERJj/oJQ,ZUUUFeet|iK?xn\Ң-osFCCe>H (ܵp$qnk?ۂ%'˓TTT}p8z /"6mڄd23gb֬Y3gv%!JiRz?ڸ<jqyt7?81vOjQ2uNҰbQr??1fݽgag)erq\> O.\Ű~^<̨tb6m#@3Gooo2r,œovٍ=rq [U^sw{\aՒ;QkfA90ۋt:o??z|_Ʈ]~PHoO;4v1Kǖ-[pepwcxxwq χ+===9B( 2nn>^p!|>ߨbUt*Quű-ۢ88H\U?Ï抢d-QJuuW_=oyXftMc8V\ Jy]-. B]Yr\^vhy`bFhBpxDDDN ﱡcߣ_aCώXyu2L&[o[oцfÚ5k`z-܂^{~YtRա[N4@޸ gٳ?3ƐL&L&eÍtVW!NA&qpE?݋uAUUY7Z[}NO0ğgygy&N8ٳG|-Pq+1w\=<vB:0 G3DF]TAj(^Î;@)``.LDoiiYdbƘlOo}q Z_ ,1r$ىchhHv}>:::}/_WY_|?҂. ۶m3pk(j gY\b5vؒ:yD FCP_=e\]]zȜ9Q]]5k\I,??aӦMM ze@&n(IF( 4z?k_taZJf^/~,^XF0|ItuuɅ;Xlٹe J7pօ 36nxtܨ(曱rJiWƒ>(?scعsk. OA- /^zīַcᮻnŋcǎss(I,=AJhӦMxꩧ>xbvm2;o?h҂+V`޽D B2>9{8餓ۋG}y{Oܹ7tK/9Bpz z\xDcXv,ō7ވ}IqŊI8H*^&w޽X 7$!tkm xSOaŪA϶O{ ewލo~9sQ+`ժUX~=1}݇ .@ `̙2{˖- Oa Ԇ puI>ґfq7gYV'?UJlr N>ds=eذ ;aL's /_|+7t$O?oR~|_5O_ v%'+ x(>s[guV\YTy$G~9袋vZ\.DQl^lY;@?aL2\sM֭ 7 Dz*|MsA?pl , qZ׿Tzp5נs0_M`"l@Θ1out !C__!;|?BؓW:{7x#f͚uTKӘ)2QV.b.[@+믗L?̘5k cZ(_Vz /K/K,9$ц(,2L#v i`hmmmx'wQBп%ؑX o6p0|9ze pLJY@ -Z'9TK,1 -`삠30@ss3jkkۋ>$Ix<ҹCB9" cV_pڂÌMV̈́2|~'Қ]AU@xh 0`0|a?JG2>PeGQ@yPeGQ@yPeǸFP.IENDB`dupeguru-4.3.1/images/old_zoom_in.png000066400000000000000000000264541426171743600176700ustar00rootroot00000000000000PNG  IHDR>asRGBbKGD pHYs$$P$tIME 78 kZ IDATx}yxřzzьf$._!ۀ l0s->N% YXpBCpX6YXpLgd7!a9I`|ɶlK}Hs]UGwzF#Yayգ~UE8('+(D%}"I  JDt kFd\LPmjob ~  Z\ *kQQ+| .$@0X jm_V ׋rn\.8L&i$ b1DQD"D"$IdE5g3_`-d駟FTUUA?( ۱~W|7Sߞ6|@~3̙3zAH9G2DOO8G"H@ pQ?0U?~gsEYYYeYr9(EQ@) & ,Cfu4ڰsN.r@o  W1YfK/)ʜD"D"\.m6t:t:aنORhmmoN$J`7~+Wƈx=  ~ C`ٰ~zTUU&6 &}˖z> zuĔd*48Xl VY=L=x^|>"CoU6`0x)?=>pc) }}x芤1kG*!"nlYLr,FJ6Gc񔫳gg`nNB2xmSq)0tCԬy&ѣG}v_@ p'`pfxpBwvv_ ÿЌ?3gR]~|T.Yu>svW{Vj6%B$9ڇtfMLg$=$3ʰޙQc]=qQq |Sa5IX, L۶m30_ "z0: +#ϧDzSMh [LaQ0tϟ⮚Z9,&jg `3}x@j+>,tvhש}tPwCے;^ZZZꫯA*@ ' Z+`ժUX`mmmyQCqT"ns_/<)9Z0P0\9>gs}6ǔzYqᆭ9N3nv.0Tu6@ ph"t]w}o߾Es!stuuv^ӋSȨ'̟eie53/=VL;3 \.!p8B-twwe۷o矟WjM/\K}}}y_CZǫ{zN7UYV`n]sy 34n!ҹuk6I&@"HD"ѷ!a>CPp Tv;-p:{^]UF5ΚE"<I`Xp8ݍt: m߾?c"Vn]pp:U1N9MTlԪ#S$rYϙS1SzD4O $ t` 5=!#FZ5iEfT7OأG&寮Elv\.~0 "Crjq>S(G&EUuX[OVS\\8Ztg!u] `&1v 8DAJ li̙Y+whP.BFWTT ZRgM袋`6C"3~hS2[qsO:QQ+12QqÓK[j('@)g^&p9Uj!g א)0F$YVl$хL{G.)\ЏySʄ*p8 !G—xt2p^IC,3&IW[3k߲ۭI=l~L!럹~=c\"  md8LF Aw`8`HU:T캿k[.C4U{,`AVe:u*jjj9{o I WӮT5B iQy2у818Xdݹ\68q'0 0<0UX2JOMm1u0,E"PJ9Gyy9v;̙rRH5,pgO<r0suDi:p {?z0RpA#, Pg*++c,guU. Bۋ-pTL19AŇNTPBu@hXsT|0p&+|e-IwH%IV+jkk銫':3b[^, (n̆^>3{#UsI8A1F)J@DQd.K)iw_n/T*%ԀfbAEu\ zxz~]SJRoRf+|n1bZ{pMǃrp8(WŨR6vsT8}nyWzzM3Yw E\E j *ӧ2Dz$"^ZC W0TO0.9\=c=@^z#0$0[u ;e$l6vPO龿no1{V'12s/O55@5`|uO)W{ֳ5ppQ%Oo4h@5J_YGgYHzi'"jl0 YTTxDiVτ\g:4ktU pU (q1Gk# +5oTwiQE>:Bf8su:t YL,*m,&q%V !` qp9D 0313齽c/NK&I4)k0jppzp *6 f٤Ȳ)S=R1!t0, @|B@?7Ŝlf̹Z@ q8!q{<2<2cyX_h6,r Gbߋ. 88%PC9 L~$3ιZ3p.rx+ř9>98ɝؕic`%X" lJ'Sj9WCߺjԫU $@0H6P~bN@L'I`,ef3cp敏 Js$(G\,(I+d  dN yB P?lZѰlE fSПe1\03pY`?~#WyGٜ4#N & )T d9f1_UɌ#* $:9 ZĝBYPDZ1Ιg!7΁)f+c$]j g&YYFL+/+#Z8(!f3L&>:f a u^P-(yV},d# `:ȇ1s9PUkQESM ;V/Xo/&iI2c#1`7JlV1ˆq3R˂b|sY5C+@8A~^D&h`нBi/B}L'F!SA!SL(tƹ(sńt")Lr)fHaD(УL/Ss*4J_(M *v]g)4'4@,  %y]ft)2Yj)xk =Xse$*1zfA3?@{偡Gu9๜}=/Ny>Kz{f0D`sBzۍP@t:OK3F:3x_rkw1F4[0\gݟJo*8Cl[OGa>t *x U )`tQ`!K4lvS#8Fcr ;F(L &ʵ\8˦i%2eI3I"ⷛ%BLB% 0mO ΋[<9VTϯ5F"јÑ1fz=Orj(N:%ڮ7DQrtog< =PK28e Tsu2ʹv\ s-%g9=dFˍ%Nj ?ۺj,BB1HgҮR I`Z4Xpd0>P ')1_^ŅuS cP \ =Gt A  gOϯHݭ]6mMDCrBRlpLuX"cO$3v ݶyO`{g$NXFc2cC sFㅔTp('h~\=(≴+U3a6IQWpdR 8 i,6 4utt.KԾ?-ij¶w`Jj̊<0NéȾ2i%$-&f6fI+ e9ҙKer,i4 QþlKWӋ2wcy;# %oۅԜ`uuvvBQQdLYf$P,@D8Mٸ 7Z1GT0Ŏc:YC!qUxFz䯭Mg~cM?4hl1?OŪDxD1Qq*ZAq1D7+ViGd2@!xHgpxGYG\'{j[u}*B+rR1 U$8N1zZ-P'SYsFxd>yq|4A T1偠XlVE 0!?!\.ۧ3?A)FpT*h4*!] ˸+|P s}-˨XPilFl,1$}6V0 {B-ݑ z>m!Ď܌A}VB 477#L P. *V>[M{g1b)ʰ}$$hNp4‘tiIq4B1& d`s `5tmX,$ R]vOQJOszyNwT0p 8әLMWUV„mn*+1`s5^,*x<ӽf1i>`FR}aѢEHRp8lp:H$pe|'seN9Fvݍ!\; ,_G.P#b ݽ}uWΆ. O;8N,.n\؁@OZ`---bN]VTTEӵ'MW'=o4fcHh[1 cpǧ9&Ӆ+I^:t:-['4!\ػw/,bHx9uxuρ+r dL/iEA)|V/sDO_ɔ"lD"?/&@`_1>6-o/)UK$3?`$fE$Ø`4{kLgru0oJjh(bzW_}>6)ѝPWDL p\ԲT:ݹKEDXT@j ܵؗ3Yj}6 ٤J4 -ҽG{ctwQJ>z`&3g,X͆::;;$RRgE5VU6J񠬬 U-]~}㤔->I|r|>TTT9GooopD,iRY뜆71wzHE#%|bf{SsErƹExYV.)++CUUjs=_~;wD"ܹs1o<,XMMMH:”ҫ~'4\Ej%\ݎʼEQb6q^fϨa2I(1cL>xsiweTiӇdF8z(6mڄE߇nǚ5k0k,>|XAR; a2vQYY)zO2DwwQ?Ӎ!o,2ު c!Y`-S* .e\s:Z0$IBuuXPWAu]]v444೟H Q>P(t|PgZd $InGMMr[F.xvS,.$\N[Sl_rXeٔu|DQK=HlZx qj"}b5KZ\ IDATPbF9N*m݆~z鷺d _ BdKtyj9G,:Y0tvvv"aݸkG|&ߏ{D^ugBPl@Okb@Wxҥxz ;a/3 TnFk#cٲeؼy3u[ouցRj\Wa7+BPp޳>lDEEE޸DѢ]4m@`XxxxV YeeeNJ3gzE*v/rDonł͛7cɒ%j+ߎ_WYj*~tt!mޛ40v-V@`?/__z&HWODBVӉMt:t:z|1677c֭0ؼy3'ewq>OA<䓢˖-駟Ç?t(zqR@ZZ,\P$~u;!J @m>a{F*,^u߿Rn? rF6l >(z! bŊسg-P?&4,<:0onBIA]Tq~fa1c0o}g2zE7Ǐ_Jذanhoo wB zKN .]O>$v1(I۰n:ag?ï~+;K,D\ MP%|| 0x-|rKxW&^׿.ħz ?񏅛xiaŊؿ ~X @u@ڹs'{EB+V]w%o6 Xv-GlB>>k6XhOM<3'8[oU ~?I6 `W@cc#!xG5cӧc˖- 7nđ#GvZp΍\:ބ>D4WUȱRKK nݻ/K,^X=,Yb\z9lk ApX:P׾sZp6lmS=CΝk\;W.;Q۷oM7$zX)no.e?ER&ߎ:X@89l^h !K/_xu[o~qwR ׋/}KksCx%Bes[ .uj"B?8W^-[p `߾e%p@w1c p K̞n݊[nE$9|_Ƃڿ+Ax!2Zns.PF@yK6iLM\9s7\ߏUW]& }$N>IpjK|qF̛7o\җ1!2ZR'/.K@? 7|dAh޼yƲNr(PlJ^z \s V\yA0H/,L v4i`h3<#\Qh B\G<uQۨ>8&ab"'4 Kऔ@/_۶m3<3,r(IV\i<|. =QMBWWթ]±BoL.d '5d_pڂN3ԁFנjЋ%L<0XL:a3H p*%PJT@J(Q %*D%Mz_?F2~ƚuIENDB`dupeguru-4.3.1/images/old_zoom_original.png000066400000000000000000000276351426171743600210700ustar00rootroot00000000000000PNG  IHDR>asBIT|d pHYs$$P$tEXtSoftwarewww.inkscape.org< IDATxyUUw}ܬ7{QѱQFGGmFb̫*# ˫*"HP$@,w߷ޫ]^>ϧ>]]{;<EN]'pr1ESS81ESGX,U Zh4=N2kXL.V3:sL<Ü&.$x c4r)++#SRRB0rNI$ 200@oo/$Ia-$Óhtq|IJX,V|9|>:,-ZDuu5n+H$hnnfǎ455D/G׎FPb1?pU^^^gͼy(//GQcz_!dvoM"(8/h4|t> |em?쳹袋(-=Lfrhi^rpݸuǃt&6óp|7hzɈFX,v]LkyM$9x˦' rQ BR)<ȫJkks =_hb k۴iӸ+)86J144D"K" QRRB (744޽{ٴi]GP'Ǖwҳ'sUW1{J&Ɍz=!?w(K`DAUz) y(R^%0 , mƖ-[>B+h4 ~IFX,xX `Z<|&M2!~hWwzctc|R>Ι_E>;K`_A4ZZZx饗hjj6GGbw*VX%\bzMap\&ge_a^i gѢE!:::(o:+<B߀Jyˣ**0W9=DZ˴v &3a3={-[Ήhq%{||e_L&9x 6lp0F< 8jUKnf̘H/N3廏uH>PW5`k5ҙYU0 ?CQDC5 D[_-ѳ}_7 _6vzW!dᯯ 囗P\.uuuv-[A* *>wT$`y~\ pUWl2@(ޞ!.z6=҅3^,tZ?AiS \B뇓@BCo6gh;[;io|9PpMY4#x<̘1K:ٳ7Zm/pa4s$7n>s= /EQBVyak'wH\$-LII` xW5meW/* z(EQJ;"I6Kѯ*juW2:Xڟȥ{5Uჽ}lN s/l&cN PRR(A,lܸ\rI#&@,Yc\}նUuW7kڃnHKf?5u7^4󬚈?RΪ5IcJ(# ^[*\jG&ivk%ҁd*[f+{]dftfƧP(dH$M0K.y>ITOU^5k8h@Q`ܚ]6)"V,BUQUAQTEAzX)0M9"iKBପ@isOz=("jZ Cd^y)+?r% N:8cƍKZ&aO4+cNߥ^J($d:vVO"y͚Y@U/pa<,ɪbj|2İMEi'佥 6A]~z],?qV]VEww%l%%%uYnق ł CvĄˎi .9^3v3H!]wAݢKeꖭaPT= LK}Q$TL%E$X8oC]bBMWTWVVRQQa~+I#_@frx0 '&YO.+-oiճfW+vTlen=SUUaQͳed]u']_6Kkm .I Cc# ^'/X$H+ 0Ci-(,,/9ܝbv!0 @ v;+ < 0.bfjBة}¬5XϞ[V𺼶cXX}^°2Vx_%AtR 6(S!bkt.l1uA튢w!d2!̉X ͛HCmCv&vHg0cJkݶ綷g6TK>: $Cm?ZzR}>>/YYQz7μK&|L>Y%|8 x pZ;Fk6.)x)|[hB .,yM7 BtjKSZ51 ۩wNRx^*+PfI1 ` 2R4G};0>XlhU*,A*v վmu B:ynTmi ܦH9,)HPUQj{wfz Yqwlfx<6 Ge HQ*+#v,7l/%tLcݶv>4smK!k8ԻmK 6إi2)k3Pv>f,-8;>zc1IK .v3m.0=5a}C [;nBBv"ay!4Q-. NX:MHDKs]i 0 &UptF-bu 3$@ISBFUC(0@5x U k])j! `Œ+z:DRz}_c5)S9M.(Z'&lCnBKH\862CB(@UYa $ P ) b+pefq#җ%ZP;{=t2%]Lb}o!@,s5grO츸I;UUSU[”l"LBP0 aHbXQ'jn߻|J2bNaޤ?,$S t]wkL2c&SaCzO^iQ`Gi-ga8o aiEBAfTrmዼ M`t&my+@wkqcx<\.wԴ>XUQ|ioSZ00U NR@(-?E Wځ|]3?'o @j:ҝٹ|~X%0#l.oyg$]&]W`7|X-|G6 "̳ oWC(VU|7GpRm=Cٞoue>h: 6PHg)rI`<8oV ^~tV'LtC*d+Rؾ9&!d"03ydl@1 $(vEĐ /l'dS[;`!j6@h#Ŏd'@s!(N/DLVwtZȰͽv3nvtO+ !rBFdTPFm9n}}y[N!h-}-b߂vOGy`<`Q:.VF0@y֞4li  u%%,Ho T0LGPP \ P0f毕h 4כ{RI%w8sP빜4w%y[ 'i hM31R;4P{Я݃PZK ! ݰbr B膁n% &4 ANY moM{R2zi/.|@UQX'Y0s(&ƣAiy<;'B>udHdՌD@;>ԥ© kqxuJKҙ\%9uv|ʔ!LZ8erB1]---L6;i ^){vtͬSYQ4CoI 6z(B>Ԁ{\.GU.4M7RYHt#LHbp~DᎰ`{k{o0rV<m%@4b/i푲Ν_KU Aw"X?nP63"/L碏]PHN͙%8OT gpȡhϝoOukFV䯩~0 1޴'A:4dYEk(V K[{FK)?1#f80u'&B 466L&råT}4/3Yͳ|WX/.RL5ŽMj^p4]^/@UUu+\eK)]ucFo"f޽iЧsdW̰%u,b|N# }_L1*cj~fW W"]Pcc=h?y衇&" B۷o'NJPo;%7n?Թ{`$'x/&-~ξ@:~n%I@ EQ2=_u}7!D?#C8p*v#ѹ2dnBٵ!̔0z Nc`8xtxޖ !\b]UUr'hϞ=tww[֭L0f_A&ײm6,vhxڴivD LV龭;^t/WÕhh@\N5(Ps*bd( % 8Kw۞pLht+s1.A_0K'ǒ\"شC2ƧǣXZɊW6X:X2O^5~ 4w}O۞ :r-z- àUCK!zbWlT:[yk:f1RV,hQ5As[ϼ-[*jW޴Kjp8L8HZsmݺ[ouw=8bw1\p!˖-SWWgV{DZ_I,fU<~6 UM{ +& p۲!d ⲹEvm&oii!Nc=F&q6tM]?kvH5?;@:? NٙjR6cBPs6vC2)at[_?a>A" ƾ7\g sq|o( vώ;D"ڹW뺾NӴmk֬O6 q3^*TUeժUTTTPYYi!s>F?>{HhU-n?RH >U-%[.\e5Sϕ)--j}}}twwSOϲyf/fɒ%,[]vN-2~o6IDATCG9p|>w*pvOGptmM Nr֨#al(j{M/d9gv```~￟w~@ ^ٻwEW'jְ;(++.rKO2Ηxnlb0ov K۪+;ՖW8g8 0]=RkaK4Ƌgryvށ[n-[M?h7 .{y> rt˗*@iӦR\i'_nZ%,\j$UY^,j) nWM݃twpvO_D20DAGsÊ}1˴i<J{o;oR[[ˇ?avx|RL6y, FwT]t>1B =CY|;A'KU.:v<MÆ!# zX/AEIp֐rx7馛F||AMkD"r-v?'౞:rWxŊD"aժaWУV{ٴWrkNj.XX Y4`Z 9zF o^+WdڵGfǎ |W x<~Rch?- KoO ,F0 qq> ݋(K.eҥ#||0H$$ {ձHp`$PP(TP#acc#ׯvZF4 |}~X,o~W\Yg޽{?ǍXz@ 93f/H?!Jلp 5`c(s eY^|Ev؁a~FwwaGh3dlݺ:D'<|\ z}}=K,\.HP(d[ 04͞ѹn]vv ֋e!͒H$/ WSS6lF"<ȣ>:k|g}vnm۶9޿A| Ū՟mnE`;x<s԰fJ$C݂oڴ3 p|B[|9()7x/~;Y}91x<~B=!V"'U\am,]s-ma!()Ό477͖u]/=؄y[Z^>x wuMMMla>@oosxϰ=8nD;Ěd(XΣ5mfl??û?OޢiZzq466zbTWWE/~ޒ}h~?_=.]x|߄n6A8@sӧD{{{뺦/t]ֺu:ϻ[?Z] ,Xg3Y w9׾5֯_Hmzjf͚: {cF CN![E6~` !f%HА[~衇F,ik֬u@ g?k6wu\|_avZ~_b .ve>N:0g&}/rrϐx;} I?>Ǐä%@1bI:z5r4kBެz?ꤐACC?< z׿5TjB:xI%mzz_SS7lxP<?L&444x~i}Q&tz֭[g677s]wq@Vo&RڛkQ:" d' SGq6 ]n+ؼy3 \s pnf 1nMR)x<xxS]/;p8?ի|'inn'D_jhhhcJ%B׀,+V7 6mеTU{o1_|1˗/gΝ֦,xS,_9Y*dk똘"qB<ߋ$K ;޽~,^xB;`Æ ?!W_}5 IFYxs/544okO8"w~ RP7n]NjL&=ï~%{noYJL{w;ϙ;444(w0+W\3<ó>;j-wm 駟[S^^'?Igx|J4 @<x]K r"ǃ_|+_[:֭[G0 ZS80qFΝmVOS^^>Ex78pBIQ/:$444B&."pB~<PSSC{{unr~?:ҹS$!Bf:=E7䮻bɒ%55Np`'x|)'>aw2=,YęV?EɌx<~g+3 9H&>^OxasRGBbKGD pHYs$$P$tIME MY IDATx}ixՙ{zWZKk*/ ج6nYL@ 8! !4 0L20&000,40`ɶlK.uR[-Kr}Tk{;9Jۤ'(J(J P`Q:~D5r"{@@p%&זIE~i`ЗvxJd?,.pwV-M/xwp xp.+jn. & tDX hHHdlQEp7oSOEcc#a6( ۱w^ 7T_6@~NÌ3zAL/d===طo>D"Qh?~:Fis9eeeEf( E4oL&f3fs޶,˰X,ENֆ۷oo~{KlfΜK.(qr_eYfr>J~!:;;b~~?U {Z,_>o!8ϺL&8N\.sxĶmD>0vͲƊ+0uƶ$gLNgh`8mF9ӐkPQQ!\N9zzzm6ٳx&_@3^yz7xN~١neSJqlNXrfӪј39{6s( ur#x9eHf1q: @"vڅm۶AQ _ZL QlXv-P(!bۻj@2wliQQ>8BXEo_?Les5K&c5CԃEEEO$8p}]K~_ \Ufǃ~p8zzz .xwOy("iq"ؤZ.q@`nḇT.M*eX\={*$C׆Y:T# N'|>$I͚g2>|[nE(/7@`6xeYM7-}ggg^50+Ӟ>{&5GL}8Ń53.tn3eC"Dt=ozJa1Y8-מW<8+kS]e/JgmwN\e'~f.ܖA}}҂zKR)V7PЂ<X+Vokkˋg5!7{;6ғ&W8TL@=%W9Pp9g\Suzve@ǽ{;eq4N*ˋ&N4 &j4غunF'֭[U83pg9;oEHdTuۓM{4̲\X8!9uE!6D1pm$muMv $rA5mOTfs#cx{W|+ЙL. 8K[ny睗kNK/TK}}}yߟBذx>Os%fԗy:g>C$ΒT-8@6D DdszV3I2}f)o'd*[8Øs`rC3R8NՊD"{7UwyMhhUk׮ժGgAaf6o#pXMy1P(v. z* xqpzNR-1'"| <ׄiSk~ gՕvR]0bn7b@ ఙT=U3*]G/A&(蕕߃Z /,1X,g=!kS2[qsKO2QY+1@=H|Þ9f^f1KOZ( ϛnmHHWceee8䓍Z@ pTWWc1Zl^㔷L&)S=UO_#Nu] j+~/X٬dvZMy&D4=l;{uUK A?@&17\p$Ip!I|ElϙQnM%f"Zg\߮q!*1´5'+!բ58bFzfx<̛7X@ `*xSLAmm-8研b``@\04.Ubv]8/=B` OJq0c?jP%F`}!AayVa4eH5U^w b`XD"srv̞=[?=j 䄐XOx<`xjKS'>|dO!;_CSIG\UûL=C\ACyBD;٬]xjK+`""G \5QpVTUU1X^5=h՞.i 哄/a[p@tAUp8(m@$< auj~}<΁JC)ll c0zNj@ٸ@ ~=)9z{jCL}?mzybΰAs a}>F5I @1 > BbҹԡK=f|W=Bd9fS\¾V9%21c`&E^u8"sT\S8rXe-IwP%IV+.W~a-/kfYLyqo$J|Nug  r 9SyP NPI|X, W 5_;R)l6, *+ER2R&^Y+AqeJdNNl]k@U~6< {#B> <<&IYw E\E j *iӦ2C(pHk r u*U}{*:^#6 T]BSΡC##$?(T/D (aEb;T3d赱t2-ZH|m1ɲ9S=]t Pq-"7d9 L~dfs3 |άx73`i Q83J< ~0pc=x4HtjE6)9WFZ@?3FjՏXt̒Iw$p5C^8L߰yl cs1xG"!Kc / X)=1̡) ;(ƛ P4l6[Q6@Y6e1sI5]̎881 E I70` 0 XBП0l3pi%Y\\5 U_"լ~a,U j?Cp"g|պey]mi%8g3h$tn\T-3tB< 8DʱT$FĎjxF )2L&;v a u^l̊ c`DL C6KK \>-e9[oKIlo='IRY 0t.y~Ō6V[8,B07\4՝ Qy7d~68}B3PW(sCQ>v 6 ,E6Sx(SdHa4y: LwXFBp) W$ '@ H e]F q  qB .aLZ D(OLpUf k 81sٮJ&TbD21peΙ"L8g88DB VY!BxhLy@V?4ٜ\}zaDk"d^.XoIZD|Z}ŞP4=SI`b* ǥb@TuuY F4dtS#8'Q` 0;&U5#IDX6O+i9 1N8eS@EKi)[(sEApq@!e<}+۶SЮű J ym+c2LII$Z Ÿ("^NeQp,/^߮ EhNϮsUC20B%cikNgc׫ .JTq(ԑ\֘ABORh'-}5;1f!tH$t3JixUHRR ,Esα# @| h %f8=D845$p@[`T@((<tl;>$ .$~pt BXt\ !yinX~}n<` Bp\EeYF6lpr/X"cO$3v?X&ݽ>Z*ˬv,I`649$8B 8*,fBxBQ4 pQiW:gC6I^)L&uhсZ\.QS{OnVcYFéX{8T,2i5K6I[Lfl2YeI+ e9ҙKer,I^Xamc/85V ~#ۅ@  ;;;((ylb3f$PfC"Ϧl@L0 /bUh 9~DG}X#E{*<#=֦Rі4hl1?ŪDxck{T:[ MBqas}qD:>:SDQю3}\p9&!(rrsNZS=]fUѣGWo:::D$Ip:;j. LeG%PZ)RA 偠HlV1!?!\.٣?A)ø^ЩT hT>uOqGWtE?"'E{}cp`|9Z:#@6IAׇ#C100*=Pt"@f$Irmʳ,_&Mg\^oȚyϱwB]ZTwX,$ R;vOQJ~HOW-$ ށG4d#z1/TD CHp,нj`Yܯ{CbJly|} ̝K$3v4_G)G#\D2J Dl?LN.WSQQ! ?EQxo1c=vdB@k@A ~)c!в2M砮BKr\/8$h ر72Y*l_=f*n7n#}k.9rD'~1Nq6hٳ1|l6 )Jƞ|r}[>.HI+( v,_ƹA?݃\(ݞ߿txgd^JO)O&W7~ZZZN;8nMMᴙ~<\8h{^Le\#z ˷eGa JdC״w/׉Zv j !g޽x<(++jJFEQv]qBJM  ItRTTTR9z{{|sg~!1 I"JU ui}qpe\%|0b ZUoSsDžpl)ܢgnRQ#nRVVj! i&ؾ};̙sbhjjB:^Ͽ3b0[V\|ŰʛZpz{4_tP& RWL&); 1u.լ|=U3qʴg6(>_.=v;VZ3g:2ҿ{7V `2vQUU%'L[(ëumqfS]]][[S~D"꧘6D);3±xd2A2W;+ά9 IPSS#YPz{{quaǎ#~\{")D) L8h x 7utEA$v֊r\.ik+:z3ik)8;\lb:>(K=Hlj?qr"b%\vVX< `bAmms=T*;?IMM z455=`Ph08 vIDATkb]|s9Zb,rD,^|[ĠŚI&5LQY6Es u+ (~qX<W; !uى\.;wꫯ|>z! Ggg\wuBkJ0Mh ^n!+xbx+Py/Nn7^o^'\.P($rD|n 6lEԀW* Cf+VCGr٦ b͛y3ƐH$H$Ы#TFU!vi䠇f8N8$}Q `ٲeصk~`0 x 4 ;w"2Lx3n  @Tq~fa޼y>}o}Jr$Jv|4RJ2{QJqW_3<3w9s&6n܈5ڊn mmm WD"t" ' @Jw"QOP__<㰘1g1vbK CКA]]]^/iD[[lJB)-74n.tfo~80Yx{9cB-[ SLAkk^_O8:P&0UUUBlE9G6E:[Ǎ?G)}Rz#<2,]W[(8Ssᣏ>Grw+ Ow]x1.\&j0|~B UPg'G;Yl#)!EQW(=C5Ȕ~R Id͛1oxM7[o~;?T@OO>13VYY 6\I4??`MvE@&KC*Z0xG7 B![Ndn7{1,[LD0_|E;]rE% J'pέ/Ƴ>m۶% wq֬Y#_x'5{.-Z}QҥK7x 7;p{9W]uz{{7\ J@X׫<ƚQiӦaƍ v:$ իWsnL$ TԔؿ?(o Boݻx׿… ˸i&,Z8YR֒9wosӽ0֭[-[PzGq饗 ~̙38UV\ye /B.{P[n7,8x-;~.3O~"2 .y2K6a@NOE]_oƘ뮻~ K/Rx^|_7v_ $+ x0!osl޼>֬YW9OG?^~ظq#"ɋN oqغu+Oop͛q뭷gۿ{ƂU M駟l6&^'oDKK 8" kdv={6q !>݃+B!m0ZIx(J5#'nܹst/}jc3D[K*A~ =?[nL͝;XV_ % =Z)㪫GS%).^@v `0O% 0~S~Z<#.\(!B10Y'$ 4jҥزe ^x!CI|rq`0L >/Cn&MjtuuD./d? Y 3Zmm;5cPZ\#5Z:@  G/Q/x+%Z V@Pj%Z V@Pj%Z 6BGӏ<IENDB`dupeguru-4.3.1/images/plus_8.png000066400000000000000000000003471426171743600165630ustar00rootroot00000000000000PNG  IHDRnvgAMAOX2 cHRMz&u0`:pQ<bKGD̿?IDAT]̱ 0Cshұpy^ liz!k2DaKq| :_HAtEXtSoftwareAdobe ImageReadyqe<IENDB`dupeguru-4.3.1/images/search_clear_13.png000066400000000000000000000006161426171743600202660ustar00rootroot00000000000000PNG  IHDR ,gAMA a cHRMz&u0`:pQ<bKGD̿ pHYs  IDATmн+S^"ٸg8dHEvu "1H$~/ Jzq+TY(ؑJ~Ի ff7IWUzts02ܱyYN {TvDUZ .Mox EPx_?/W|l\o1T0ϡ;ȌDq)u/7dA. -.5,^&UnU+ڗM /ktYƱχFIP KIENDB`dupeguru-4.3.1/locale/000077500000000000000000000000001426171743600146315ustar00rootroot00000000000000dupeguru-4.3.1/locale/ar/000077500000000000000000000000001426171743600152335ustar00rootroot00000000000000dupeguru-4.3.1/locale/ar/LC_MESSAGES/000077500000000000000000000000001426171743600170205ustar00rootroot00000000000000dupeguru-4.3.1/locale/ar/LC_MESSAGES/columns.po000066400000000000000000000046121426171743600210430ustar00rootroot00000000000000# msgid "" msgstr "" "Language-Team: Arabic (https://www.transifex.com/voltaicideas/teams/116153/ar/)\n" "Language: ar\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "" #: core\me\prioritize.py:23 msgid "Duration" msgstr "" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "" #: core\me\result_table.py:22 msgid "Time" msgstr "" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "" #: core\me\result_table.py:27 msgid "Title" msgstr "" #: core\me\result_table.py:28 msgid "Artist" msgstr "" #: core\me\result_table.py:29 msgid "Album" msgstr "" #: core\me\result_table.py:30 msgid "Genre" msgstr "" #: core\me\result_table.py:31 msgid "Year" msgstr "" #: core\me\result_table.py:32 msgid "Track Number" msgstr "" #: core\me\result_table.py:33 msgid "Comment" msgstr "" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "" #: core\prioritize.py:156 msgid "Size" msgstr "" dupeguru-4.3.1/locale/ar/LC_MESSAGES/core.po000066400000000000000000000111411426171743600203060ustar00rootroot00000000000000# msgid "" msgstr "" "Language-Team: Arabic (https://www.transifex.com/voltaicideas/teams/116153/ar/)\n" "Language: ar\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "" #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "" #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "" #: core\app.py:72 msgid "Loading" msgstr "" #: core\app.py:73 msgid "Moving" msgstr "" #: core\app.py:74 msgid "Copying" msgstr "" #: core\app.py:75 msgid "Sending to Trash" msgstr "" #: core\app.py:308 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" #: core\app.py:318 msgid "No duplicates found." msgstr "" #: core\app.py:333 msgid "All marked files were copied successfully." msgstr "" #: core\app.py:334 msgid "All marked files were moved successfully." msgstr "" #: core\app.py:335 msgid "All marked files were successfully sent to Trash." msgstr "" #: core\app.py:343 msgid "Could not load file: {}" msgstr "" #: core\app.py:399 msgid "'{}' already is in the list." msgstr "" #: core\app.py:401 msgid "'{}' does not exist." msgstr "" #: core\app.py:410 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "" #: core\app.py:486 msgid "Select a directory to copy marked files to" msgstr "" #: core\app.py:487 msgid "Select a directory to move marked files to" msgstr "" #: core\app.py:527 msgid "Select a destination for your exported CSV" msgstr "" #: core\app.py:534 core\app.py:801 core\app.py:811 msgid "Couldn't write to file: {}" msgstr "" #: core\app.py:559 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" #: core\app.py:727 core\app.py:740 msgid "You are about to remove %d files from results. Continue?" msgstr "" #: core\app.py:774 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "" #: core\app.py:821 msgid "The selected directories contain no scannable file." msgstr "" #: core\app.py:835 msgid "Collecting files to scan" msgstr "" #: core\app.py:891 msgid "%s (%d discarded)" msgstr "" #: core\engine.py:244 core\engine.py:288 msgid "0 matches found" msgstr "" #: core\engine.py:262 core\engine.py:296 msgid "%d matches found" msgstr "" #: core\gui\deletion_options.py:73 msgid "You are sending {} file(s) to the Trash." msgstr "" #: core\gui\exclude_list_table.py:15 msgid "Regular Expressions" msgstr "" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "" #: core\me\scanner.py:23 msgid "Tags" msgstr "" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "" #: core\pe\matchblock.py:181 msgid "Performed %d/%d chunk matches" msgstr "" #: core\pe\matchblock.py:191 msgid "Preparing for matching" msgstr "" #: core\pe\matchblock.py:244 msgid "Verified %d/%d matches" msgstr "" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "" #: core\prioritize.py:70 msgid "None" msgstr "" #: core\prioritize.py:100 msgid "Ends with number" msgstr "" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "" #: core\prioritize.py:102 msgid "Longest" msgstr "" #: core\prioritize.py:103 msgid "Shortest" msgstr "" #: core\prioritize.py:140 msgid "Highest" msgstr "" #: core\prioritize.py:140 msgid "Lowest" msgstr "" #: core\prioritize.py:169 msgid "Newest" msgstr "" #: core\prioritize.py:169 msgid "Oldest" msgstr "" #: core\results.py:142 msgid "%d / %d (%s / %s) duplicates marked." msgstr "" #: core\results.py:149 msgid " filter: %s" msgstr "" #: core\scanner.py:85 msgid "Read size of %d/%d files" msgstr "" #: core\scanner.py:109 msgid "Read metadata of %d/%d files" msgstr "" #: core\scanner.py:147 msgid "Almost done! Fiddling with results..." msgstr "" #: core\se\scanner.py:18 msgid "Folders" msgstr "" dupeguru-4.3.1/locale/ar/LC_MESSAGES/ui.po000066400000000000000000000536251426171743600200100ustar00rootroot00000000000000# msgid "" msgstr "" "Language-Team: Arabic (https://www.transifex.com/voltaicideas/teams/116153/ar/)\n" "Language: ar\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" #: qt/app.py:81 msgid "Quit" msgstr "" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "" #: qt/app.py:87 msgid "Open Debug Log" msgstr "" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "" #: qt/app.py:251 msgid "{} file (*.{})" msgstr "" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr "" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "" #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "" #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "" #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "" #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "" #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "" #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "" #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "" #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "" #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "" #: qt/result_window.py:102 msgid "Mark" msgstr "" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "" #: qt/result_window.py:185 msgid "{} Results" msgstr "" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "" #: qt/result_window.py:194 msgid "Delta Values" msgstr "" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "" #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "" #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "" #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" #: qt\app.py:256 msgid "Results" msgstr "" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "" #: qt\preferences_dialog.py:285 msgid "General" msgstr "" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "" dupeguru-4.3.1/locale/columns.pot000066400000000000000000000042431426171743600170400ustar00rootroot00000000000000 msgid "" msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: utf-8\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "" #: core\me\prioritize.py:23 msgid "Duration" msgstr "" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:94 #: core\se\result_table.py:19 msgid "Filename" msgstr "" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "" #: core\me\result_table.py:22 msgid "Time" msgstr "" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:165 core\se\result_table.py:23 msgid "Modification" msgstr "" #: core\me\result_table.py:27 msgid "Title" msgstr "" #: core\me\result_table.py:28 msgid "Artist" msgstr "" #: core\me\result_table.py:29 msgid "Album" msgstr "" #: core\me\result_table.py:30 msgid "Genre" msgstr "" #: core\me\result_table.py:31 msgid "Year" msgstr "" #: core\me\result_table.py:32 msgid "Track Number" msgstr "" #: core\me\result_table.py:33 msgid "Comment" msgstr "" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "" #: core\prioritize.py:158 msgid "Size" msgstr "" dupeguru-4.3.1/locale/core.pot000066400000000000000000000107751426171743600163170ustar00rootroot00000000000000 msgid "" msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: utf-8\n" #: core\app.py:44 msgid "There are no marked duplicates. Nothing has been done." msgstr "" #: core\app.py:45 msgid "There are no selected duplicates. Nothing has been done." msgstr "" #: core\app.py:46 msgid "You're about to open many files at once. Depending on what those files are opened with, doing so can create quite a mess. Continue?" msgstr "" #: core\app.py:73 msgid "Scanning for duplicates" msgstr "" #: core\app.py:74 msgid "Loading" msgstr "" #: core\app.py:75 msgid "Moving" msgstr "" #: core\app.py:76 msgid "Copying" msgstr "" #: core\app.py:77 msgid "Sending to Trash" msgstr "" #: core\app.py:291 msgid "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again." msgstr "" #: core\app.py:302 msgid "No duplicates found." msgstr "" #: core\app.py:317 msgid "All marked files were copied successfully." msgstr "" #: core\app.py:319 msgid "All marked files were moved successfully." msgstr "" #: core\app.py:321 msgid "All marked files were deleted successfully." msgstr "" #: core\app.py:323 msgid "All marked files were successfully sent to Trash." msgstr "" #: core\app.py:328 msgid "Could not load file: {}" msgstr "" #: core\app.py:384 msgid "'{}' already is in the list." msgstr "" #: core\app.py:386 msgid "'{}' does not exist." msgstr "" #: core\app.py:394 msgid "All selected %d matches are going to be ignored in all subsequent scans. Continue?" msgstr "" #: core\app.py:471 msgid "Select a directory to copy marked files to" msgstr "" #: core\app.py:473 msgid "Select a directory to move marked files to" msgstr "" #: core\app.py:512 msgid "Select a destination for your exported CSV" msgstr "" #: core\app.py:518 core\app.py:773 core\app.py:783 msgid "Couldn't write to file: {}" msgstr "" #: core\app.py:541 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" #: core\app.py:697 core\app.py:709 msgid "You are about to remove %d files from results. Continue?" msgstr "" #: core\app.py:745 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "" #: core\app.py:792 msgid "The selected directories contain no scannable file." msgstr "" #: core\app.py:808 msgid "Collecting files to scan" msgstr "" #: core\app.py:858 msgid "%s (%d discarded)" msgstr "" #: core\directories.py:190 msgid "Collected {} files to scan" msgstr "" #: core\directories.py:206 msgid "Collected {} folders to scan" msgstr "" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "" #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "" #: core\me\scanner.py:23 msgid "Tags" msgstr "" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "" #: core\prioritize.py:70 msgid "None" msgstr "" #: core\prioritize.py:102 msgid "Ends with number" msgstr "" #: core\prioritize.py:103 msgid "Doesn't end with number" msgstr "" #: core\prioritize.py:104 msgid "Longest" msgstr "" #: core\prioritize.py:105 msgid "Shortest" msgstr "" #: core\prioritize.py:142 msgid "Highest" msgstr "" #: core\prioritize.py:142 msgid "Lowest" msgstr "" #: core\prioritize.py:171 msgid "Newest" msgstr "" #: core\prioritize.py:171 msgid "Oldest" msgstr "" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "" #: core\results.py:141 msgid " filter: %s" msgstr "" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "" #: core\se\scanner.py:18 msgid "Folders" msgstr "" dupeguru-4.3.1/locale/cs/000077500000000000000000000000001426171743600152365ustar00rootroot00000000000000dupeguru-4.3.1/locale/cs/LC_MESSAGES/000077500000000000000000000000001426171743600170235ustar00rootroot00000000000000dupeguru-4.3.1/locale/cs/LC_MESSAGES/columns.po000066400000000000000000000054401426171743600210460ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # msgid "" msgstr "" "Last-Translator: Fuan , 2021\n" "Language-Team: Czech (https://www.transifex.com/voltaicideas/teams/116153/cs/)\n" "Language: cs\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "Cesta k souboru" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "Chybové hlášení" #: core\me\prioritize.py:23 msgid "Duration" msgstr "Doba trvání" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "Bitrate" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "Vzorkovací frekvence" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "Název souboru" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "Složka" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "Velikost (MB)" #: core\me\result_table.py:22 msgid "Time" msgstr "Čas" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "Vzorkovací frekvence" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "Typ" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "Změna" #: core\me\result_table.py:27 msgid "Title" msgstr "Titul" #: core\me\result_table.py:28 msgid "Artist" msgstr "Umělec" #: core\me\result_table.py:29 msgid "Album" msgstr "Album" #: core\me\result_table.py:30 msgid "Genre" msgstr "Žánr" #: core\me\result_table.py:31 msgid "Year" msgstr "Rok" #: core\me\result_table.py:32 msgid "Track Number" msgstr "Číslo stopy" #: core\me\result_table.py:33 msgid "Comment" msgstr "Komentář" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "Shoda %" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "Slov" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "Počet kopií" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "Rozměry" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "Velikost (KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "Časové razítko EXIF" #: core\prioritize.py:156 msgid "Size" msgstr "Velikost" dupeguru-4.3.1/locale/cs/LC_MESSAGES/core.po000066400000000000000000000155151426171743600203220ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # msgid "" msgstr "" "Last-Translator: Fuan , 2021\n" "Language-Team: Czech (https://www.transifex.com/voltaicideas/teams/116153/cs/)\n" "Language: cs\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "Neexistují žádné označené duplikáty. Nic se nestalo." #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "Nejsou k dispozici žádné vybrané duplikáty. Nic se nestalo." #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" "Chystáte se otevřít více souborů najednou. V závislosti na tom, s čím jsou " "tyto soubory otevřeny, to může způsobit docela nepořádek. Pokračovat?" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "Vyhledávám duplicity" #: core\app.py:72 msgid "Loading" msgstr "Nahrávám" #: core\app.py:73 msgid "Moving" msgstr "Přesouvám" #: core\app.py:74 msgid "Copying" msgstr "Kopíruji" #: core\app.py:75 msgid "Sending to Trash" msgstr "Vyhazuji do koše" #: core\app.py:289 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" "Předchozí akce stále nebyla ukončena. Novou zatím nemůžete spustit. Počkejte" " pár sekund a zkuste to znovu." #: core\app.py:300 msgid "No duplicates found." msgstr "Nebyli nalezeny žádné duplicity." #: core\app.py:315 msgid "All marked files were copied successfully." msgstr "Všechny označené soubory byly úspěšně zkopírovány." #: core\app.py:317 msgid "All marked files were moved successfully." msgstr "Všechny označené soubory byly úspěšně přesunuty." #: core\app.py:319 msgid "All marked files were deleted successfully." msgstr "" #: core\app.py:321 msgid "All marked files were successfully sent to Trash." msgstr "Všechny označené soubory byly úspěšně odeslány do koše." #: core\app.py:326 msgid "Could not load file: {}" msgstr "Soubor se nepodařilo načíst: {}" #: core\app.py:382 msgid "'{}' already is in the list." msgstr "'{}' již je v seznamu." #: core\app.py:384 msgid "'{}' does not exist." msgstr "'{}' neexistuje." #: core\app.py:392 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "" "Všech %d vybraných shod bude v následujících hledáních ignorováno. " "Pokračovat?" #: core\app.py:469 msgid "Select a directory to copy marked files to" msgstr "Vyberte adresář, do kterého chcete zkopírovat označené soubory" #: core\app.py:471 msgid "Select a directory to move marked files to" msgstr "Vyberte adresář, kam chcete přesunout označené soubory" #: core\app.py:510 msgid "Select a destination for your exported CSV" msgstr "Vyberte cíl pro exportovaný soubor CSV" #: core\app.py:516 core\app.py:771 core\app.py:781 msgid "Couldn't write to file: {}" msgstr "Nelze zapisovat do souboru: {}" #: core\app.py:539 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" "Nedefinoval jste žádný uživatelský příkaz. Nadefinujete ho v předvolbách." #: core\app.py:695 core\app.py:707 msgid "You are about to remove %d files from results. Continue?" msgstr "Chystáte se z výsledků odstranit %d souborů. Pokračovat?" #: core\app.py:743 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} duplicitní skupiny byly změněny změně priorit." #: core\app.py:790 msgid "The selected directories contain no scannable file." msgstr "Vybrané adresáře neobsahují žádné soubory vhodné k prohledávání." #: core\app.py:803 msgid "Collecting files to scan" msgstr "Shromažďuji prohlížené soubory" #: core\app.py:850 msgid "%s (%d discarded)" msgstr "%s (%d vyřazeno)" #: core\directories.py:191 msgid "Collected {} files to scan" msgstr "" #: core\directories.py:207 msgid "Collected {} folders to scan" msgstr "" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "Posíláte-{} soubory do koše." #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "Regulární výrazy" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "Opravdu chcete odstranit všech %d položek ze seznamu výjimek?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "Název souboru" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "Název souboru - pole" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "Název souboru - pole (bez objednávky)" #: core\me\scanner.py:23 msgid "Tags" msgstr "Tagy" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "Obsah" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "Analyzováno %d/%d snímků" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "Provedeno %d/%d porovnání bloků" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "Připravuji porovnávání" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "Ověřeno %d/%d shod" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "Přečetl EXIF %d/%d obrázků" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "Časové razítko EXIF" #: core\prioritize.py:70 msgid "None" msgstr "Zádný" #: core\prioritize.py:100 msgid "Ends with number" msgstr "Končí číslem" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "Nekončí číslem" #: core\prioritize.py:102 msgid "Longest" msgstr "Nejdelší" #: core\prioritize.py:103 msgid "Shortest" msgstr "Nejkratší" #: core\prioritize.py:140 msgid "Highest" msgstr "Nejvyšší" #: core\prioritize.py:140 msgid "Lowest" msgstr "Nejnižší" #: core\prioritize.py:169 msgid "Newest" msgstr "Nejnovější" #: core\prioritize.py:169 msgid "Oldest" msgstr "Nejstarší" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) duplicit označeno." #: core\results.py:141 msgid " filter: %s" msgstr " filtr: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "Read size of %d/%d files" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "Načtena metadata %d/%d souborů" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "Skoro hotovo! Fidlování s výsledky..." #: core\se\scanner.py:18 msgid "Folders" msgstr "Složky" dupeguru-4.3.1/locale/cs/LC_MESSAGES/ui.po000066400000000000000000001043101426171743600177770ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2022 # Fuan , 2022 # msgid "" msgstr "" "Last-Translator: Fuan , 2022\n" "Language-Team: Czech (https://www.transifex.com/voltaicideas/teams/116153/cs/)\n" "Language: cs\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" #: qt/app.py:81 msgid "Quit" msgstr "Ukončete" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "Možnosti" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Seznam ignorovaných" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Vyčistit cache snímků" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Nápověda dupeGuru" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "O aplikaci" #: qt/app.py:87 msgid "Open Debug Log" msgstr "Otevřete protokol ladění" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "Opravdu chcete odstranit veškeré uložené analýzy snímků?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "Picture cache cleared." #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} soubor (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Možnosti mazání" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Propojte odstraněné soubory" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" "Po smazán duplikát, umístit odkaz cílení referenční soubor nahradit " "odstraněný soubor." #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "Pevný odkaz" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "Symbolický odkaz" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr " (nepodporovaný)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Přímo smazání souborů" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "Místo odesílání souborů do koše je přímo odstraňte. Tato možnost se obvykle " "používá jako řešení, když nefunguje normální metoda odstranění." #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Pokračovat" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Zrušit" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "Atribut" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "Vybráno" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "Referenční" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "Nahrát výsledky..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "Okno s výsledky" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "Přidat složku..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "Soubor" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "Zobrazit" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "Nápověda" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "Nahrát nedávné výsledky" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "Aplikační režim:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "Muzika" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "Obrázek" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "Standard" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "Typ skenování:" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "Více možností" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "Vyberte složky, které chcete prohledat a stiskněte \"Prohledat\"." #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "Nahrát výsledky" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "Prohledat" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "Neuložené výsledky" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "Máte neuložené výsledky, opravdu si přejete skončit?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "Vyberte složku, kterou chcete přidat do prohledávacího seznamu" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "Vyberte soubor s výsledky, který chcete nahrát" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "Všechny soubory (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "dupeGuru Výsledek (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "Spusťte nové skenování" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "Máte neuložené výsledky, opravdu si přejete pokračovat?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "Název" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "Stav" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "Vyjmuto" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "Normální" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "Odebrat vybrané" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "Vyprázdnit" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "Zavřít" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "Detaily" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "Prohledávané tagy:" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "Stopa" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "Umělec" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "Album" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "Titul" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "Žánr" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "Rok" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "Váha slov" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "Shoda podobných slov" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "Různé druhy souborů" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "Při filtrování používat regulární výrazy" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "Po smazání a přesunu odstranit prázdné složky" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "Ignorovat duplicity ve formě hardlinků na stejný soubor" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "Ladící režim (vyžaduje restart)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "Porovnávat snímky s různými rozměry" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "Tvrdost filtru:" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "Více výsledků" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "Méně výsledků" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "Velikost písma:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "Jazyk:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "Kopírovat a přesunout:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "Přímo v cílovém umístění" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "Vytvořit s relativní cestou" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "Vytvořit s absolutní cestou" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "Uživatelský příkaz (argumenty: %d pro duplicity, %r pro odkazy):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "dupeGuru has to restart for language changes to take effect." #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "Změnit prioritu duplicit" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "Do pole vpravo přidejte kritéria a klepnutím na tlačítko OK odešlete " "duplicity, které těmto kritériím vyhovují do referenčního umístění příslušné" " skupiny. Více informací naleznete v nápovědě." #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "Problémy!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "Při zpracování některých (nebo všech) souborů se vyskytly problémy. Jejich " "příčina je popsána v tabulce dole. Dotčené soubory nebyli odstraněny z " "výsledků." #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "Ukázat vybrané ve správci souborů" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "Akce" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "Zobrazit pouze duplicity" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "Zobrazit rozdíly" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "Odeslat označené položky do koše..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "Označené přesunout..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "Označené kopírovat..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "Odstranit označené z výsledků" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "Změnit prioritu výsledků..." #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "Odstranit výběr z výsledků" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "Přidat výběr na seznam výjimek" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "Označit jako referenční" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "Vybrané otevřít výchozí aplikací" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "Otevřete složku obsahující vybrané" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "Vybrané přejmenovat" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "Označit vše" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "Zrušit označení" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "Invertovat označení" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "Označit vybrané" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "Export do HTML" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "Export do CSV" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "Uložit výsledky..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "Spustit vlastní příkaz" #: qt/result_window.py:102 msgid "Mark" msgstr "Označit" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "Sloupce" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "Výchozí nastavení" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} Výsledky" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "Jen duplicity" #: qt/result_window.py:194 msgid "Delta Values" msgstr "Hodnoty Delta" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "Vyberte soubor pro uložení výsledků" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "Ignorovat soubory menší než" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ Výsledky" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "Akce" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "Přidat novou složku..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "Pokročilé" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "Automaticky kontrolovat aktualizace" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "Základní" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "Vše do popředí" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "Zkontrolovat aktualizace..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "Zavřít okno" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "Kopírovat" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "Uživatelský příkaz (argumenty: %d pro duplicity, %r pro odkazy):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "Vyjmout" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Delta" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "Detaily vybraného souboru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "Detaily" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "Adresáře" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "Předvolby dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "Výsledky dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "dupeGuru Website" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "Upravit" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "Export výsledků do CSV" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "Exportovat výsledky do XHTML" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "Méně výsledků" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "Filtr" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "Tvrdost filtru:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Filtrovat výsledky..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "Výběr složky" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "Velikost písma:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "Skrýt dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "Skrýt ostatní" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "Ignorovat soubory menší než:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "Nahrát ze souboru..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "Minimalizovat" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "Režim" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "Více výsledků" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "Ok" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "Vložit" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "Předvolby..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "Rychlý pohled" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "Ukončit dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "Výchozí nastavení" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "Obnovit výchozí" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "Odhalit" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Vybrané otevřít ve Finderu" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "Vybrat vše" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Vyhodit označené do koše..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "Služby" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "Zobrazit vše" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "Spustit hledání duplicit" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "The name '%@' already exists." #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "Okno" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Zoom" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "Vyloučení Filtry" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "Výsledky skenování" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "Načíst složky..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "Uložit složky..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "Vyberte soubor složky, kterou chcete načíst" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "dupeGuru Složky (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "Vyberte soubor, do kterého chcete uložit své složky" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "dupeGuru Složky (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "Přidat" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "obnovit výchozí" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "Testovací řetězec" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "Zadejte python regulární výraz zde..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "Sem zadejte cestu k systému souborů nebo název souboru..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Tyto regulární výrazy pythonů (rozlišují velká a malá písmena) odfiltrují soubory během skenování.
    Pokud se jejich název shoduje s jedním z vybraných regulárních výrazů, bude mít jejich výchozí stav na kartě „Složky“ nastaven na „Vyloučeno“.
    U každého shromážděného souboru se provedou dva testy, aby se zjistilo, zda jej zcela ignorovat nebo ne:
  • 1. Regulární výrazy bez oddělovače cesty budou porovnávány pouze s názvem souboru.
  • \n" "
  • 2. Regulární výrazy s alespoň jedním oddělovačem cesty budou porovnány s úplnou cestou k souboru.

  • \n" "Příklad: pokud chcete odfiltrovat soubory PNG pouze z složky „My Pictures“:
    .*My\\sPictures\\\\.*\\.png

    Regulární výraz můžete vyzkoušet pomocí tlačítka „testovací řetězec“ po vložení falešné cesty do testovacího pole:
    C:\\\\User\\My Pictures\\test.png

    \n" "Odpovídající regulární výrazy budou zvýrazněny.
    Pokud existuje alespoň jedno zvýraznění, bude testovaná cesta nebo název souboru během skenování ignorována.

    Složky a soubory začínající tečkou \".\" jsou ve výchozím nastavení odfiltrovány.

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "Chyba kompilace:" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "zvýšení zoom" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "zmenšit zoom" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "Normální velikost" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "Nejlépe fit" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "Režim mezipaměti obrázků:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "Přepsat ikony motivů na panelu nástrojů prohlížeče obrázků" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" "Využít naše vlastní vnitřní ikony namísto ty, které poskytují téma motorem" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "Zobrazit posuvníky v prohlížečích obrázků" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" "Pokud se zobrazený obrázek nevejde do výřezu, zobrazte posuvníky tak, aby se" " pohled pohyboval." #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "Použít výchozí pozici pro panel karet (vyžaduje restart)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "Umístěte panel karet pod hlavní nabídku namísto vedle ní.\n" "V systému MacOS místo toho vyplní panel karet šířku okna." #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "Pro reference použijte tučné písmo" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "Referenční barva popředí:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "Referenční barva pozadí:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "Barva popředí Delta:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "Zobrazit záhlaví a lze jej ukotvit" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" "Zatímco je záhlaví skryto, přetáhněte plovoucí okno pomocí modifikační " "klávesy" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "Záhlaví lze deaktivovat, pouze když je okno ukotveno" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "Vertikální záhlaví" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "Změňte záhlaví z vodorovné nahoře na svislou na levé straně" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "Zobrazit panel karet" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Tyto regulární výrazy pythonů (rozlišují velká a malá písmena) odfiltrují soubory během skenování.
    Pokud se jejich název shoduje s jedním z vybraných regulárních výrazů, bude mít jejich výchozí stav na kartě „Složky“ nastaven na „Vyloučeno“.
    U každého shromážděného souboru se provedou dva testy, aby se zjistilo, zda jej zcela ignorovat nebo ne:
  • 1. Regulární výrazy bez oddělovače cesty budou porovnávány pouze s názvem souboru.
  • \n" "
  • 2. Regulární výrazy s alespoň jedním oddělovačem cesty budou porovnány s úplnou cestou k souboru.

  • \n" "Příklad: pokud chcete odfiltrovat soubory PNG pouze z složky „My Pictures“:
    .*My\\sPictures\\\\.*\\.png

    Regulární výraz můžete vyzkoušet pomocí tlačítka „testovací řetězec“ po vložení falešné cesty do testovacího pole:
    C:\\\\User\\My Pictures\\test.png

    \n" "Odpovídající regulární výrazy budou zvýrazněny.
    Pokud existuje alespoň jedno zvýraznění, bude testovaná cesta nebo název souboru během skenování ignorována.

    Složky a soubory začínající tečkou \".\" jsou ve výchozím nastavení odfiltrovány.

    " #: qt\app.py:256 msgid "Results" msgstr "Výsledky" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "Obecné rozhraní" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "Tabulka výsledků" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "Okno podrobností" #: qt\preferences_dialog.py:285 msgid "General" msgstr "Všeobecné" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "Zobrazit" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" #: qt\app.py:299 msgid "Cache cleared." msgstr "" #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "" #: qt\about_box.py:31 msgid "About {}" msgstr "O {}" #: qt\about_box.py:47 msgid "Version {}" msgstr "Verze {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "" #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "Licencován pod GPLv3" #: qt\about_box.py:68 msgid "No update available." msgstr "" #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "Chybové hlášení" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "Něco se pokazilo. Co takhle nahlásit chybu?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "Chybové zprávy by měly být vykázáno jako emise GitHub. Můžete zkopírovat chybovou TraceBack výše a vložit ji do nové emise.\n" "\n" "Prosím, ujistěte se, že ke spuštění hledání jakýchkoli již existujících otázek předem. Také se ujistěte, vyzkoušet nejnovější dostupnou verzi z úložiště, protože chyba jste se setkali již mohla být oprava.\n" "\n" "To, co obvykle opravdu pomáhá, je přidat popis toho, jak jste se dostali k chybě. Dík!\n" "\n" "Přestože by aplikace měla po této chybě pokračovat, může být v nestabilním stavu, proto se doporučuje aplikaci restartovat." #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Přejít na Github" #: qt\preferences.py:24 msgid "Czech" msgstr "česky" #: qt\preferences.py:25 msgid "German" msgstr "Německy" #: qt\preferences.py:26 msgid "Greek" msgstr "řecky" #: qt\preferences.py:27 msgid "English" msgstr "Anglicky." #: qt\preferences.py:28 msgid "Spanish" msgstr "španělsky" #: qt\preferences.py:29 msgid "French" msgstr "Francouzsky" #: qt\preferences.py:30 msgid "Armenian" msgstr "arménsky" #: qt\preferences.py:31 msgid "Italian" msgstr "italsky" #: qt\preferences.py:32 msgid "Japanese" msgstr "Japonština" #: qt\preferences.py:33 msgid "Korean" msgstr "korejsky" #: qt\preferences.py:34 msgid "Malay" msgstr "Malajština" #: qt\preferences.py:35 msgid "Dutch" msgstr "holandsky" #: qt\preferences.py:36 msgid "Polish" msgstr "polsky" #: qt\preferences.py:37 msgid "Brazilian" msgstr "brazilsky" #: qt\preferences.py:38 msgid "Russian" msgstr "rusky" #: qt\preferences.py:39 msgid "Turkish" msgstr "Turečtina" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "ukrajinsky" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "vietnamsky" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "čínsky (zjednodušeně)" #: qt\recent.py:54 msgid "Clear List" msgstr "Vymazání seznamu" #: qt\search_edit.py:78 msgid "Search..." msgstr "Hledat..." dupeguru-4.3.1/locale/de/000077500000000000000000000000001426171743600152215ustar00rootroot00000000000000dupeguru-4.3.1/locale/de/LC_MESSAGES/000077500000000000000000000000001426171743600170065ustar00rootroot00000000000000dupeguru-4.3.1/locale/de/LC_MESSAGES/columns.po000066400000000000000000000052501426171743600210300ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # msgid "" msgstr "" "Last-Translator: Andrew Senetar , 2021\n" "Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\n" "Language: de\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "Dateipfad" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "Fehlermeldung" #: core\me\prioritize.py:23 msgid "Duration" msgstr "Dauer" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "Bitrate" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "Abtastrate" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "Dateiname" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "Ordner" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "Größe (MB)" #: core\me\result_table.py:22 msgid "Time" msgstr "Zeit" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "Abtastrate" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "Typ" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "Geändert" #: core\me\result_table.py:27 msgid "Title" msgstr "Titel" #: core\me\result_table.py:28 msgid "Artist" msgstr "Künstler" #: core\me\result_table.py:29 msgid "Album" msgstr "Album" #: core\me\result_table.py:30 msgid "Genre" msgstr "Genre" #: core\me\result_table.py:31 msgid "Year" msgstr "Jahr" #: core\me\result_table.py:32 msgid "Track Number" msgstr "Titel Nummer" #: core\me\result_table.py:33 msgid "Comment" msgstr "Kommentar" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "Übereinstimmung %" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "genutzte Wörter" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "Anzahl der Duplikate" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "Auflösung" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "Größe (KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "EXIF Zeitstempel" #: core\prioritize.py:156 msgid "Size" msgstr "Größe" dupeguru-4.3.1/locale/de/LC_MESSAGES/core.po000066400000000000000000000157111426171743600203030ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # Robert M, 2021 # msgid "" msgstr "" "Last-Translator: Robert M, 2021\n" "Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\n" "Language: de\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "Keine markierten Duplikate, daher wurde nichts getan." #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "Keine ausgewählten Duplikate, daher wurde nichts getan." #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" "Sie sind dabei, sehr viele Dateien gleichzeitig zu öffnen. Das kann zu " "ziemlichem Durcheinander führen! Trotzdem fortfahren?" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "Suche nach Duplikaten" #: core\app.py:72 msgid "Loading" msgstr "Lade" #: core\app.py:73 msgid "Moving" msgstr "Verschiebe" #: core\app.py:74 msgid "Copying" msgstr "Kopiere" #: core\app.py:75 msgid "Sending to Trash" msgstr "Verschiebe in den Papierkorb" #: core\app.py:289 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" "Eine vorherige Aktion ist noch in der Bearbeitung. Sie können noch keine " "Neue starten. Warten Sie einige Sekunden und versuchen es erneut." #: core\app.py:300 msgid "No duplicates found." msgstr "Keine Duplikate gefunden." #: core\app.py:315 msgid "All marked files were copied successfully." msgstr "Alle markierten Dateien wurden erfolgreich kopiert." #: core\app.py:317 msgid "All marked files were moved successfully." msgstr "Alle markierten Dateien wurden erfolgreich verschoben." #: core\app.py:319 msgid "All marked files were deleted successfully." msgstr "Alle markierten Dateien wurden erfolgreich gelöscht." #: core\app.py:321 msgid "All marked files were successfully sent to Trash." msgstr "" "Alle markierten Dateien wurden erfolgreich in den Papierkorb verschoben." #: core\app.py:326 msgid "Could not load file: {}" msgstr "Konnte Datei {} nicht laden." #: core\app.py:382 msgid "'{}' already is in the list." msgstr "'{}' ist bereits in der Liste." #: core\app.py:384 msgid "'{}' does not exist." msgstr "'{}' existiert nicht." #: core\app.py:392 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "" "Alle %d ausgewählten Dateien werden in zukünftigen Scans ignoriert. " "Fortfahren?" #: core\app.py:469 msgid "Select a directory to copy marked files to" msgstr "" "Wählen Sie ein Verzeichnis aus, in das markierte Dateien kopiert werden " "sollen" #: core\app.py:471 msgid "Select a directory to move marked files to" msgstr "" "Wählen Sie ein Verzeichnis aus, in das markierte Dateien verschoben werden " "sollen" #: core\app.py:510 msgid "Select a destination for your exported CSV" msgstr "Zielverzeichnis für den CSV Export angeben" #: core\app.py:516 core\app.py:771 core\app.py:781 msgid "Couldn't write to file: {}" msgstr "Konnte Datei {} nicht schreiben." #: core\app.py:539 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" "Sie haben noch keinen Befehl erstellt. Bitte dies in den Einstellungen vornehmen.\n" "Bsp.: \"C:\\Program Files\\Diff\\Diff.exe\" \"%d\" \"%r\"" #: core\app.py:695 core\app.py:707 msgid "You are about to remove %d files from results. Continue?" msgstr "%d Dateien werden aus der Ergebnisliste entfernt. Fortfahren?" #: core\app.py:743 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} Duplikat-Gruppen wurden durch die Neu-Priorisierung geändert." #: core\app.py:790 msgid "The selected directories contain no scannable file." msgstr "Ausgewählte Ordner enthalten keine scannbaren Dateien." #: core\app.py:803 msgid "Collecting files to scan" msgstr "Sammle zu scannende Dateien..." #: core\app.py:850 msgid "%s (%d discarded)" msgstr "%s (%d verworfen)" #: core\directories.py:191 msgid "Collected {} files to scan" msgstr "{} Dateien für Scan gesammelt" #: core\directories.py:207 msgid "Collected {} folders to scan" msgstr "{} Ordner für Scan gesammelt" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "%d Treffer in %d Gruppen gefunden" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "Verschiebe {} Datei(en) in den Papierkorb." #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "Reguläre Ausdrücke" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "Möchten Sie wirklich alle %d Einträge aus der Ausnahmeliste löschen?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "Dateiname" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "Dateiname - Bereiche" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "Dateiname - Bereiche (ohne Reihenfolge)" #: core\me\scanner.py:23 msgid "Tags" msgstr "Tags" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "Inhalt" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "Analysiere Bild %d/%d" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "%d/%d Chunk-Matches ausgeführt" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "Bereite Matching vor" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "%d/%d verifizierte Übereinstimmungen" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "Lese EXIF von Bild %d/%d" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "EXIF Zeitstempel" #: core\prioritize.py:70 msgid "None" msgstr "Nichts" #: core\prioritize.py:100 msgid "Ends with number" msgstr "Endet mit Zahl" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "Endet nicht mit Zahl" #: core\prioritize.py:102 msgid "Longest" msgstr "Längste" #: core\prioritize.py:103 msgid "Shortest" msgstr "Kürzeste" #: core\prioritize.py:140 msgid "Highest" msgstr "Höchste" #: core\prioritize.py:140 msgid "Lowest" msgstr "Niedrigste" #: core\prioritize.py:169 msgid "Newest" msgstr "Neuste" #: core\prioritize.py:169 msgid "Oldest" msgstr "Älterste" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) Duplikate markiert." #: core\results.py:141 msgid " filter: %s" msgstr " Filter: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "Lese Größe von %d/%d Dateien" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "Lese Metadaten von %d/%d Dateien" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "Fast fertig! Arrangiere Ergebnisse..." #: core\se\scanner.py:18 msgid "Folders" msgstr "Ordner" dupeguru-4.3.1/locale/de/LC_MESSAGES/ui.po000066400000000000000000001061751426171743600177750ustar00rootroot00000000000000# Translators: # Robert M, 2022 # Andrew Senetar , 2022 # Fuan , 2022 # msgid "" msgstr "" "Last-Translator: Fuan , 2022\n" "Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\n" "Language: de\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: qt/app.py:81 msgid "Quit" msgstr "Beenden" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "Optionen" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Ausnahme-Liste" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Bilder-Cache leeren" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "dupeGuru Hilfe" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "Über dupeGuru" #: qt/app.py:87 msgid "Open Debug Log" msgstr "Debug Log öffnen" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "" "Möchten Sie wirklich alle zwischengespeicherten Bildanalysen entfernen?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "Bilder-Cache geleert." #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} Datei (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Lösch-Optionen" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Verlinke gelöschte Dateien" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" "Doppelte Dateien werden gelöscht, an deren Stelle wird eine Verknüpfung auf " "die Referenz-Datei erstellt." #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "Hardlink" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "Symlink" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr "(nicht unterstützt)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Ohne Papierkorb löschen" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "Anstatt Dateien in den Papierkorb zu verschieben, können Sie diese direkt " "löschen. Diese Option wird in der Regel genutzt, falls die normale " "Löschmethode nicht funktioniert." #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Fortfahren" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Abbrechen" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "Attribut" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "Ausgewählt" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "Referenz" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "Ergebnis laden..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "Ergebnisfenster" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "Ordner hinzufügen..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "Datei" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "Ansicht" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "Hilfe" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "Lade letztes Suchergebnis" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "Anwendungsmodus:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "Musik" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "Bild" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "Standard" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "Scantyp:" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "Optionen" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "Zu durchsuchende Ordner auswählen und \"Suche starten\" drücken." #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "Lade Ergebnisse" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "Suche starten" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "Ungespeicherte Ergebnisse" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "" "Sie haben ungespeicherte Ergebnisse. Wollen Sie wirklich dupeGuru beenden?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "Wählen Sie einen Ordner aus, um ihn der Scanliste hinzuzufügen" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "Wählen Sie eine Ergebnisdatei zum Laden aus" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "Alle Dateien (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "dupeGuru Suchergebnisse (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "Starte einen neuen Suchlauf" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "Sie haben ungespeicherte Ergebnisse. Möchten Sie wirklich fortfahren?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "Name" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "Zustand" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "Ausgeschlossen" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "Normal" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "Auswahl löschen" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "Liste leeren" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "Schließen" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "Details" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "Folgende Tags scannen:" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "Track" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "Künstler" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "Album" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "Titel" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "Genre" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "Jahr" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "Wortgewichtung" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "Gleiche ähnliche Wörter ab" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "Dateitypen dürfen gemischt werden" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "Nutze reguläre Ausdrücke beim Filtern" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "Entferne leere Ordner beim Löschen oder Verschieben" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "Ignoriere Duplikate mit Hardlinks auf dieselbe Datei" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "Debug Modus (Neustart nötig)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "Gleiche Bilder mit unterschiedlicher Auflösung ab" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "Filter Empfindlichkeit:" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "Mehr Ergebnisse" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "Weniger Ergebnisse" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "Schriftgröße:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "Sprache:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "Kopieren und Verschieben:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "Direkt ins Ziel" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "Relativen Pfad neu erstellen" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "Absoluten Pfad neu erstellen" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "Eigener Befehl (Variablen: %d für Duplikat, %r für Referenz):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "dupeGuru muss neustarten, um die Sprachänderung durchzuführen." #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "Re-priorisiere Duplikate" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "Fügen Sie Kriterien zur rechten Box hinzu. Klicken Sie OK, um die Duplikate," " die diesen Kriterien am besten entsprechen, zur Referenzposition der " "entsprechenden Gruppe zu senden. Lesen Sie die Hilfe für mehr Informationen." #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "Probleme!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "Es gab Probleme bei der Verarbeitung einiger (aller) Dateien. Der Ursache " "dieser Probleme ist unten genauer beschrieben. Diese Dateien wurden " "\"nicht\" aus Ihren Suchergebnissen entfernt." #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "Zeige Markierte" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "Aktionen" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "Nur Duplikate anzeigen" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "Zeige Delta-Werte" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "Verschiebe Markierte in den Papierkorb..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "Verschiebe Markierte nach..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "Kopiere Markierte nach..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "Entferne Markierte aus den Ergebnissen" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "Re-priorisiere Ergebnisse..." #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "Entferne Auswahl aus den Ergebnissen" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "Füge Auswahl der Ausnahmeliste hinzu" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "Mache Auswahl zur Referenz" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "Öffne Auswahl mit Standard-Anwendung" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "Öffne den Über-Ordner der Auswahl" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "Auswahl umbenennen" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "Alles markieren" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "Nichts markieren" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "Auswahl umkehren" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "Auswahl markieren" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "Exportiere als HTML..." #: qt/result_window.py:88 msgid "Export To CSV" msgstr "Exportiere als CSV..." #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "Speichere Ergebnisse..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "Eigenen Befehl ausführen" #: qt/result_window.py:102 msgid "Mark" msgstr "Auswählen" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "Spalten" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "Auf Voreinstellung zurücksetzen" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} (Ergebnisse)" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "Nur Duplikate anzeigen" #: qt/result_window.py:194 msgid "Delta Values" msgstr "Zeige Delta-Werte" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "Datei zum Speichern der Suchergebnisse auswählen" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "Ignoriere Dateien kleiner als" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ Ergebnisse" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "Aktion" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "Neuer Ordner..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "Fortgeschritten" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "Automatisch nach Updates suchen" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "Einfach" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "Alle nach vorne bringen" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "Auf Updates prüfen..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "Fenster schließen" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "Kopieren" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "Eigener Befehl (Variablen: %d für Duplikat, %r für Referenz):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "Ausschneiden" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Delta" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "Details der ausgewählten Datei" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "Details Panel" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "Verzeichnisse" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "dupeGuru Einstellungen" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "dupeGuru Ergebnisse" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "dupeGuru Website" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "Bearbeiten" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "Exportiere als CSV..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "Exportiere als XHTML..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "Weniger Suchergebnisse" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "Filter" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "Filter Empfindlichkeit:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Filter Suchergebnisse..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "Ordner-Auswahlfenster" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "Schriftgröße:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "dupeGuru ausblenden" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "Andere ausblenden" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "Ignoriere Dateien kleiner als:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "Lade von Datei..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "Minimieren" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "Modus" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "Mehr Suchergebnisse" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "Ok" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "Einfügen" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "Einstellungen..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "Quick Look" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "dupeGuru beenden" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "Auf Voreinstellung zurücksetzen" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "Auf Voreinstellungen zurücksetzen" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "Zeige" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Zeige Auswahl im Finder" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "Alles markieren" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Verschiebe Markierte in den Papierkorb..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "Services" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "Alle einblenden" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "Starte Duplikat-Scan" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "Der Name '%@' existiert bereits." #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "Fenster" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Zoom" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "Ausschlussfilter" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "Scan-Ergebnisse" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "Verzeichnisse laden..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "Verzeichnisse speichern..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "Wählen Sie eine zu ladende Verzeichnisdatei aus" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "dupeGuru Verzeichnisse (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "" "Wählen Sie eine Datei aus, in der Ihre Verzeichnisse gespeichert werden " "sollen" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "dupeGuru Verzeichnisse (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "Addieren" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "Standardeinstellungen wiederherstellen" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "Testzeichenfolge" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "Geben Sie hier einen regulären Python-Ausdruck ein..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "Geben Sie hier einen Dateisystempfad oder Dateinamen ein..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Diese regulären Python-Ausdrücke (Groß- und Kleinschreibung beachten) filtern Dateien während des Scannens heraus.
    Der Standardstatus von Verzeichnissen wird auf der Registerkarte \"Verzeichnisse\" auf \"Ausgeschlossen\" gesetzt, wenn ihr Name zufällig mit einem der ausgewählten regulären Ausdrücke übereinstimmt.
    Für jede gesammelte Datei werden zwei Tests durchgeführt, um festzustellen, ob sie vollständig ignoriert werden soll oder nicht:
  • 1. Reguläre Ausdrücke ohne Pfadtrennzeichen werden nur mit dem Dateinamen verglichen.
  • \n" "
  • 2. Reguläre Ausdrücke mit mindestens einem Pfadtrennzeichen werden mit dem vollständigen Pfad zur Datei verglichen.

  • \n" "Beispiel: Wenn Sie PNG-Dateien nur aus dem Verzeichnis \"Meine Bilder\" herausfiltern möchten:
    .*Meine\\sBilder\\\\.*\\.png

    Sie können den regulären Ausdruck mit der Schaltfläche \"Testzeichenfolge\" testen, nachdem Sie einen falschen Pfad in das Testfeld eingefügt haben:
    C:\\\\Nutzer\\Meine Bilder\\test.png

    \n" "Übereinstimmende reguläre Ausdrücke werden hervorgehoben.
    Wenn mindestens eine Markierung vorhanden ist, wird der getestete Pfad oder Dateiname beim Scannen ignoriert.

    Verzeichnisse und Dateien, die mit einem Punkt '.' Beginnen. werden standardmäßig herausgefiltert.

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "Kompilierungsfehler:" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "Erhöhen Sie den Zoom" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "Verringern Sie den Zoom" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "Normale Größe" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "Beste Passform" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "Bild-Cache-Modus:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "Überschreiben Sie Themensymbole in der Viewer-Symbolleiste" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" "Verwenden Sie unsere eigenen internen Symbole anstelle der von der Theme " "Engine bereitgestellten" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "Bildlaufleisten in Bildbetrachtern anzeigen" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" "Wenn das angezeigte Bild nicht zum Ansichtsfenster passt, zeigen Sie " "Bildlaufleisten an, um die Ansicht zu überspannen" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "" "Standardposition für Registerkartenleiste verwenden (Neustart erforderlich)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "Platzieren Sie die Registerkartenleiste unter dem Hauptmenü und nicht daneben\n" "Unter MacOS füllt die Registerkartenleiste stattdessen die Fensterbreite aus." #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "Verwenden Sie Fettdruck als Referenz" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "Vordergrundfarbe für Referenzen:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "Hintergrundfarbe für Referenzen:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "Vordergrundfarbe für Delta:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "Zeigt die Titelleiste an und kann angedockt werden" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" "Während die Titelleiste ausgeblendet ist, ziehen Sie das schwebende Fenster " "mit der Modifikatortaste herum" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "" "Die Titelleiste kann nur deaktiviert werden, während das Fenster angedockt " "ist" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "Vertikale Titelleiste" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "Ändern Sie die Titelleiste von horizontal oben in vertikal links" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "Registerkartenleiste anzeigen" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Diese regulären Python-Ausdrücke (Groß- und Kleinschreibung beachten) filtern Dateien während des Scannens heraus.
    Der Standardstatus von Verzeichnissen wird auf der Registerkarte \"Verzeichnisse\" auf \"Ausgeschlossen\" gesetzt, wenn ihr Name zufällig mit einem der ausgewählten regulären Ausdrücke übereinstimmt.
    Für jede gesammelte Datei werden zwei Tests durchgeführt, um festzustellen, ob sie vollständig ignoriert werden soll oder nicht:
  • 1. Reguläre Ausdrücke ohne Pfadtrennzeichen werden nur mit dem Dateinamen verglichen.
  • \n" "
  • 2. Reguläre Ausdrücke mit mindestens einem Pfadtrennzeichen werden mit dem vollständigen Pfad zur Datei verglichen.

  • \n" "Beispiel: Wenn Sie PNG-Dateien nur aus dem Verzeichnis \"Meine Bilder\" herausfiltern möchten:
    .*Meine\\sBilder\\\\.*\\.png

    Sie können den regulären Ausdruck mit der Schaltfläche \"Testzeichenfolge\" testen, nachdem Sie einen falschen Pfad in das Testfeld eingefügt haben:
    C:\\\\Nutzer\\Meine Bilder\\test.png

    \n" "Übereinstimmende reguläre Ausdrücke werden hervorgehoben.
    Wenn mindestens eine Markierung vorhanden ist, wird der getestete Pfad oder Dateiname beim Scannen ignoriert.

    Verzeichnisse und Dateien, die mit einem Punkt '.' Beginnen. werden standardmäßig herausgefiltert.

    " #: qt\app.py:256 msgid "Results" msgstr "Ergebnisse" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "Allgemeine Schnittstelle" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "Ergebnistabelle" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "Detailfenster" #: qt\preferences_dialog.py:285 msgid "General" msgstr "Allgemeines" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "Anzeige" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "Dateien partiell hashen die größer sind als" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "MB" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "Benutzer System-eigene Dialoge" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" "Benutzer System-eigene Dialoge für Aktionen wie Datei/Ordern-Auswahl\n" "Manche System-eigene Dialoge sind in ihren Funktionen limitiert." #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "Ignoriere Dateien größer als" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" #: qt\app.py:299 msgid "Cache cleared." msgstr "" #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "" #: qt\about_box.py:31 msgid "About {}" msgstr "Über {}" #: qt\about_box.py:47 msgid "Version {}" msgstr "Version {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "" #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "Lizenziert unter GPLv3" #: qt\about_box.py:68 msgid "No update available." msgstr "" #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "Fehlermeldung" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "Etwas ist schief gelaufen. Wie wäre es, den Fehler zu melden?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "Fehlerberichte sollten als Github-Probleme gemeldet werden. Sie können den obigen Fehler-Traceback kopieren und in eine neue Ausgabe einfügen.\n" "\n" "Bitte stellen Sie sicher, dass Sie vorher nach bereits vorhandenen Problemen suchen. Stellen Sie außerdem sicher, dass Sie die neueste Version testen, die im Repository verfügbar ist, da der aufgetretene Fehler möglicherweise bereits behoben wurde.\n" "\n" "Was normalerweise wirklich hilft, ist, wenn Sie eine Beschreibung hinzufügen, wie Sie den Fehler erhalten haben. Vielen Dank!\n" "\n" "Obwohl die Anwendung nach diesem Fehler weiterhin ausgeführt werden sollte, befindet sie sich möglicherweise in einem instabilen Zustand. Es wird daher empfohlen, die Anwendung neu zu starten." #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Geh zu Github" #: qt\preferences.py:24 msgid "Czech" msgstr "Tschechisch" #: qt\preferences.py:25 msgid "German" msgstr "Deutsch" #: qt\preferences.py:26 msgid "Greek" msgstr "Griechisch" #: qt\preferences.py:27 msgid "English" msgstr "Englisch" #: qt\preferences.py:28 msgid "Spanish" msgstr "Spanisch" #: qt\preferences.py:29 msgid "French" msgstr "Französisch" #: qt\preferences.py:30 msgid "Armenian" msgstr "Armenisch" #: qt\preferences.py:31 msgid "Italian" msgstr "Italienisch" #: qt\preferences.py:32 msgid "Japanese" msgstr "Japanisch" #: qt\preferences.py:33 msgid "Korean" msgstr "Koreanisch" #: qt\preferences.py:34 msgid "Malay" msgstr "Malaiisch" #: qt\preferences.py:35 msgid "Dutch" msgstr "Niederländisch" #: qt\preferences.py:36 msgid "Polish" msgstr "Polnisch" #: qt\preferences.py:37 msgid "Brazilian" msgstr "Brasilianisch" #: qt\preferences.py:38 msgid "Russian" msgstr "Russisch" #: qt\preferences.py:39 msgid "Turkish" msgstr "Türkisch" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "Ukrainisch" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "Vietnamesisch" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "Chinesisch (Vereinfachtes)" #: qt\recent.py:54 msgid "Clear List" msgstr "Liste löschen" #: qt\search_edit.py:78 msgid "Search..." msgstr "Suche..." dupeguru-4.3.1/locale/el/000077500000000000000000000000001426171743600152315ustar00rootroot00000000000000dupeguru-4.3.1/locale/el/LC_MESSAGES/000077500000000000000000000000001426171743600170165ustar00rootroot00000000000000dupeguru-4.3.1/locale/el/LC_MESSAGES/columns.po000066400000000000000000000060111426171743600210340ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # msgid "" msgstr "" "Last-Translator: Fuan , 2021\n" "Language-Team: Greek (https://www.transifex.com/voltaicideas/teams/116153/el/)\n" "Language: el\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "Διαδρομή αρχείου" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "Μήνυμα σφάλματος" #: core\me\prioritize.py:23 msgid "Duration" msgstr "Διάρκεια" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "Bitrate" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "Ρυθμός δειγματοληψίας" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "Όνομα αρχείου" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "Φάκελος" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "Μέγεθος (MB)" #: core\me\result_table.py:22 msgid "Time" msgstr "Χρόνος" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "Ρυθμός δειγματοληψίας" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "Τύπος" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "Τροποποίηση" #: core\me\result_table.py:27 msgid "Title" msgstr "Τίτλος" #: core\me\result_table.py:28 msgid "Artist" msgstr "Καλλιτέχνης" #: core\me\result_table.py:29 msgid "Album" msgstr "Αλμπουμ" #: core\me\result_table.py:30 msgid "Genre" msgstr "Είδος" #: core\me\result_table.py:31 msgid "Year" msgstr "Έτος" #: core\me\result_table.py:32 msgid "Track Number" msgstr "Αριθμός κομματιού" #: core\me\result_table.py:33 msgid "Comment" msgstr "Σχόλιο" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "Ταύτιση %" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "Χρησιμοποιημένες λέξεις" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "Αριθμός διπλοτύπων" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "Διαστάσεις" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "Μέγεθος (KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "Χρονοσήμανση EXIF" #: core\prioritize.py:156 msgid "Size" msgstr "Μέγεθος-Διαστάσεις?" dupeguru-4.3.1/locale/el/LC_MESSAGES/core.po000066400000000000000000000204121426171743600203050ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # msgid "" msgstr "" "Last-Translator: Fuan , 2021\n" "Language-Team: Greek (https://www.transifex.com/voltaicideas/teams/116153/el/)\n" "Language: el\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "Δεν υπάρχουν μαρκαρισμένα διπλότυπα. Δεν έγινε τίποτα." #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "Δεν υπάρχουν επιλεγμένα διπλότυπα. Δεν έγινε τίποτα." #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" "Πρόκειται να ανοίξετε πολλά αρχεία ταυτόχρονα. Ανάλογα με το ποιο πρόγραμμα " "ανοίγουν αυτάτα αρχεία, κάτι τέτοιο μπορεί να προκαλέσει ένα μικρό χάος. " "Συνέχεια;" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "Σάρωση για διπλότυπα" #: core\app.py:72 msgid "Loading" msgstr "Φόρτωση" #: core\app.py:73 msgid "Moving" msgstr "Μετακίνηση" #: core\app.py:74 msgid "Copying" msgstr "Αντιγραφή" #: core\app.py:75 msgid "Sending to Trash" msgstr "Αποστολή στα σκουπίδια" #: core\app.py:289 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" "Μια προηγούμενη ενέργεια είναι σε εξέλιξη. Δεν μπορείτε να ξεκινήσετε " "καινούργια ακόμα. Περιμένετε λίγα δευτερόλεπτα, έπειτα προσπαθήστε ξανά." #: core\app.py:300 msgid "No duplicates found." msgstr "Δεν βρέθηκαν διπλότυπα." #: core\app.py:315 msgid "All marked files were copied successfully." msgstr "Όλα τα επιλεγμένα αρχεία αντιγράφηκαν επιτυχώς." #: core\app.py:317 msgid "All marked files were moved successfully." msgstr "Όλα τα επιλεγμένα αρχεία μετακινήθηκαν επιτυχώς." #: core\app.py:319 msgid "All marked files were deleted successfully." msgstr "" #: core\app.py:321 msgid "All marked files were successfully sent to Trash." msgstr "Όλα τα επιλεγμένα αρχεία στάλθηκαν με επιτυχία στον κάδο." #: core\app.py:326 msgid "Could not load file: {}" msgstr "Δεν ήταν δυνατή η φόρτωση του αρχείου: {}" #: core\app.py:382 msgid "'{}' already is in the list." msgstr "'{}' υπάρχει ήδη στη λίστα." #: core\app.py:384 msgid "'{}' does not exist." msgstr "'{}' δεν υπάρχει." #: core\app.py:392 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "" "Όλα τα επιλεγμένα %d στοιχεία θα αγνοηθούν σε μελλοντικές σαρώσεις.Συνέχεια;" #: core\app.py:469 msgid "Select a directory to copy marked files to" msgstr "Επιλέξτε έναν κατάλογο για να αντιγράψετε επισημασμένα αρχεία." #: core\app.py:471 msgid "Select a directory to move marked files to" msgstr "Επιλέξτε έναν κατάλογο για να μετακινήσετε τα επισημασμένα αρχεία." #: core\app.py:510 msgid "Select a destination for your exported CSV" msgstr "Επιλέξτε έναν προορισμό για το εξαγόμενο CSV σας" #: core\app.py:516 core\app.py:771 core\app.py:781 msgid "Couldn't write to file: {}" msgstr "Δεν ήταν δυνατή η εγγραφή στο αρχείο: {}" #: core\app.py:539 msgid "You have no custom command set up. Set it up in your preferences." msgstr "Δεν έχετε ορίσει ειδική εντολή. Ρυθμίστε τη στις προτιμήσεις σας. " #: core\app.py:695 core\app.py:707 msgid "You are about to remove %d files from results. Continue?" msgstr "Πρόκειται να αφαιρέσετε %d αρχεία από τα αποτελέσματα. Συνέχεια;" #: core\app.py:743 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} ομάδες διπλοτύπων άλλαξαν από το επαναπροσδιορισμό." #: core\app.py:790 msgid "The selected directories contain no scannable file." msgstr "Οι επιλεγμένοι φάκελοι δεν περιέχουν σαρώσιμα αρχεία." #: core\app.py:803 msgid "Collecting files to scan" msgstr "Συλλογή αρχείων για σάρωση" #: core\app.py:850 msgid "%s (%d discarded)" msgstr "%s (%d απορρίφθηκαν)" #: core\directories.py:191 msgid "Collected {} files to scan" msgstr "" #: core\directories.py:207 msgid "Collected {} folders to scan" msgstr "" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "Στέλνετε {} αρχεία στα σκουπίδια." #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "Κανονικές εκφράσεις" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "Θέλετε να αφαιρέσετε όλα τα %d στοιχεία από τη λίστα αγνόησης; " #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "Ονομα αρχείου" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "Όνομα αρχείου - Πεδία" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "Όνομα αρχείου - Πεδία (Χωρίς παραγγελία)" #: core\me\scanner.py:23 msgid "Tags" msgstr "ετικέτα" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "Περιεχόμενα" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "Ανάλυση %d/%d εικόνων" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "Εκτέλεση %d/%d μερικής ταυτοποίησης" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "Προετοιμασία για σύγκριση" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "Πιστοποίηση %d/%d ταυτόσημων" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "Ανάγνωση EXIF %d/%d εικόνες" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "Χρονική σήμανση EXIF" #: core\prioritize.py:70 msgid "None" msgstr "Καμμία" #: core\prioritize.py:100 msgid "Ends with number" msgstr "Λήγει με αριθμό" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "Δεν λήγει με αριθμό" #: core\prioritize.py:102 msgid "Longest" msgstr "Μεγαλύτερο" #: core\prioritize.py:103 msgid "Shortest" msgstr "Μικρότερο" #: core\prioritize.py:140 msgid "Highest" msgstr "Υψηλότερη" #: core\prioritize.py:140 msgid "Lowest" msgstr "Χαμηλότερη" #: core\prioritize.py:169 msgid "Newest" msgstr "Νεώτερο" #: core\prioritize.py:169 msgid "Oldest" msgstr "Παλαιότερο" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) επιλεγμένα διπλότυπα." #: core\results.py:141 msgid " filter: %s" msgstr " φίλτρο: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "Ανάγνωση μεγέθους %d/%d αρχείων" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "Ανάγνωση μεταδεδομένων των %d/%d αρχείων" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "Σχεδόν τελείωσα! Παιχνίδι με αποτελέσματα ..." #: core\se\scanner.py:18 msgid "Folders" msgstr "ντοσιέ" dupeguru-4.3.1/locale/el/LC_MESSAGES/ui.po000066400000000000000000001260621426171743600200020ustar00rootroot00000000000000# Translators: # Fuan , 2022 # Andrew Senetar , 2022 # msgid "" msgstr "" "Last-Translator: Andrew Senetar , 2022\n" "Language-Team: Greek (https://www.transifex.com/voltaicideas/teams/116153/el/)\n" "Language: el\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: qt/app.py:81 msgid "Quit" msgstr "Έξοδος" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "Επιλογές" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Λίστα αγνόησης" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Εκκαθάριση μνήμης cache εικόνων" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Βοήθεια για το dupeGuru" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "Σχετικά με το dupeGuru" #: qt/app.py:87 msgid "Open Debug Log" msgstr "Άνοιγμα αρχείου αποσφαλμάτωσης" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "" "Θέλετε πραγματικά να αφαιρέσετε όλη την αποθηκευμένη ανάλυση εικόνων σας;" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "Μνήμη cache εικόνων εκκαθαρίστηκε." #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} αρχείο (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Επιλογές διαγραφής" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Σύνδεση διαγεγραμμένων αρχείων" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" "Μετά την αντιγραφή ενός διπλοτύπου, τοποθετήστε ένα σύνδεσμο που στοχεύει το" " αρχείοαναφοράς για να αντικαταστήσετε το διαγεγραμμένο αρχείο." #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "Σκληρός σύνδεσμος" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "συμβολικός σύνδεσμος" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr " (δεν υποστηρίζεται)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Απευθείας διαγραφή αρχείων" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "Αντί για την αποστολή αρχείων στα σκουπίδια, να διαγραφούν άμεσα. Αυτή η " "επιλογή είναι Συνήθως μια λύση, όταν η κανονική μέθοδο διαγραφής δεν " "λειτουργεί." #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Προχωρήστε" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Ακύρωση" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "Χαρακτηριστικό" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "Επιλεγμένα" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "Αναφορά" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "Φόρτωση αποτελεσμάτων..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "Παράθυρο αποτελεσμάτων" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "Προσθήκη φακέλου..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "Αρχείο" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "Προβολή" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "Βοήθεια" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "Φόρτωση πρόσφατων αποτελεσμάτων" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "Λειτουργία εφαρμογής:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "μουσική" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "Εικόνα" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "πρότυπο" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "Είδος σάρωσης" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "Περισσότερες επιλογές" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "Επιλέξτε τους φακέλους για να σαρώσετε και πατήστε το πλήκτρο \"Σάρωση\"." #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "Φόρτωση αποτελεσμάτων" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "Σάρωση" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "Μη αποθηκευμένα αποτελέσματα" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "" "Έχετε μη αποθηκευμένα αποτελέσματα, θέλετε πραγματικά να κλείσετε το " "πρόγραμμα;" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "Επιλέξτε ένα φάκελο για να προσθέσετε στη λίστα σάρωσης" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "Επιλέξτε αρχείο αποτελεσμάτων προς φόρτωση" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "Όλα τα αρχεία (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "Αποτελέσματα dupeGuru (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "Έναρξη νέας σάρωσης" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "Έχετε μη αποθηκευμένα αποτελέσματα, θέλετε πραγματικά να συνεχίσετε;" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "Όνομα" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "Κατάσταση" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "Εξαιρούμενο" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "Κανονικό" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "Αφαίρεση επιλεγμένων" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "Αφαίρεση" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "Κλείσιμο" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "Λεπτομέρειες" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "Ετικέτες προς σάρωση:" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "Κομμάτι" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "Καλλιτέχνης" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "Άλμπουμ" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "Τίτλος" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "Είδος" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "Έτος" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "Στάθμιση λέξης" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "Ταίριασμα παρόμοιων λέξεων" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "Να περιλαμβάνουν διαφορετικούς τύπους αρχείων" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "Χρήση κανονικών εκφράσεων κατά το φιλτράρισμα" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "Κατάργηση κενών φακέλων για διαγραφή ή μετακίνηση" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "Αγνόηση διπλοτύπων με hardlinking στο ίδιο αρχείο" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "Λειτουργία αποσφαλμάτωσης(απαιτείται επανεκκίνηση)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "Ταίριασμα εικόνων διαφορετικών διαστάσεων" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "Σκληρότητα φίλτρου" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "Περισσότερα αποτελέσματα" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "Λιγότερα αποτελέσματα" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "Μέγεθος γραμματοσειράς:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "Γλώσσα:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "Αντιγραφή και μετακίνηση:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "Απευθείας στον προορισμό" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "Ανασύνθεση σχετικής διαδρομής" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "Ανασύνθεση απόλυτης διαδρομής" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "" "Προσωποποιημένη εντολή (παράμετροι: %d για διπλότυπο, %r για αναφορά):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "" "Πρέπει να γίνει επανεκκίνηση του dupeGuru για να ισχύσουν οι αλλαγές " "γλώσσας." #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "Επαναπροσδιορισμός προτεραιότητας διπλοτύπων" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "Προσθέστε κριτήρια στο πλαίσιο δεξιά και κάντε κλικ στο κουμπί OK για να " "στείλετε τα διπλότυπα που αντιστοιχούν καλύτερα σε αυτά τα κριτήρια, στην " "αντίστοιχη θέση αναφοράς των ομάδων τους. Διαβάστε το αρχείο βοήθειας για " "περισσότερες πληροφορίες." #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "Προβλήματα!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "Υπήρχαν προβλήματα επεξεργασίας για κάποιο (ή όλα) από τα αρχεία. Η αιτία " "τωνΠροβλημάτων περιγράφεται στον παρακάτω πίνακα. Τα εν λόγω αρχεία δεν " "Αφαιρέθηκαν από τα αποτελέσματα σας." #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "Αποκάλυψη επιλεγμένων" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "Ενέργειες" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "Προβολή μόνο διπλοτύπων" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "Προβολή τιμών διαφοράς (Delta)" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "Αποστολή των μαρκαρισμένων στον Κάδο Ανακύκλωσης..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "Μετακίνηση μαρκαρισμένων σε..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "Αντιγραφή μαρκαρισμένων σε..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "Αφαίρεση μαρκαρισμένων από τα αποτελέσματα" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "Επαναπροσδιορισμός προτεραιότητας αποτελεσμάτων..." #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "Αφαίρεση επιλεγμένων από τα αποτελέσματα" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "Προσθήκη επιλεγμένων στη λίστα αγνόησης" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "Μετατροπή επιλεγμένων σε αναφορά" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "Άνοιγμα επιλεγμένου/ων με Προεπιλεγμένη εφαρμογή" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "Άνοιγμα φακέλου που περιέχει το/τα επιλεγμένο/α" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "Μετονομασία επιλεγμένου/ων" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "Μαρκάρισμα όλων" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "Ξεμαρκάρισμα όλων" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "Αντιστροφή μαρκαρίσματος" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "Μαρκάρισμα επιλεγμένου/ων" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "Εξαγωγή σε HTML" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "Εξαγωγή σε CSV" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "Αποθήκευση αποτελεσμάτων..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "Εκτέλεση προσαρμοσμένης εντολής" #: qt/result_window.py:102 msgid "Mark" msgstr "Μαρκάρισμα" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "Στήλες" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "Επαναφορά προεπιλογών" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} Αποτελέσματα" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "Μόνο διπλότυπα" #: qt/result_window.py:194 msgid "Delta Values" msgstr "Τιμές διαφοράς (Delta)" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "Επιλέξτε ένα αρχείο για να αποθηκεύσετε τα αποτελέσματα σας" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "Αγνόηση αρχείων μικρότερων από" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ Αποτελέσματα" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "Ενέργεια" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "Προσθήκη νέου φακέλου..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "Προχωρημένες" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "Αυτόματος έλεγχος για ενημερώσεις" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "Βασική" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "Μεταφορά όλων στο προσκήνιο" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "Έλεγχος για αναβάθμιση..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "Κλείσιμο παραθύρου" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "Αντιγραφή" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "" "Προσωποποιημένη εντολή (παράμετροι: %d για διπλότυπο, %r για αναφορά):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "Αποκοπή" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Delta" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "Λεπτομέρειες επιλεγμένου αρχείου" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "Πίνακας λεπτομερειών" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "Φάκελοι" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "Προτιμήσεις dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "Αποτελέσματα dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "Ιστότοπος dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "Επεξεργασία" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "Εξαγωγή αποτελεσμάτων σε CSV" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "Εξαγωγή αποτελεσμάτων σε XHTML" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "Λιγότερα αποτελέσματα" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "Φίλτρο" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "Σκληρότητα φίλτρου:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Φίλτρο αποτελεσμάτων..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "Παράθυρο επιλογής φακέλου" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "Μέγεθος γραμματοσειράς:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "Απόκρυψη dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "Απόκρυψη υπολοίπων" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "Αγνόηση αρχείων μικρότερων από:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "Φόρτωση από αρχείο..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "Ελαχιστοποίηση" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "Λειτουργία" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "Περισσότερα αποτελέσματα" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "Ok" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "Επικόλληση" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "Προτιμήσεις..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "Γρήγηορη προβολή" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "Κλείσιμο dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "Επαναφορά προεπιλογής" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "Επαναφορά προεπιλογών" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "Εμφάνιση" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Εμφάνιση επιλεγμένων στην Εύρεση" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "Επιλογή όλων" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Αποστολή μαρκαρισμένων στα Σκουπίδια..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "Υπηρεσίες" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "Εμφάνιση όλων" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "Έναρξη σάρωσης διπλοτύπων" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "Το όνομα'%@' υπάρχει ήδη." #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "Παράθυρο" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Ζουμ" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "Φίλτρα αποκλεισμού" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "Αποτελέσματα σάρωσης" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "Φόρτωση καταλόγων..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "Αποθήκευση καταλόγων..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "Επιλέξτε ένα αρχείο καταλόγων για φόρτωση" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "καταλόγους του dupeguru (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "Επιλέξτε ένα αρχείο για να αποθηκεύσετε τους καταλόγους σας" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "καταλόγους του dupeguru (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "Προσθήκη" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "Επαναφέρετε τις προεπιλογές" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "Σειρά δοκιμής" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "Πληκτρολογήστε μια τυπική έκφραση python εδώ..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "Πληκτρολογήστε μια διαδρομή συστήματος ή ένα όνομα αρχείου εδώ..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Αυτές οι τυπικές εκφράσεις python (πεζών-κεφαλαίων) θα φιλτράρουν τα αρχεία κατά τη διάρκεια των σαρώσεων.
    Οι διευθυντές θα έχουν επίσης την προεπιλεγμένη κατάστασή τους σε Εξαίρεση στην καρτέλα Κατάλογοι εάν το όνομά τους συμβαίνει να ταιριάζει με μία από τις επιλεγμένες κανονικές εκφράσεις.
    Για κάθε αρχείο που συλλέγεται, εκτελούνται δύο δοκιμές για να προσδιοριστεί εάν θα το αγνοηθεί πλήρως ή όχι:
  • 1. Οι κανονικές εκφράσεις χωρίς διαχωριστικό διαδρομών σε αυτές θα συγκρίνονται μόνο με το όνομα αρχείου.
  • \n" "
  • 2. Οι κανονικές εκφράσεις με τουλάχιστον ένα διαχωριστικό διαδρομών σε αυτές θα συγκριθούν με την πλήρη διαδρομή προς το αρχείο.

  • \n" "Παράδειγμα: εάν θέλετε να φιλτράρετε αρχεία .PNG μόνο από τον κατάλογο \"Οι εικόνες μου\":
    .*Οι\\sεικόνες\\sμου\\\\.*\\.png

    Μπορείτε να δοκιμάσετε την κανονική έκφραση με το κουμπί \"δοκιμαστική συμβολοσειρά\" αφού επικολλήσετε μια ψεύτικη διαδρομή στο πεδίο δοκιμής:
    C:\\\\χρήστης\\Οι εικόνες μου\\test.png

    \n" "Θα επισημανθεί η αντιστοίχιση των τυπικών εκφράσεων.
    Εάν υπάρχει τουλάχιστον μία επισήμανση, η διαδρομή ή το όνομα αρχείου που δοκιμάστηκε θα αγνοηθεί κατά τη διάρκεια των σαρώσεων.

    Κατάλογοι και αρχεία που ξεκινούν με τελεία \".\" φιλτράρονται από προεπιλογή.

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "Σφάλμα συλλογής:" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "Αυξήστε το ζουμ" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "Μείωση ζουμ" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "Κανονικό μέγεθος" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "ταιριάζει καλύτερα" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "Λειτουργία προσωρινής μνήμης εικόνας:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "" "Παράκαμψη εικονιδίων θέματος στη γραμμή εργαλείων του προγράμματος προβολής" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" "Χρησιμοποιήστε τα δικά μας εσωτερικά εικονίδια αντί αυτών που παρέχονται από" " τη μηχανή θέματος" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "Εμφάνιση γραμμών κύλισης σε προγράμματα προβολής εικόνων" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" "Όταν η εικόνα που εμφανίζεται δεν ταιριάζει στη θύρα προβολής, εμφανίστε τις" " γραμμές κύλισης για να εκτείνετε την προβολή γύρω" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "" "Χρησιμοποιήστε την προεπιλεγμένη θέση για τη γραμμή καρτελών (απαιτείται " "επανεκκίνηση)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "Τοποθετήστε τη γραμμή καρτελών κάτω από το κύριο μενού και όχι δίπλα σε αυτό\n" "Σε MacOS, η γραμμή καρτελών θα γεμίσει το πλάτος του παραθύρου." #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "Χρησιμοποιήστε έντονη γραμματοσειρά για αναφορές" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "Χρώμα προσκηνίου αναφοράς:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "Χρώμα προσκηνίου αναφοράς:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "Χρώμα προσκηνίου δέλτα:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "Εμφάνιση της γραμμής τίτλου και δυνατότητα σύνδεσης" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" "Ενώ η γραμμή τίτλου είναι κρυφή, χρησιμοποιήστε το πλήκτρο τροποποίησης για " "να σύρετε το κυμαινόμενο παράθυρο γύρω" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "" "Η γραμμή τίτλου μπορεί να απενεργοποιηθεί μόνο όταν το παράθυρο είναι " "συνδεδεμένο" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "Κάθετη γραμμή τίτλου" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "" "Αλλάξτε τη γραμμή τίτλου από οριζόντια στην κορυφή, σε κατακόρυφη στην " "αριστερή πλευρά" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "Εμφάνιση γραμμής καρτελών" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Αυτές οι τυπικές εκφράσεις python (πεζών-κεφαλαίων) θα φιλτράρουν τα αρχεία κατά τη διάρκεια των σαρώσεων.
    Οι διευθυντές θα έχουν επίσης την προεπιλεγμένη κατάστασή τους σε Εξαίρεση στην καρτέλα Κατάλογοι εάν το όνομά τους συμβαίνει να ταιριάζει με μία από τις επιλεγμένες κανονικές εκφράσεις.
    Για κάθε αρχείο που συλλέγεται, εκτελούνται δύο δοκιμές για να προσδιοριστεί εάν θα το αγνοηθεί πλήρως ή όχι:
  • 1. Οι κανονικές εκφράσεις χωρίς διαχωριστικό διαδρομών σε αυτές θα συγκρίνονται μόνο με το όνομα αρχείου.
  • \n" "
  • 2. Οι κανονικές εκφράσεις με τουλάχιστον ένα διαχωριστικό διαδρομών σε αυτές θα συγκριθούν με την πλήρη διαδρομή προς το αρχείο.

  • \n" "Παράδειγμα: εάν θέλετε να φιλτράρετε αρχεία .PNG μόνο από τον κατάλογο \"Οι εικόνες μου\":
    .*Οι\\sεικόνες\\sμου\\\\.*\\.png

    Μπορείτε να δοκιμάσετε την κανονική έκφραση με το κουμπί \"δοκιμαστική συμβολοσειρά\" αφού επικολλήσετε μια ψεύτικη διαδρομή στο πεδίο δοκιμής:
    C:\\\\χρήστης\\Οι εικόνες μου\\test.png

    \n" "Θα επισημανθεί η αντιστοίχιση των τυπικών εκφράσεων.
    Εάν υπάρχει τουλάχιστον μία επισήμανση, η διαδρομή ή το όνομα αρχείου που δοκιμάστηκε θα αγνοηθεί κατά τη διάρκεια των σαρώσεων.

    Κατάλογοι και αρχεία που ξεκινούν με τελεία \".\" φιλτράρονται από προεπιλογή.

    " #: qt\app.py:256 msgid "Results" msgstr "Αποτελέσματα" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "Γενική διεπαφή" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "Πίνακας αποτελεσμάτων" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "Παράθυρο λεπτομερειών" #: qt\preferences_dialog.py:285 msgid "General" msgstr "Γενικός" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "Απεικόνιση" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" #: qt\app.py:299 msgid "Cache cleared." msgstr "" #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "" #: qt\about_box.py:31 msgid "About {}" msgstr "Σχετικά {}" #: qt\about_box.py:47 msgid "Version {}" msgstr "Έκδοση {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "" #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "Άδεια χρήσης βάσει GPLv3" #: qt\about_box.py:68 msgid "No update available." msgstr "" #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "Αναφορά σφάλματος" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "Κάτι πήγε στραβά. Μήπως να αναφερθεί το σφάλμα;" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "Οι αναφορές σφαλμάτων πρέπει να αναφέρονται ως ζητήματα Github. Μπορείτε να αντιγράψετε την ανίχνευση σφαλμάτων παραπάνω και να την επικολλήσετε σε ένα νέο ζήτημα.\n" "\n" "Βεβαιωθείτε ότι έχετε πραγματοποιήσει αναζήτηση για τυχόν υπάρχοντα ζητήματα εκ των προτέρων. Επίσης, φροντίστε να δοκιμάσετε την πιο πρόσφατη διαθέσιμη έκδοση από το αποθετήριο, καθώς το σφάλμα που αντιμετωπίζετε ενδέχεται να έχει ήδη διορθωθεί.\n" "\n" "Αυτό που συνήθως βοηθάει συνήθως είναι εάν προσθέσετε μια περιγραφή για το πώς λάβατε το σφάλμα. Ευχαριστώ!\n" "\n" "Παρόλο που η εφαρμογή θα πρέπει να συνεχίσει να εκτελείται μετά από αυτό το σφάλμα, ενδέχεται να βρίσκεται σε ασταθή κατάσταση, επομένως συνιστάται να κάνετε επανεκκίνηση της εφαρμογής." #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Επίσκεψη Github" #: qt\preferences.py:24 msgid "Czech" msgstr "Τσέχικα" #: qt\preferences.py:25 msgid "German" msgstr "Γερμανικά" #: qt\preferences.py:26 msgid "Greek" msgstr "Ελληνικά" #: qt\preferences.py:27 msgid "English" msgstr "Αγγλικά" #: qt\preferences.py:28 msgid "Spanish" msgstr "Ισπανικά" #: qt\preferences.py:29 msgid "French" msgstr "Γαλλικά" #: qt\preferences.py:30 msgid "Armenian" msgstr "Αρμένικα" #: qt\preferences.py:31 msgid "Italian" msgstr "Ιταλικά" #: qt\preferences.py:32 msgid "Japanese" msgstr "Ιαπωνικά" #: qt\preferences.py:33 msgid "Korean" msgstr "Κορεάτικα" #: qt\preferences.py:34 msgid "Malay" msgstr "Μαλαϊκά" #: qt\preferences.py:35 msgid "Dutch" msgstr "Γερμανικά" #: qt\preferences.py:36 msgid "Polish" msgstr "Πολωνικά" #: qt\preferences.py:37 msgid "Brazilian" msgstr "Βραζιλιάνικα" #: qt\preferences.py:38 msgid "Russian" msgstr "Ρώσικα" #: qt\preferences.py:39 msgid "Turkish" msgstr "Τουρκικά" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "Ουκρανέζικα" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "Βιετναμέζικα" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "Κινέζικα (Απλοποιημένα)" #: qt\recent.py:54 msgid "Clear List" msgstr "Εκκαθάριση λίστας" #: qt\search_edit.py:78 msgid "Search..." msgstr "Αναζήτηση..." dupeguru-4.3.1/locale/en/000077500000000000000000000000001426171743600152335ustar00rootroot00000000000000dupeguru-4.3.1/locale/en/LC_MESSAGES/000077500000000000000000000000001426171743600170205ustar00rootroot00000000000000dupeguru-4.3.1/locale/en/LC_MESSAGES/columns.po000066400000000000000000000045641426171743600210510ustar00rootroot00000000000000# msgid "" msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: utf-8\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "File Path" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "Error Message" #: core\me\prioritize.py:23 msgid "Duration" msgstr "Duration" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "Bitrate" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "Samplerate" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "Filename" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "Folder" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "Size (MB)" #: core\me\result_table.py:22 msgid "Time" msgstr "Time" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "Sample Rate" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "Kind" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "Modification" #: core\me\result_table.py:27 msgid "Title" msgstr "Title" #: core\me\result_table.py:28 msgid "Artist" msgstr "Artist" #: core\me\result_table.py:29 msgid "Album" msgstr "Album" #: core\me\result_table.py:30 msgid "Genre" msgstr "Genre" #: core\me\result_table.py:31 msgid "Year" msgstr "Year" #: core\me\result_table.py:32 msgid "Track Number" msgstr "Track Number" #: core\me\result_table.py:33 msgid "Comment" msgstr "Comment" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "Match %" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "Words Used" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "Dupe Count" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "Dimensions" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "Size (KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "EXIF Timestamp" #: core\prioritize.py:156 msgid "Size" msgstr "Size" dupeguru-4.3.1/locale/en/LC_MESSAGES/core.po000066400000000000000000000141041426171743600203100ustar00rootroot00000000000000# msgid "" msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: utf-8\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "There are no marked duplicates. Nothing has been done." #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "There are no selected duplicates. Nothing has been done." #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "Scanning for duplicates" #: core\app.py:72 msgid "Loading" msgstr "Loading" #: core\app.py:73 msgid "Moving" msgstr "Moving" #: core\app.py:74 msgid "Copying" msgstr "Copying" #: core\app.py:75 msgid "Sending to Trash" msgstr "Sending to Trash" #: core\app.py:308 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." #: core\app.py:318 msgid "No duplicates found." msgstr "No duplicates found." #: core\app.py:333 msgid "All marked files were copied successfully." msgstr "All marked files were copied successfully." #: core\app.py:334 msgid "All marked files were moved successfully." msgstr "All marked files were moved successfully." #: core\app.py:335 msgid "All marked files were successfully sent to Trash." msgstr "All marked files were successfully sent to Trash." #: core\app.py:343 msgid "Could not load file: {}" msgstr "Could not load file: {}" #: core\app.py:399 msgid "'{}' already is in the list." msgstr "'{}' already is in the list." #: core\app.py:401 msgid "'{}' does not exist." msgstr "'{}' does not exist." #: core\app.py:410 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" #: core\app.py:486 msgid "Select a directory to copy marked files to" msgstr "Select a directory to copy marked files to" #: core\app.py:487 msgid "Select a directory to move marked files to" msgstr "Select a directory to move marked files to" #: core\app.py:527 msgid "Select a destination for your exported CSV" msgstr "Select a destination for your exported CSV" #: core\app.py:534 core\app.py:801 core\app.py:811 msgid "Couldn't write to file: {}" msgstr "Couldn't write to file: {}" #: core\app.py:559 msgid "You have no custom command set up. Set it up in your preferences." msgstr "You have no custom command set up. Set it up in your preferences." #: core\app.py:727 core\app.py:740 msgid "You are about to remove %d files from results. Continue?" msgstr "You are about to remove %d files from results. Continue?" #: core\app.py:774 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} duplicate groups were changed by the re-prioritization." #: core\app.py:821 msgid "The selected directories contain no scannable file." msgstr "The selected directories contain no scannable file." #: core\app.py:835 msgid "Collecting files to scan" msgstr "Collecting files to scan" #: core\app.py:891 msgid "%s (%d discarded)" msgstr "%s (%d discarded)" #: core\engine.py:244 core\engine.py:288 msgid "0 matches found" msgstr "0 matches found" #: core\engine.py:262 core\engine.py:296 msgid "%d matches found" msgstr "%d matches found" #: core\gui\deletion_options.py:73 msgid "You are sending {} file(s) to the Trash." msgstr "You are sending {} file(s) to the Trash." #: core\gui\exclude_list_table.py:15 msgid "Regular Expressions" msgstr "Regular Expressions" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "Do you really want to remove all %d items from the ignore list?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "Filename" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "Filename - Fields" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "Filename - Fields (No Order)" #: core\me\scanner.py:23 msgid "Tags" msgstr "Tags" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "Contents" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "Analyzed %d/%d pictures" #: core\pe\matchblock.py:181 msgid "Performed %d/%d chunk matches" msgstr "Performed %d/%d chunk matches" #: core\pe\matchblock.py:191 msgid "Preparing for matching" msgstr "Preparing for matching" #: core\pe\matchblock.py:244 msgid "Verified %d/%d matches" msgstr "Verified %d/%d matches" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "Read EXIF of %d/%d pictures" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "EXIF Timestamp" #: core\prioritize.py:70 msgid "None" msgstr "None" #: core\prioritize.py:100 msgid "Ends with number" msgstr "Ends with number" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "Doesn't end with number" #: core\prioritize.py:102 msgid "Longest" msgstr "Longest" #: core\prioritize.py:103 msgid "Shortest" msgstr "Shortest" #: core\prioritize.py:140 msgid "Highest" msgstr "Highest" #: core\prioritize.py:140 msgid "Lowest" msgstr "Lowest" #: core\prioritize.py:169 msgid "Newest" msgstr "Newest" #: core\prioritize.py:169 msgid "Oldest" msgstr "Oldest" #: core\results.py:142 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) duplicates marked." #: core\results.py:149 msgid " filter: %s" msgstr " filter: %s" #: core\scanner.py:85 msgid "Read size of %d/%d files" msgstr "Read size of %d/%d files" #: core\scanner.py:109 msgid "Read metadata of %d/%d files" msgstr "Read metadata of %d/%d files" #: core\scanner.py:147 msgid "Almost done! Fiddling with results..." msgstr "Almost done! Fiddling with results..." #: core\se\scanner.py:18 msgid "Folders" msgstr "Folders" dupeguru-4.3.1/locale/en/LC_MESSAGES/ui.po000066400000000000000000000706431426171743600200070ustar00rootroot00000000000000# msgid "" msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: utf-8\n" #: qt/app.py:81 msgid "Quit" msgstr "Quit" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "Options" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Ignore List" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Clear Picture Cache" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "dupeGuru Help" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "About dupeGuru" #: qt/app.py:87 msgid "Open Debug Log" msgstr "Open Debug Log" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "Do you really want to remove all your cached picture analysis?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "Picture cache cleared." #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} file (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Deletion Options" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Link deleted files" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "Hardlink" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "Symlink" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr " (unsupported)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Directly delete files" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Proceed" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Cancel" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "Attribute" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "Selected" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "Reference" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "Load Results..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "Results Window" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "Add Folder..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "File" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "View" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "Help" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "Load Recent Results" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "Application Mode:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "Music" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "Picture" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "Standard" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "Scan Type:" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "More Options" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "Select folders to scan and press \"Scan\"." #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "Load Results" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "Scan" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "Unsaved results" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "You have unsaved results, do you really want to quit?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "Select a folder to add to the scanning list" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "Select a results file to load" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "All Files (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "dupeGuru Results (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "Start a new scan" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "You have unsaved results, do you really want to continue?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "Name" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "State" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "Excluded" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "Normal" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "Remove Selected" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "Clear" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "Close" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "Details" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "Tags to scan:" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "Track" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "Artist" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "Album" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "Title" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "Genre" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "Year" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "Word weighting" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "Match similar words" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "Can mix file kind" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "Use regular expressions when filtering" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "Remove empty folders on delete or move" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "Ignore duplicates hardlinking to the same file" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "Debug mode (restart required)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "Match pictures of different dimensions" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "Filter Hardness:" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "More Results" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "Fewer Results" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "Font size:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "Language:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "Copy and Move:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "Right in destination" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "Recreate relative path" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "Recreate absolute path" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "Custom Command (arguments: %d for dupe, %r for ref):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "dupeGuru has to restart for language changes to take effect." #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "Re-Prioritize duplicates" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "Problems!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "Reveal Selected" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "Actions" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "Show Dupes Only" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "Show Delta Values" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "Send Marked to Recycle Bin..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "Move Marked to..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "Copy Marked to..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "Remove Marked from Results" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "Re-Prioritize Results..." #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "Remove Selected from Results" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "Add Selected to Ignore List" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "Make Selected into Reference" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "Open Selected with Default Application" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "Open Containing Folder of Selected" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "Rename Selected" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "Mark All" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "Mark None" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "Invert Marking" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "Mark Selected" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "Export To HTML" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "Export To CSV" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "Save Results..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "Invoke Custom Command" #: qt/result_window.py:102 msgid "Mark" msgstr "Mark" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "Columns" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "Reset to Defaults" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} Results" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "Dupes Only" #: qt/result_window.py:194 msgid "Delta Values" msgstr "Delta Values" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "Select a file to save your results to" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "Ignore files smaller than" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ Results" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "Action" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "Add New Folder..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "Advanced" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "Automatically check for updates" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "Basic" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "Bring All to Front" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "Check for update..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "Close Window" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "Copy" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "Custom command (arguments: %d for dupe, %r for ref):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "Cut" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Delta" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "Details of Selected File" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "Details Panel" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "Directories" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "dupeGuru Preferences" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "dupeGuru Results" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "dupeGuru Website" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "Edit" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "Export Results to CSV" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "Export Results to XHTML" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "Fewer results" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "Filter" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "Filter hardness:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Filter Results..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "Folder Selection Window" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "Font Size:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "Hide dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "Hide Others" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "Ignore files smaller than:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "Load from file..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "Minimize" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "Mode" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "More results" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "Ok" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "Paste" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "Preferences..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "Quick Look" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "Quit dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "Reset to Default" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "Reset To Defaults" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "Reveal" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Reveal Selected in Finder" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "Select All" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Send Marked to Trash..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "Services" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "Show All" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "Start Duplicate Scan" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "The name '%@' already exists." #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "Window" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Zoom" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "Exclusion Filters" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "Scan Results" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "Load Directories..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "Save Directories..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "Select a directories file to load" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "dupeGuru Results (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "Select a file to save your directories to" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "dupeGuru Directories (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "Add" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "Restore defaults" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "Test string" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "Type a python regular expression here..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "Type a file system path or filename here..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "Compilation error: " #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "Increase zoom" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "Decrease zoom" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "Normal size" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "Best fit" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "Picture cache mode:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "Override theme icons in viewer toolbar" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" "Use our own internal icons instead of those provided by the theme engine" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "Show scrollbars in image viewers" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "Use default position for tab bar (requires restart)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "Use bold font for references" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "Reference foreground color:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "Reference background color:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "Delta foreground color:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "Show the title bar and can be docked" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "The title bar can only be disabled while the window is docked" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "Vertical title bar" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "" "Change the title bar from horizontal on top, to vertical on the left side" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "Show tab bar" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " #: qt\app.py:256 msgid "Results" msgstr "Results" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "General Interface" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "Result Table" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "Details Window" #: qt\preferences_dialog.py:285 msgid "General" msgstr "General" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "Display" dupeguru-4.3.1/locale/es/000077500000000000000000000000001426171743600152405ustar00rootroot00000000000000dupeguru-4.3.1/locale/es/LC_MESSAGES/000077500000000000000000000000001426171743600170255ustar00rootroot00000000000000dupeguru-4.3.1/locale/es/LC_MESSAGES/columns.po000066400000000000000000000053311426171743600210470ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # msgid "" msgstr "" "Last-Translator: Andrew Senetar , 2021\n" "Language-Team: Spanish (https://www.transifex.com/voltaicideas/teams/116153/es/)\n" "Language: es\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "Ruta de Fichero" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "Mensaje de error" #: core\me\prioritize.py:23 msgid "Duration" msgstr "Duración" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "Tasa de bits" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "Tasa de Muestreo" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "Nombre de fichero" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "Carpeta" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "Tamaño (MB)" #: core\me\result_table.py:22 msgid "Time" msgstr "Hora" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "Tasa de Muestreo" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "Clase" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "Modificación" #: core\me\result_table.py:27 msgid "Title" msgstr "Título" #: core\me\result_table.py:28 msgid "Artist" msgstr "Artista" #: core\me\result_table.py:29 msgid "Album" msgstr "Álbum" #: core\me\result_table.py:30 msgid "Genre" msgstr "Género" #: core\me\result_table.py:31 msgid "Year" msgstr "Año" #: core\me\result_table.py:32 msgid "Track Number" msgstr "Pista Número" #: core\me\result_table.py:33 msgid "Comment" msgstr "Comentario" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "Coincidencia %" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "Palabras Empleadas" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "Duplicado Número" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "Dimensiones" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "Tamaño (KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "Marca horaria EXIF" #: core\prioritize.py:156 msgid "Size" msgstr "Tamaño" dupeguru-4.3.1/locale/es/LC_MESSAGES/core.po000066400000000000000000000160401426171743600203160ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # IlluminatiWave, 2022 # msgid "" msgstr "" "Last-Translator: IlluminatiWave, 2022\n" "Language-Team: Spanish (https://www.transifex.com/voltaicideas/teams/116153/es/)\n" "Language: es\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: core\app.py:44 msgid "There are no marked duplicates. Nothing has been done." msgstr "No hay duplicados marcados. No se ha hecho nada." #: core\app.py:45 msgid "There are no selected duplicates. Nothing has been done." msgstr "No hay duplicados seleccionados. No se ha hecho nada." #: core\app.py:46 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" "Está a punto de abrir muchas imágenes. Dependiendo de los ficheros que se " "abran, abrirlos puede colgar la máquina. ¿Continuar?" #: core\app.py:73 msgid "Scanning for duplicates" msgstr "Buscando duplicados" #: core\app.py:74 msgid "Loading" msgstr "Cargando" #: core\app.py:75 msgid "Moving" msgstr "Moviendo" #: core\app.py:76 msgid "Copying" msgstr "Copiando" #: core\app.py:77 msgid "Sending to Trash" msgstr "Enviando a la Papelera" #: core\app.py:291 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" "Una acción previa sigue ejecutándose. No puede abrir una nueva todavía. " "Espere unos segundos y vuelva a intentarlo." #: core\app.py:302 msgid "No duplicates found." msgstr "No se han encontrado duplicados." #: core\app.py:317 msgid "All marked files were copied successfully." msgstr "" "Todos los ficheros seleccionados han sido copiados satisfactoriamente." #: core\app.py:319 msgid "All marked files were moved successfully." msgstr "Todos los ficheros seleccionados se han movidos satisfactoriamente." #: core\app.py:321 msgid "All marked files were deleted successfully." msgstr "Todos los ficheros seleccionados se han eliminado satisfactoriamente." #: core\app.py:323 msgid "All marked files were successfully sent to Trash." msgstr "Todo los ficheros marcados se han enviado a la papelera exitosamente." #: core\app.py:328 msgid "Could not load file: {}" msgstr "No se pudo cargar el archivo: {}" #: core\app.py:384 msgid "'{}' already is in the list." msgstr "'{}' ya está en la lista." #: core\app.py:386 msgid "'{}' does not exist." msgstr "'{}' no existe." #: core\app.py:394 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "" "Todas las %d coincidencias seleccionadas van a ser ignoradas en las " "subsiguientes exploraciones. ¿Continuar?" #: core\app.py:471 msgid "Select a directory to copy marked files to" msgstr "Seleccione un directorio donde desee copiar los archivos marcados" #: core\app.py:473 msgid "Select a directory to move marked files to" msgstr "Seleccione un directorio al que desee mover los archivos marcados" #: core\app.py:512 msgid "Select a destination for your exported CSV" msgstr "Seleccionar un destino para el CSV seleccionado" #: core\app.py:518 core\app.py:773 core\app.py:783 msgid "Couldn't write to file: {}" msgstr "No se pudo escribir en el archivo: {}" #: core\app.py:541 msgid "You have no custom command set up. Set it up in your preferences." msgstr "No hay comandos configurados. Establézcalos en sus preferencias." #: core\app.py:697 core\app.py:709 msgid "You are about to remove %d files from results. Continue?" msgstr "Está a punto de eliminar %d ficheros de resultados. ¿Continuar?" #: core\app.py:745 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} grupos de duplicados han sido cambiados por la re-priorización." #: core\app.py:792 msgid "The selected directories contain no scannable file." msgstr "Las carpetas seleccionadas no contienen ficheros para explorar." #: core\app.py:808 msgid "Collecting files to scan" msgstr "Recopilando ficheros a explorar" #: core\app.py:858 msgid "%s (%d discarded)" msgstr "%s (%d descartados)" #: core\directories.py:190 msgid "Collected {} files to scan" msgstr "{} ficheros recopilados para explorar" #: core\directories.py:206 msgid "Collected {} folders to scan" msgstr "{} carpetas recopiladas para explorar" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "%d coincidencias encontradas en %d grupos" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "Enviando {} fichero(s) a la Papelera" #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "Expresiones regulares" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "" "¿Desea realmente eliminar todos los %d elementos de la lista de exclusión?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "Nombre de archivo" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "Nombre de archivo - Campos" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "Nombre de archivo - Campos (sin orden)" #: core\me\scanner.py:23 msgid "Tags" msgstr "Etiquetas" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "Contenido" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "Analizadas %d/%d imágenes" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "Realizado %d/%d trozos coincidentes" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "Preparando para coincidencias" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "Verificadas %d/%d coincidencias" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "Leído EXIF de %d/%d imágenes" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "Marca horaria EXIF" #: core\prioritize.py:70 msgid "None" msgstr "Ninguno" #: core\prioritize.py:102 msgid "Ends with number" msgstr "Termina con un número" #: core\prioritize.py:103 msgid "Doesn't end with number" msgstr "No termina con un número" #: core\prioritize.py:104 msgid "Longest" msgstr "El más largo" #: core\prioritize.py:105 msgid "Shortest" msgstr "El más corto" #: core\prioritize.py:142 msgid "Highest" msgstr "El más alto" #: core\prioritize.py:142 msgid "Lowest" msgstr "El más bajo" #: core\prioritize.py:171 msgid "Newest" msgstr "El más nuevo" #: core\prioritize.py:171 msgid "Oldest" msgstr "El más antiguo" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) duplicados marcados." #: core\results.py:141 msgid " filter: %s" msgstr "filtro: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "Tamaño de lectura de %d/%d ficheros" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "Leyendo metadatos de %d/%d ficheros" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "¡Casi termino! Jugando con los resultados..." #: core\se\scanner.py:18 msgid "Folders" msgstr "Carpetas" dupeguru-4.3.1/locale/es/LC_MESSAGES/ui.po000066400000000000000000001064511426171743600200110ustar00rootroot00000000000000# Translators: # Fuan , 2022 # Andrew Senetar , 2022 # IlluminatiWave, 2022 # msgid "" msgstr "" "Last-Translator: IlluminatiWave, 2022\n" "Language-Team: Spanish (https://www.transifex.com/voltaicideas/teams/116153/es/)\n" "Language: es\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: qt/app.py:81 msgid "Quit" msgstr "Salir" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "Opciones" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Lista de exclusión" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Limpiar el Cache de Fotos" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Ayuda de dupeGuru" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "Acerca de dupeGuru" #: qt/app.py:87 msgid "Open Debug Log" msgstr "Abrir Registro de Depuración" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "¿Desea realmente eliminar todo el caché de análisis de imágenes?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "Caché de fotos limpio." #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} fichero (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Opciones de borrado" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Enlace a ficheros borrados" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" "Después de borrar un duplicado, poner un enlace señalando el fichero de " "referencia para reemplazar el fichero borrado." #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "Enlace permanente" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "Enlace Sym" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr "(no soportado)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Borrar ficheros directamente" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "En lugar de enviar ficheros a la papelera, borrarlos directamente. Esta " "opción se usa habitualmente como alternativa cuando el método normal de " "borrado no funciona." #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Proceder" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Cancelar" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "Atributos" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "Seleccionado" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "Referencia" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "Cargar Resultados..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "Ventana de Resultados" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "Añadir carpeta..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "Fichero" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "Vista" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "Ayuda" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "Cargar Resultados Recientes" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "Modo de aplicación:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "Música" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "Imagen" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "Estándar" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "Tipo de Exploración:" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "Mas Opciones" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "Seleccionar carpetas a explorar y pulsar \"Explorar\"." #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "Cargar Resultados" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "Explorar" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "Resultados sin guardar" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "Tiene resultados sin guardar, ¿está seguro que quiere salir?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "Seleccionar una carpeta a añadir a la lista de exploración" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "Seleccionar un fichero de resultados para cargar" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "Todos los Ficheros (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "Resultados dupeGuru (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "Empezar una nueva exploración" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "Tiene resultados sin guardar, ¿realmente desea continuar?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "Nombre" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "Estado" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "Excluído" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "Normal" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "Quitar Seleccionados" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "Limpiar" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "Cerrar" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "Detalles" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "Etiquetas a explorar:" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "Pista" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "Artista" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "Álbum" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "Título" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "Género" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "Año" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "Ponderación de Palabra" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "Coincidencia con palabras similares" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "Puede mezclar tipos de fichero" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "Use expresiones regulares cuando filtre" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "Eliminar carpetas vacías al borrar o mover" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "Ignorar duplicados enlazando al mismo fichero" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "Mode de depuración (se requiere reinicio)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "Coincidencia de imágenes de distintas dimensiones" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "Dureza del Filtro:" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "Más Resultados" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "Menos Resultados" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "Tamaño de fuente:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "Idioma:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "Copiar y Mover:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "Derechos en destino" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "Recrear ruta relativa" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "Recrear ruta absoluta" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "Comando de Usuario (argumentos: %d para duplicado, %r para ref):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "" "dupeGuru debe reinicializarse para que los cambios de lenguaje surjan " "efecto." #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "Re-Priorizar duplicados" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "Añadir criterio en la casilla izquierda y OK para enviar los duplicados " "correspondientes al mejor de esos criterios a sus respectivos grupos de " "posición de referencia. Consultar la ayuda para más información." #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "¡Problemas!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "Problemas procesando algunos (o todos) los ficheros. El origen de los " "problemas está indicados en la lista inferior. Estos ficheros no se han " "elimanos de sus resultados. " #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "Revelar seleccionados" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "Acciones" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "Mostrar Sólo Duplicados" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "Mostras los Valores Delta" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "Enviar los Marcados a la Papelera de Reciclaje..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "Mover los Marcados a..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "Copiar los Marcados a..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "Quitar los Marcados de los Resultados" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "Re-priorizar los Resultados" #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "Quitar Seleccionados de los Resultados" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "Añadir los seleccionados a la Lista de Exclusión" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "Poner Seleccionados en Referencia" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "Abrir seleccionados con la Aplicación por Defecto" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "Abrir la Carpeta Contenedora de los Seleccionados" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "Renombrar los Seleccionados" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "Marcar Todos" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "Marcar Ninguno" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "Marcado Inverso" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "Marcar Seleccionados" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "Exportar a HTML" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "Exportar a CSV" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "Guardar Resultados..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "Invocar Comando de Usuario" #: qt/result_window.py:102 msgid "Mark" msgstr "Marcar" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "Columnas" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "Reiniciar a Valores por Defecto." #: qt/result_window.py:185 msgid "{} Results" msgstr "{} Resultados" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "Duplicados Únicamente" #: qt/result_window.py:194 msgid "Delta Values" msgstr "Valores Delta" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "Seleccionar un fichero al que guardar los resultados" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "Ignorar ficheros más pequeños de" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ Resultados" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "Acción" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "Añadir Nueva Carpeta..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "Avanzado" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "Comprobar las actualizaciones automáticamente." #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "Básico" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "Traer Todos al Frente" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "Comprobando actualizaciones..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "Cerrar Ventana" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "Copiar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "Comando de usuario (argumentos: %d para duplicado, %r para ref):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "cortar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Delta" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "Detalles del Fichero Seleccionado" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "Panel de Detalles" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "Directorios" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeguru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "Preferencias de dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "Resultados de dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "Web de dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "Editar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "Exportar resultados a CSV" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "Exportar resultados a XHTML" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "Menos resultados" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "Filtro" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "Dureza del filtro:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Resultados del Filtro..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "Ventana de Selección de Carpetas" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "Tamaño de Fuente:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "Minimizar dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "Esconder Otros" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "Ignorar ficheros más pequeños de:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "Cargar fichero desde..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "Minimizar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "Modo" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "Más resultados" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "De acuerdo" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "Pegar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "Preferencias" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "Ojear" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "Cerrar dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "Reiniciar a Valor por Defecto." #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "Reiniciar a Valores por Defecto" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "Mostrar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Mostrar Seleccionados en Buscar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "Seleccionar Todos" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Mover los Marcados a la Papelera..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "Servicios" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "Mostrar Todos" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "Empezar la Exploración de Duplicados" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "El nombre '%@' ya existe." #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "Ventana" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Zoom" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "Filtros de exclusión" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "Resultados del Escáner" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "Cargar Carpetas..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "Guardar Carpetas..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "Seleccione un archivo de carpetas para cargar" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "Resultados dupeGuru (*.dupeguru)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "Seleccionar un fichero al que guardar las carpetas" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "Carpetas dupeGuru (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "Agregar" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "Reiniciar a Valores por Defecto" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "Cadena de prueba" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "Escriba una expresión regular de Python aquí..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "" "Escriba una ruta del sistema de archivos o un nombre de archivo aquí..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Estas expresiones regulares de Python (sensibles a mayúsculas y minúsculas) filtrarán los archivos durante los escaneos.
    Carpetas también tendrá su establecido en Excluido en la pestaña Carpetas si su nombre coincide con una de las expresiones regulares seleccionadas.
    Para cada archivo recopilado, se realizan dos pruebas para determinar si se debe ignorar por completo o no.
  • 1. Las expresiones regulares sin separador de ruta se compararán solo con el nombre del archivo.
  • \n" "
  • 2. Las expresiones regulares con al menos un separador de ruta en ellas se compararán con la ruta completa al archivo.

  • \n" "Ejemplo: si desea filtrar archivos .PNG solo del directorio \"Mis imágenes\":
    .*Mis\\sImágenes\\\\.*\\.png

    Puede probar la expresión regular con el botón \"cadena de prueba\" después de pegar una ruta falsa en el campo de prueba:
    C:\\\\Usario\\Mis Imágenes\\test.png

    \n" "Se resaltarán las expresiones regulares coincidentes.
    Si hay al menos un resaltado, la ruta o el nombre de archivo probado se ignorará durante los escaneos.

    Directorios y archivos que comienzan con un punto '.' se ignoran de forma predeterminada.

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "Error de compilación:" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "Aumentar el zoom" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "Disminuir zoom" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "Tamaño normal" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "Mejor ajuste" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "Modo de caché de imágenes:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "Anula los iconos de temas en la barra de herramientas del visor" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" "Utilice nuestros propios iconos internos en lugar de los proporcionados por " "el motor de temas" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "Mostrar barras de desplazamiento en visores de imágenes" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" "Cuando la imagen mostrada no se ajusta a la ventana gráfica, muestre barras " "de desplazamiento para abarcar la vista" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "" "Use la posición predeterminada para la barra de pestañas (requiere " "reiniciar)." #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "Coloque la barra de pestañas debajo del menú principal en lugar de al lado.\n" "En MacOS, la barra de pestañas llenará el ancho de la ventana." #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "Utilice fuente en negrita para las referencias." #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "Color del texto de referencia:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "Color de fondo de referencia:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "Color del texto de delta:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "Muestra la barra de título y se puede acoplar." #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" "Si la barra de título está ocultada, use la tecla modificadora para " "arrastrar la ventana flotante." #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "" "La barra de título solo se puede desactivar si la ventana está acoplada." #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "Barra de título vertical" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "" "Cambie la barra de título de horizontal en la parte superior a vertical en " "el lado izquierdo." #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "Mostrar barra de pestañas" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Estas expresiones regulares de Python (sensibles a mayúsculas y minúsculas) filtrarán los archivos durante los escaneos.
    Carpetas también tendrá su establecido en Excluido en la pestaña Carpetas si su nombre coincide con una de las expresiones regulares seleccionadas.
    Para cada archivo recopilado, se realizan dos pruebas para determinar si se debe ignorar por completo o no.
  • 1. Las expresiones regulares sin separador de ruta se compararán solo con el nombre del archivo.
  • \n" "
  • 2. Las expresiones regulares con al menos un separador de ruta en ellas se compararán con la ruta completa al archivo.

  • \n" "Ejemplo: si desea filtrar archivos .PNG solo del directorio \"Mis imágenes\":
    .*Mis\\sImágenes\\\\.*\\.png

    Puede probar la expresión regular con el botón \"cadena de prueba\" después de pegar una ruta falsa en el campo de prueba:
    C:\\\\Usario\\Mis Imágenes\\test.png

    \n" "Se resaltarán las expresiones regulares coincidentes.
    Si hay al menos un resaltado, la ruta o el nombre de archivo probado se ignorará durante los escaneos.

    Directorios y archivos que comienzan con un punto '.' se ignoran de forma predeterminada.

    " #: qt\app.py:256 msgid "Results" msgstr "Resultados" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "Interfaz general" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "Tabla de resultados" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "Ventana de detalles" #: qt\preferences_dialog.py:285 msgid "General" msgstr "General" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "Visualización" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "Archivos de hash parcialmente mayores a" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "MB" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "Usar diálogos nativos del SO" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" "Para acciones como la selección de archivos/carpetas, utilice los diálogos nativos del SO\n" "Algunos diálogos nativos tienen una funcionalidad limitada." #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "Ignorar los ficheros mayores a" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "Borrar caché" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" "¿Seguro que quieres borrar la caché? Esto eliminará todos los hashes de " "ficheros y análisis de imágenes almacenados en la caché." #: qt\app.py:299 msgid "Cache cleared." msgstr "Caché eliminada." #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "Usar tema oscuro" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "Perfilar operación de análisis" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" "Perfilar la operación de análisis y guardar los registros para su " "optimización." #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "Registro guardado en: {}" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "Depurar" #: qt\about_box.py:31 msgid "About {}" msgstr "Acerca de {}" #: qt\about_box.py:47 msgid "Version {}" msgstr "Versión {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "Buscando actualizaciones..." #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "Licenciado en GPLv3" #: qt\about_box.py:68 msgid "No update available." msgstr "Sin actualizaciones disponibles." #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "Nueva versión disponible {}, descargar aquí " #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "Informe de error" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "Algo salió mal. ¿Qué tal informar el error?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "Los informes de errores deben notificarse en Problemas de Github. Puede copiar el seguimiento del error anterior y pegarlo en un nuevo número.\n" "\n" "Asegúrese de realizar una búsqueda de los problemas ya existentes de antemano. También asegúrese de probar la última versión disponible en el repositorio, ya que es posible que el error que está experimentando ya se haya corregido.\n" "\n" "Lo que generalmente ayuda es agregar una descripción de cómo obtuvo el error. ¡Gracias!\n" "\n" "Aunque la aplicación debería continuar ejecutándose después de este error, puede estar en un estado inestable, por lo que se recomienda que reinicie la aplicación." #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Ir a Github" #: qt\preferences.py:24 msgid "Czech" msgstr "Checo" #: qt\preferences.py:25 msgid "German" msgstr "Alemán" #: qt\preferences.py:26 msgid "Greek" msgstr "Griego" #: qt\preferences.py:27 msgid "English" msgstr "Inglés" #: qt\preferences.py:28 msgid "Spanish" msgstr "Español" #: qt\preferences.py:29 msgid "French" msgstr "Francés" #: qt\preferences.py:30 msgid "Armenian" msgstr "Armenio" #: qt\preferences.py:31 msgid "Italian" msgstr "Italiano" #: qt\preferences.py:32 msgid "Japanese" msgstr "Japonés" #: qt\preferences.py:33 msgid "Korean" msgstr "Coreano" #: qt\preferences.py:34 msgid "Malay" msgstr "Malayo" #: qt\preferences.py:35 msgid "Dutch" msgstr "Holandés" #: qt\preferences.py:36 msgid "Polish" msgstr "Polaco" #: qt\preferences.py:37 msgid "Brazilian" msgstr "Brasileño" #: qt\preferences.py:38 msgid "Russian" msgstr "Ruso" #: qt\preferences.py:39 msgid "Turkish" msgstr "Turco" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "Ucraniano" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "Vietnamita" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "Chino (simplificado)" #: qt\recent.py:54 msgid "Clear List" msgstr "Limpiar lista" #: qt\search_edit.py:78 msgid "Search..." msgstr "Búsqueda..." dupeguru-4.3.1/locale/fr/000077500000000000000000000000001426171743600152405ustar00rootroot00000000000000dupeguru-4.3.1/locale/fr/LC_MESSAGES/000077500000000000000000000000001426171743600170255ustar00rootroot00000000000000dupeguru-4.3.1/locale/fr/LC_MESSAGES/columns.po000066400000000000000000000052601426171743600210500ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # msgid "" msgstr "" "Last-Translator: Fuan , 2021\n" "Language-Team: French (https://www.transifex.com/voltaicideas/teams/116153/fr/)\n" "Language: fr\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "Chemin du fichier" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "Message d'erreur" #: core\me\prioritize.py:23 msgid "Duration" msgstr "Durée" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "Bitrate" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "Échantillonnage" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "Nom de fichier" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "Dossier" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "Taille (MB)" #: core\me\result_table.py:22 msgid "Time" msgstr "Temps" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "Sample Rate" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "Type" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "Modification" #: core\me\result_table.py:27 msgid "Title" msgstr "Titre" #: core\me\result_table.py:28 msgid "Artist" msgstr "Artiste" #: core\me\result_table.py:29 msgid "Album" msgstr "Album" #: core\me\result_table.py:30 msgid "Genre" msgstr "Genre" #: core\me\result_table.py:31 msgid "Year" msgstr "Année" #: core\me\result_table.py:32 msgid "Track Number" msgstr "Track" #: core\me\result_table.py:33 msgid "Comment" msgstr "Commentaire" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "Match %" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "Mots" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "Nombre de Doublons" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "Dimensions" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "Taille (KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "Date EXIF" #: core\prioritize.py:156 msgid "Size" msgstr "Taille" dupeguru-4.3.1/locale/fr/LC_MESSAGES/core.po000066400000000000000000000153621426171743600203240ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # msgid "" msgstr "" "Last-Translator: Fuan , 2021\n" "Language-Team: French (https://www.transifex.com/voltaicideas/teams/116153/fr/)\n" "Language: fr\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "Aucun doublon marqué. Rien à faire." #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "Aucun doublon sélectionné. Rien à faire." #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" "Beaucoup de fichiers seront ouverts en même temps. Cela peut gravement " "encombrer votre système. Continuer?" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "Scan de doublons en cours" #: core\app.py:72 msgid "Loading" msgstr "Chargement en cours" #: core\app.py:73 msgid "Moving" msgstr "Déplacement en cours" #: core\app.py:74 msgid "Copying" msgstr "Copie en cours" #: core\app.py:75 msgid "Sending to Trash" msgstr "Envoi de fichiers à la corbeille" #: core\app.py:289 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" "Une action précédente est encore en cours. Attendez quelques secondes avant " "d'en repartir une nouvelle." #: core\app.py:300 msgid "No duplicates found." msgstr "Aucun doublon trouvé." #: core\app.py:315 msgid "All marked files were copied successfully." msgstr "Tous les fichiers marqués ont été copiés correctement." #: core\app.py:317 msgid "All marked files were moved successfully." msgstr "Tous les fichiers marqués ont été déplacés correctement." #: core\app.py:319 msgid "All marked files were deleted successfully." msgstr "" #: core\app.py:321 msgid "All marked files were successfully sent to Trash." msgstr "" "Tous les fichiers marqués ont été correctement envoyés à la corbeille." #: core\app.py:326 msgid "Could not load file: {}" msgstr "Impossible d'ouvrir le fichier: {}" #: core\app.py:382 msgid "'{}' already is in the list." msgstr "'{}' est déjà dans la liste." #: core\app.py:384 msgid "'{}' does not exist." msgstr "'{}' n'existe pas." #: core\app.py:392 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "%d fichiers seront ignorés des prochains scans. Continuer?" #: core\app.py:469 msgid "Select a directory to copy marked files to" msgstr "Sélectionnez un dossier vers lequel copier les fichiers marqués." #: core\app.py:471 msgid "Select a directory to move marked files to" msgstr "Sélectionnez un dossier vers lequel déplacer les fichiers marqués." #: core\app.py:510 msgid "Select a destination for your exported CSV" msgstr "Choisissez une destination pour votre exportation CSV" #: core\app.py:516 core\app.py:771 core\app.py:781 msgid "Couldn't write to file: {}" msgstr "Impossible d'écrire le fichier: {}" #: core\app.py:539 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" "Vous n'avez pas de commande personnalisée. Ajoutez-la dans vos préférences." #: core\app.py:695 core\app.py:707 msgid "You are about to remove %d files from results. Continue?" msgstr "%d fichiers seront retirés des résultats. Continuer?" #: core\app.py:743 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} groupes de doublons ont été modifiés par la re-prioritisation." #: core\app.py:790 msgid "The selected directories contain no scannable file." msgstr "Les dossiers sélectionnés ne contiennent pas de fichiers valides." #: core\app.py:803 msgid "Collecting files to scan" msgstr "Collecte des fichiers à scanner" #: core\app.py:850 msgid "%s (%d discarded)" msgstr "%s (%d hors-groupe)" #: core\directories.py:191 msgid "Collected {} files to scan" msgstr "" #: core\directories.py:207 msgid "Collected {} folders to scan" msgstr "" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "Vous envoyez {} fichier(s) à la corbeille." #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "Expressions régulières" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "" "Voulez-vous vider la liste de fichiers ignorés des %d items qu'elle " "contient?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "Nom de fichier" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "Nom de fichier - Champs" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "Nom de fichier - Champs (sans ordre)" #: core\me\scanner.py:23 msgid "Tags" msgstr "Tags" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "Contenu" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "Analyzé %d/%d images" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "%d/%d blocs d'images comparés" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "Préparation pour la comparaison" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "Vérifié %d/%d paires" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "Lu l'EXIF de %d/%d images" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "Date EXIF" #: core\prioritize.py:70 msgid "None" msgstr "Aucune" #: core\prioritize.py:100 msgid "Ends with number" msgstr "Chiffres à la fin" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "Pas de chiffres à la finr" #: core\prioritize.py:102 msgid "Longest" msgstr "Le plus long" #: core\prioritize.py:103 msgid "Shortest" msgstr "Le plus court" #: core\prioritize.py:140 msgid "Highest" msgstr "Plus grand" #: core\prioritize.py:140 msgid "Lowest" msgstr "Moins grand" #: core\prioritize.py:169 msgid "Newest" msgstr "Plus récent" #: core\prioritize.py:169 msgid "Oldest" msgstr "Moins récent" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) doublons marqués." #: core\results.py:141 msgid " filter: %s" msgstr " filtre: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "Lu la taille de %d/%d fichiers" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "Lu les métadonnées de %d/%d fichiers" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "Bientôt terminé! Bidouille des résultats..." #: core\se\scanner.py:18 msgid "Folders" msgstr "Dossiers" dupeguru-4.3.1/locale/fr/LC_MESSAGES/ui.po000066400000000000000000001051341426171743600200060ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2022 # Fuan , 2022 # msgid "" msgstr "" "Last-Translator: Fuan , 2022\n" "Language-Team: French (https://www.transifex.com/voltaicideas/teams/116153/fr/)\n" "Language: fr\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: qt/app.py:81 msgid "Quit" msgstr "Quitter" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "Options" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Liste de doublons ignorés" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Vider la cache d'images" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Aide dupeGuru" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "À propos de dupeGuru" #: qt/app.py:87 msgid "Open Debug Log" msgstr "Ouvrir logs de déboguage" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "Voulez-vous vraiment vider la cache de vos analyses précédentes?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "La cache des analyses précédentes a été vidée." #: qt/app.py:251 msgid "{} file (*.{})" msgstr "Fichier {} (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Options de suppression" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Remplacer les fichiers effacés par des liens" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" "Après avoir effacé un fichier, remplacer celui-ci par un lien vers le " "fichier référence." #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "Hardlink" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "Symlink" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr "(non pris en charge)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Supprimer les fichiers directement" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "Au lieu de passer par la corbeille, supprimer directement. Cette option " "n'est généralement utilisée qu'en cas de problème." #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Continuer" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Annuler" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "Attribut" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "Sélectionné" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "Référence" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "Charger résultats..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "Fenêtre de résultats" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "Ajouter dossier..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "Fichier" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "Voir" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "Aide" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "Charger résultats récents" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "Mode de l'application:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "Musique" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "Image" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "Standard" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "Type de scan:" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "Plus d'options" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "Sélectionnez les dossiers à scanner puis faites \"Scan\"." #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "Charger" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "Scan" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "Résultats non sauvegardés" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "Vos résultats ne sont pas sauvegardés. Voulez-vous vraiment quitter?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "Sélectionnez un dossier à ajouter à la liste" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "Sélectionnez un fichier résultats à charger" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "Tout les fichiers (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "Résultats dupeGuru (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "Commencer un nouveau scan" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "" "Vos résultats ne sont pas sauvegardés. Voulez-vous vraiment continuer?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "Nom" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "Type" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "Exclu" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "Normal" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "Effacer sélection" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "Vider" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "Fermer" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "Détails" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "Tags à scanner:" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "Track" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "Artiste" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "Album" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "Titre" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "Genre" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "Année" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "Proportionalité des mots" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "Comparer les mots similaires" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "Comparer les fichiers de différents types" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "Utiliser les expressions régulières pour les filtres" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "Effacer les dossiers vides après un déplacement" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "Ignorer doublons avec hardlink vers le même fichier" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "Mode de déboguage (redémarrage requis)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "Comparer les images de tailles différentes" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "Seuil du filtre:" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "Plus de doublons" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "Moins de doublons" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "Taille de police:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "Langue:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "Déplacements de fichiers:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "Directement à la destination" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "Re-créer chemins relatifs" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "Re-créer chemins absolus" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "Commande perso. (arguments: %d pour doublon, %r pour réf):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "dupeGuru doit redémarrer pour appliquer le changement de langue." #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "Re-prioriser les doublons" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "Ajoutez des critères dans la liste de droite pour envoyer les doublons qui " "correspondent le plus à ces critère à la position de référence. Une lecture " "préalable du fichier d'aide est conseillée." #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "Problèmes!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "Des problèmes ont été rencontrés lors du traitement de certains fichiers. La" " nature de ces problèmes est décrite dans la liste ci-dessous. Ces fichiers " "n'ont pas été retirés des résultats." #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "Révéler Fichier" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "Actions" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "Ne pas montrer les références" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "Montrer les valeurs en tant que delta" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "Envoyer marqués à la corbeille..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "Déplacer marqués vers..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "Copier marqués vers..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "Retirer marqués des résultats" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "Re-prioriser les résultats" #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "Retirer sélectionnés des résultats" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "Ajouter sélectionnés à la liste de fichiers ignorés" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "Transformer sélectionnés en références" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "Ouvrir sélectionné avec l'application par défaut" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "Ouvrir le dossier contenant le fichier sélectionné" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "Renommer sélectionné" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "Tout marquer" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "Tout démarquer" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "Inverser le marquage" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "Marquer sélectionnés" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "Exporter vers HTML" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "Exporter vers CSV" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "Sauvegarder résultats..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "Invoquer commande personnalisée" #: qt/result_window.py:102 msgid "Mark" msgstr "Marquer" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "Colonnes" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "Réinitialiser" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} résultats" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "Sans réf." #: qt/result_window.py:194 msgid "Delta Values" msgstr "Delta" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "Sélectionnez un fichier dans lequel sauvegarder les résultats" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "Ignorer les fichiers plus petits que" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "Résultats de %@" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "Action" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "Ajouter dossier..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "Avancé" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "Vérifier automatiquement les mises à jour" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "Simple" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "Tout ramener au premier plan" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "Mise à jour..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "Fermer" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "Copier" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "Commande perso. (arguments: %d pour doublon, %r pour réf):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "Couper" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Delta" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "Détails du fichier sélectionné" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "Fenêtre de détails" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "Dossiers" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "Préférences de dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "Résultats dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "Site web de dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "Édition" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "Exporter les résultats vers CSV" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "Exporter les résultats vers HTML" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "moins de doublons" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "Filtre" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "Seuil du filtre:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Filtrer les résultats..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "Fenêtre de sélection de dossiers" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "Taille de police:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "Masquer dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "Masquer les autres" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "Ignorer les fichiers plus petits que:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "Charger un fichier..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "Placer dans le Dock" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "Mode" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "plus de doublons" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "Ok" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "Coller" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "Préférences..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "Quick Look" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "Quitter dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "Colonnes par défault" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "Valeurs par défaut" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "Révéler" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Révéler sélectionné dans Finder" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "Tout sélectionner" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Envoyer marqués à la corbeille..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "Services" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "Tout afficher" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "Commencer à scanner" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "Le nom '%@' existe déjà." #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "Fenêtre" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Réduire/agrandir" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "Filtres d'exclusion" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "Résultats de scan" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "Charger dossiers..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "Sauvegarder dossiers..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "Sélectionnez un fichier de dossier à charger" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "Dossiers dupeGuru (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "Sélectionnez un fichier pour y sauvegarder vos dossiers" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "Dossiers dupeGuru (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "Ajouter" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "Réinitialiser" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "Tester chaîne" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "Tapez une expression régulière python ici..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "Tapez un chemin ou un nom de fichier ici..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Ces expressions régulières python (sensible aux majuscules) peuvent ignorer les fichiers pendant les scans. Les dossiers auront également leur état par défaut mis sur Exclus dans l'onglet Dossiers si leur nom correspond à une des expressions régulières sélectionnées
    Pour chaque fichier collecté, deux tests sont faits pour déterminer s'il doit être totalement ignoré:
  • 1. Les expressions régulières sans séparateur de chemin sont comparées au nom de fichier seul.
  • \n" "
  • 2. Les expressions régulières avec au moins un séparateur de chemin sont comparées au chemin complet vers le fichier.

  • \n" "Exemple: si vous voulez uniquement ignorer les fichiers .PNG du dossier \"Mes Images\":
    .*Mes\\sImages\\\\.*\\.png

    Vous pouvez tester l'expression régulière via le bouton \"Tester la chaîne de caractères\" après avoir tapé un faux chemin de fichier dans le champs correspondant:
    C:\\\\Utilisateur\\Mes Images\\test.png

    \n" "Les expressions régulières qui fonctionnent seront surlignées.
    S'il y a au moins un surlignage, le chemin ou nom de fichier testé sera ignoré durant les scans.

    Les dossiers et fichiers commençant par un point '.' sont ignorés par défaut.

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "Erreur de compilation:" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "Accroître zoom" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "Décroître zoom" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "Taille normale" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "Meilleur ajustement" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "Mode de cache d'images:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "Outrepasser le thème d’icônes dans le visualiseur" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" "Utiliser nos propres icônes plutôt que celles fournies par le moteur du " "thème" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "Afficher les barres de défilement dans visualiseur d'images" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" "Si l'image affichée ne rentre par dans la fenêtre, afficher les barres de " "défilement pour faire glisser la vue" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "Position par défaut pour la barre d'onglets (redémarrage requis)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "Placer la barre d'onglets sous le menu principal plutôt qu'à côté.\n" "Sur MacOS, cette barre remplira la fenêtre en largeur." #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "Utiliser police en gras pour les références" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "Couleur du texte des référénces:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "Couleur de fond pour les références:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "Couleur du texte des delta:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "Affiche barre de titre et peut être ancrée" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" "Quand la barre de titre est cachée, utilisez la touche méta pour déplacer la" " fenêtre flottante" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "" "La barre de titre ne peut être désactivée que quand la fenêtre est ancrée" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "Barre de titre verticale" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "" "Placer la barre de titre à la verticale côté gauche plutôt que à " "l'horizontale en haut" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "Afficher la barre d'onglets" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Ces expressions régulières python (sensible aux majuscules) peuvent ignorer les fichiers pendant les scans. Les dossiers auront également leur état par défaut mis sur Exclus dans l'onglet Dossiers si leur nom correspond à une des expressions régulières sélectionnées
    Pour chaque fichier collecté, deux tests sont faits pour déterminer s'il doit être totalement ignoré:
  • 1. Les expressions régulières sans séparateur de chemin sont comparées au nom de fichier seul.
  • \n" "
  • 2. Les expressions régulières avec au moins un séparateur de chemin sont comparées au chemin complet vers le fichier.

  • \n" "Exemple: si vous voulez uniquement ignorer les fichiers .PNG du dossier \"Mes Images\":
    .*Mes\\sImages\\\\.*\\.png

    Vous pouvez tester l'expression régulière via le bouton \"Tester la chaîne de caractères\" après avoir tapé un faux chemin de fichier dans le champs correspondant:
    C:\\\\Utilisateur\\Mes Images\\test.png

    \n" "Les expressions régulières qui fonctionnent seront surlignées.
    S'il y a au moins un surlignage, le chemin ou nom de fichier testé sera ignoré durant les scans.

    Les dossiers et fichiers commençant par un point '.' sont ignorés par défaut.

    " #: qt\app.py:256 msgid "Results" msgstr "Résultats" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "Interface générale" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "Tableau de résultats" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "Fenêtre de détails" #: qt\preferences_dialog.py:285 msgid "General" msgstr "Général" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "Affichage" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" #: qt\app.py:299 msgid "Cache cleared." msgstr "" #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "" #: qt\about_box.py:31 msgid "About {}" msgstr "A propos de {}" #: qt\about_box.py:47 msgid "Version {}" msgstr "Version {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "" #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "Sous licence GPLv3" #: qt\about_box.py:68 msgid "No update available." msgstr "" #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "Rapport d'erreur" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "Un problème est survenu. Rapporter l'erreur?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "Les rapports d'erreur doivent être envoyé via les tickets Github. Vous pouvez copier l'historique d'erreur ci-dessus et le coller dans un nouveau ticket.\n" "\n" "Veuillez vous assurer auparavant d'avoir fait une recherche pour un ticket similaire. Assurez-vous aussi d'avoir testé la toute dernière version disponible depuis le dépôt car le bug que vous avez rencontré a peut-être déjà été corrigé. \n" "\n" "Décrire comment vous avez rencontré cette erreur est aussi très précieux. Merci!\n" "\n" " Même si cette application continue de fonctionner après cette erreur, elle peut être dans un état instable, et il est donc recommandé de relancer l'application." #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Aller sur Github" #: qt\preferences.py:24 msgid "Czech" msgstr "Tchèque" #: qt\preferences.py:25 msgid "German" msgstr "Allemand" #: qt\preferences.py:26 msgid "Greek" msgstr "Grecque" #: qt\preferences.py:27 msgid "English" msgstr "Anglais" #: qt\preferences.py:28 msgid "Spanish" msgstr "Espagnol" #: qt\preferences.py:29 msgid "French" msgstr "Français" #: qt\preferences.py:30 msgid "Armenian" msgstr "Arménien" #: qt\preferences.py:31 msgid "Italian" msgstr "Italien" #: qt\preferences.py:32 msgid "Japanese" msgstr "Japonais" #: qt\preferences.py:33 msgid "Korean" msgstr "Coréen" #: qt\preferences.py:34 msgid "Malay" msgstr "Malaisien" #: qt\preferences.py:35 msgid "Dutch" msgstr "Néerlandais" #: qt\preferences.py:36 msgid "Polish" msgstr "Polonais" #: qt\preferences.py:37 msgid "Brazilian" msgstr "Brésilien" #: qt\preferences.py:38 msgid "Russian" msgstr "Russe" #: qt\preferences.py:39 msgid "Turkish" msgstr "Turc" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "Ukrainien" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "Vietnamien" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "Chinois (Simplifié)" #: qt\recent.py:54 msgid "Clear List" msgstr "Vider la liste" #: qt\search_edit.py:78 msgid "Search..." msgstr "Recherche..." dupeguru-4.3.1/locale/hy/000077500000000000000000000000001426171743600152515ustar00rootroot00000000000000dupeguru-4.3.1/locale/hy/LC_MESSAGES/000077500000000000000000000000001426171743600170365ustar00rootroot00000000000000dupeguru-4.3.1/locale/hy/LC_MESSAGES/columns.po000077500000000000000000000056311426171743600210660ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # msgid "" msgstr "" "Last-Translator: Fuan , 2021\n" "Language-Team: Armenian (https://www.transifex.com/voltaicideas/teams/116153/hy/)\n" "Language: hy\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "Ֆայլի ճ-ը" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "Սխալի գրությունը" #: core\me\prioritize.py:23 msgid "Duration" msgstr "Տևողությունը" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "Բիթրեյթը" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "Սիմպլրեյթը" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "Ֆայլի անունը" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "Թղթապանակ" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "Չափը (ՄԲ)" #: core\me\result_table.py:22 msgid "Time" msgstr "Ժամանակը" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "Սեմփլրեյթը" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "Տեսակ" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "Փոփոխությունը" #: core\me\result_table.py:27 msgid "Title" msgstr "Անունը" #: core\me\result_table.py:28 msgid "Artist" msgstr "Կատարողը" #: core\me\result_table.py:29 msgid "Album" msgstr "Ալբոմը" #: core\me\result_table.py:30 msgid "Genre" msgstr "Ժանրը" #: core\me\result_table.py:31 msgid "Year" msgstr "Տարին" #: core\me\result_table.py:32 msgid "Track Number" msgstr "Շավիղի համարը" #: core\me\result_table.py:33 msgid "Comment" msgstr "Մեկնաբանություն" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "Համընկնում %-ին" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "Բառ է օգտ." #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "Խաբկանքի ք-ը" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "Չափերը" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "Չափը (ԿԲ)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "EXIF Timestamp" #: core\prioritize.py:156 msgid "Size" msgstr "Չափը" dupeguru-4.3.1/locale/hy/LC_MESSAGES/core.po000077500000000000000000000201701426171743600203310ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # msgid "" msgstr "" "Last-Translator: Fuan , 2021\n" "Language-Team: Armenian (https://www.transifex.com/voltaicideas/teams/116153/hy/)\n" "Language: hy\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "Նշված կրկնօրինակներ չկան: Ոչինչ չի արվել." #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "Ընտրված կրկնօրինակներ չկան: Ոչինչ չի արվել." #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" "Դուք պատրաստվում եք միանգամից շատ ֆայլեր բացել: Կախված այն բանից, թե ինչով " "են բացվում այդ ֆայլերը, դա անելը կարող է բավականին խառնաշփոթ ստեղծել: " "Շարունակել?" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "Ստուգվում են կրկնօրինակները" #: core\app.py:72 msgid "Loading" msgstr "Բացվում է" #: core\app.py:73 msgid "Moving" msgstr "Տեղափոխվում է" #: core\app.py:74 msgid "Copying" msgstr "Պատճենվում է" #: core\app.py:75 msgid "Sending to Trash" msgstr "Ուղարկվում է Աղբարկղ" #: core\app.py:289 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" "Նախորդ գործողությունը դեռևս ձեռադրում է այստեղ: Չեք կարող սկսել մեկ ուրիշը: " "Սպասեք մի քանի վայրկյան և կրկին փորձեք:" #: core\app.py:300 msgid "No duplicates found." msgstr "Կրկնօրինակներ չկան:" #: core\app.py:315 msgid "All marked files were copied successfully." msgstr "Բոլոր նշված ֆայլերը հաջողությամբ պատճենվել են:" #: core\app.py:317 msgid "All marked files were moved successfully." msgstr "Բոլոր նշված ֆայլերը հաջողությամբ տեղափոխվել են:" #: core\app.py:319 msgid "All marked files were deleted successfully." msgstr "" #: core\app.py:321 msgid "All marked files were successfully sent to Trash." msgstr "Բոլոր նշված ֆայլերը հաջողությամբ Ջնջվել են:" #: core\app.py:326 msgid "Could not load file: {}" msgstr "Հնարավոր չէ բեռնել ֆայլը: {}" #: core\app.py:382 msgid "'{}' already is in the list." msgstr "'{}'-ը արդեն առկա է ցանկում:" #: core\app.py:384 msgid "'{}' does not exist." msgstr "'{}'-ը գոյություն չունի:" #: core\app.py:392 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "" "Ընտրված %d համընկնումները կանտեսվեն հետագա բոլոր ստուգումներից: Շարունակե՞լ:" #: core\app.py:469 msgid "Select a directory to copy marked files to" msgstr "Ընտրեք գրացուցակ, որտեղ ցանկանում եք պատճենել նշված ֆայլերը" #: core\app.py:471 msgid "Select a directory to move marked files to" msgstr "" "Խնդրում ենք ընտրել գրացուցակ, որտեղ ցանկանում եք տեղափոխել նշված ֆայլերը" #: core\app.py:510 msgid "Select a destination for your exported CSV" msgstr "Ընտրեք նպատակակետ ձեր արտահանված CSV- ի համար" #: core\app.py:516 core\app.py:771 core\app.py:781 msgid "Couldn't write to file: {}" msgstr "Չէր կարող գրել է ֆայլը: {}" #: core\app.py:539 msgid "You have no custom command set up. Set it up in your preferences." msgstr "Դուք չեք կատարել Հրամանի ընտրություն: Կատարեք այն կարգավորումներում:" #: core\app.py:695 core\app.py:707 msgid "You are about to remove %d files from results. Continue?" msgstr "Դուք պատրաստվում եք ջնջելու %d ֆայլեր: Շարունակե՞լ:" #: core\app.py:743 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} կրկնօրինակ խմբերը փոխվել են առաջնահերթության կարգով:" #: core\app.py:790 msgid "The selected directories contain no scannable file." msgstr "Ընտրված թղթապանակները պարունակում են չստուգվող ֆայլ:" #: core\app.py:803 msgid "Collecting files to scan" msgstr "Հավաքվում են ֆայլեր՝ ստուգելու համար" #: core\app.py:850 msgid "%s (%d discarded)" msgstr "%s (%d անպիտան)" #: core\directories.py:191 msgid "Collected {} files to scan" msgstr "" #: core\directories.py:207 msgid "Collected {} folders to scan" msgstr "" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "Դուք {} ֆայլ եք ուղարկում աղբարկղ:" #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "Կանոնավոր արտահայտություններ" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "Ցանկանու՞մ եք հեռացնել բոլոր %d ֆայլերը անտեսումների ցանկից:" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "Ֆայլի անունը" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "Ֆայլի անվանումը - Դաշտեր" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "Ֆայլի անուն - դաշտեր (պատվեր չկա)" #: core\me\scanner.py:23 msgid "Tags" msgstr "Tags" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "Բովանդակություն" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "Ստուգվում է %d/%d նկարները" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "Կատարվում է %d/%d տվյալի համընկնում" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "Նախապատրաստեցվում է համընկնումը" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "Ստուգում է %d/%d համընկնումները" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "Կարդալ EXIF-ը d/%d նկարներից" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "EXIF Timestamp" #: core\prioritize.py:70 msgid "None" msgstr "Ոչինչ" #: core\prioritize.py:100 msgid "Ends with number" msgstr "Ավարտվում է թվով" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "Չի ավարտվում է թվով" #: core\prioritize.py:102 msgid "Longest" msgstr "Ամենաերկար" #: core\prioritize.py:103 msgid "Shortest" msgstr "Ամենակարճը" #: core\prioritize.py:140 msgid "Highest" msgstr "Ամենաբարձրը" #: core\prioritize.py:140 msgid "Lowest" msgstr "Ամենացածրը" #: core\prioritize.py:169 msgid "Newest" msgstr "Նորագույնը" #: core\prioritize.py:169 msgid "Oldest" msgstr "Ամենահինը" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) նշված կրկնօրինակներ:" #: core\results.py:141 msgid " filter: %s" msgstr "ֆիլտր. %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "Կարդալ %d/%d ֆայլերի չափը" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "Կարդալ %d/%d ֆայլերի մետատվյալները" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "Գրեթե արված է! Արդյունքների կազմակերպում..." #: core\se\scanner.py:18 msgid "Folders" msgstr "Թղթապանակներ" dupeguru-4.3.1/locale/hy/LC_MESSAGES/ui.po000077500000000000000000001045301426171743600200210ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2022 # Fuan , 2022 # msgid "" msgstr "" "Last-Translator: Fuan , 2022\n" "Language-Team: Armenian (https://www.transifex.com/voltaicideas/teams/116153/hy/)\n" "Language: hy\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: qt/app.py:81 msgid "Quit" msgstr "Փակել" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "Ընտրանքներ" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "անտեսել ցուցակ" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Մաքրել նկարի պահոցը" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "dupeGuru-ի Օգնությունը" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "dupeGuru-ի մասին" #: qt/app.py:87 msgid "Open Debug Log" msgstr "Բացել Սխալների մատյանը" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "Ցանկանու՞մ եք հեռացնել բոլոր պահված նկարները ստուգելուց:" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "Նկարի պահոցը մաքրվել է:" #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} ֆայլ (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Ջնջումների Ընտրանքներ" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Կապել ջնջված ֆայլերը" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" "րկնօրինակը ջնջելուց հետո տեղադրեք հղում, տեղադրել հղում նպատակային հղվող " "ֆայլը փոխարինել ջնջված ֆայլը." #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "Hardlink" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "Symlink" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr " (չաջակցվող)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Ուղղակիորեն ջնջեք ֆայլերը" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "Ֆայլերը աղբարկղ ուղարկելու փոխարեն, դրանք ուղղակիորեն ջնջեք: Այս տարբերակը " "սովորաբար օգտագործվում է որպես լուծում, երբ ջնջման սովորական մեթոդը չի " "գործում:" #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Շարունակել" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Չեղարկել" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "Հատկանիշը" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "Ընտրված" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "Հղումը" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "Բացել արդյունքները..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "Արդյունքի պատուհանը" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "Ավելացնել թղթապանակ..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "Ֆայլ" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "Տեսքը" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "Օգնություն" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "Բացել Վերջին արդյունքները" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "Դիմումի ռեժիմ" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "Երաժշտություն" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "Նկար" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "Ստանդարտ" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "Ստուգելու տեսակը." #: qt/directories_dialog.py:135 msgid "More Options" msgstr "Լրացուցիչ ընտրանքներ" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "Ընտրեք ստուգելու թղթապանակները և սեղմեք \"Ստուգել\":" #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "Բացել արդյունքները" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "Ստուգել" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "Չպահպանված արդյունքները" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "Դուք ունեք չպահպանված արդյունքներ, իրո՞ք փակել:" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "Ընտրեք ստուգման ցանկը ավելացնելու թղթապանակը" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "Ընտրեք արդյունքի ֆայլը՝ բացելու համար" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "Բոլոր ֆայլերը (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "dupeGuru-ի արդյունքները (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "Սկսել նոր ստուգումը" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "Ունեք չպահպանված արդյունքներ, իրո՞ք շարունակել:" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "Անունը" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "Վիճակը" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "Բացառված" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "Նորմալ" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "Հեռացնել ընտրվածը" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "Մաքրել" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "Փակել" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "Մանրամասն" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "Ստուգվող կցապիտակները." #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "Շավիղը" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "Կատարողը" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "Ալբոմը" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "Վերնագիրը" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "Ժանրը" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "Տարին" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "Բառի չափը" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "Ըստ նման բառերի համընկնման" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "Կարող է խառը տեսակի" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "Օգտ. կանոնավոր սահմանումներ ֆիլտրելիս" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "Հեռացնել" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "Անտեսել կրկնօրինակները հարդ նույն ֆայլը" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "Սխալի եղանակը (պահանջում է վերագործարկում)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "Նկարների համընկնում տարբեր չափերով" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "Ֆիլտրի կարգը." #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "Լր. արդյունքներ" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "Քիչ արդյունքներ" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "Տառի չափը." #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "Լեզուն." #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "Պատճենել և Տեղափոխել." #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "Նշվածից աջ" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "Վերաստեղծել հարաբերական ճ-ը" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "Վերաստեղծել բացարձակ ճ-ը" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "Հրամանի կատարում (փաստարկներ. %d խաբկանքի, %r հղման համար)" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "dupeGuru-ը պետք է վերագործարկվի՝ լեզուն կիրառելու համար:" #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "Վերաառաջնավորել կրկնօրինակները" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "Ավելացրեք պահանջներ աջ վանդակում և սեղմեք ԼԱՎ՝ ուղարկելու համար պատճեները, " "որոնք համապատասխանում են այս պահանջներին: Մանրամասները Օգնությունում:" #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "Խնդիրներ!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "Խնդիրներ են առկա որոշ (կամ բոլոր) ֆայլերի հետ գործողություններում: Այդ " "խնդիրների լուծումը նկարագրված է հետևյալ աղյուսակում: Այս ֆայլերը չեն հեռացվի" " արդյունքներից:" #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "Ցուցադրել ընտրվածը" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "Գործողություններ" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "Ցուցադրել միայն պատճեները" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "Ցուցադրել դելտա նշան-ը" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "Ուղարկել նշվածները Աղբարկղ..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "Տեղափ. նշվածը՝" #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "Պատճ. նշվածը՝" #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "Հեռացնել նշվածները ցանկից" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "Վերաառաջնայնավորել արդյունքները..." #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "ՀԵռացնել ընտրվածը արդյունքներից" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "Ավելացնել ընտրվածը Անտեսումների ցանկ" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "Ընտրված նյութը դարձրեք տեղեկատու նյութ:" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "Բացել ընտրվածը Հիմնական ծրագրով" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "Բացել ընտրվածը պարունակող թղթապանակը" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "Անվանափոխել ընտրվածը" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "Նշել բոլորը" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "Ոչինչ չնշել" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "Ետարկել նշումը" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "Նշել ընտրվածը" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "Արտածել HTML-ով" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "Արտահանել CSV" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "Պահպանել արդյունքները..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "Անտեսել Հրամանի կատարումը" #: qt/result_window.py:102 msgid "Mark" msgstr "Նշել" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "Սյուները" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "Ետարկել ծրագրայինի" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} Արդյունքներ" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "Միայն կրկ." #: qt/result_window.py:194 msgid "Delta Values" msgstr "Դելտա արժեքներ" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "Ընտրեք ֆայլը՝ պահպանելու արդյունքները՝" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "Անտեսել ֆայլերը, որոնք փոքր են՝" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "ԿԲ" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ Արդյունքներ" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "Գործողությունը" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "Ավելացնել նոր թղթապանակ..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "Ընդլայնված" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "Ինքնաշխատ ստուգել թարմացումները" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "Բազային" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "Բոլորի առջևում" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "Ստուգել թարմացումները..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "Փակել պատուհանը" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "Պատճենել" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "Հրամանի կատարում (փաստարկները. %d խաբկանքի զոհ, %r հղման համար)." #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "Կտրել" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Դելտա" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "Ընտրված ֆայլի մանրամասները" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "Մանրամասների վահանակը" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "Թղթապանակներ" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "dupeGuru-ի կարգավորումները" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "dupeGuru-ի արդյունքները" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "dupeGuru-ի վեբ կայքը" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "Խմբագրել" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "Արտահանել արդյունքները CSV ձևաչափով:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "Արտածել արդյունքները XHTML-ով" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "Քիչ արդյունքներ" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "Ֆիլտրը" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "Ֆիլտրի կարգը." #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Զտել արդյունքները:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "Թղթապանակը ընտրելու պատուհանը" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "Տառատեսակի չափը ՝" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "Թաքցնել dupeGuru-ին" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "Թաքցնել մյուսները" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "Անտեսել նմանատիպ ֆայլերը." #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "Բացել ֆայլից..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "Թաքցնել" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "Եղանակը" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "Լր. արդյունքներ" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "ԼԱՎ" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "Տեղադրել" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "Կարգավորումներ..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "Quick Look" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "Փակել dupeGuru-ը" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "Ետարկել ծրագրայինի" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "Զրոյացնել լռելյայն" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "Բացահայտել" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Ցուցադրել ընտրվածը Գտնվածում" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "Ընտրել բոլորը" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Ուղարկել նշվածները Աղբարկղ..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "Առայություններ" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "Ցուցադրել բոլորը" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "Սկսել կրկնօրինակների ստուգումը" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "'%@' անունը արդեն առկա է:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "Պատուհանը" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Չափը" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "Բացառման ֆիլտրեր" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "Սկան արդյունքներ" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "բեռնվածքի տեղեկագրքեր..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "Պահել գրացուցակները..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "Ընտրեք գրացուցակների ֆայլ `բեռնելու համար" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "dupeGuru տեղեկատուներ (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "Ընտրեք ֆայլ, ձեր գրացուցակները պահելու համար" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "dupeGuru տեղեկատուներ (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "Ավելացնել" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "Վերականգնել Նախնականը" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "Թեստային լարային" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "" #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "Նկարի քեշի ռեժիմ:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" #: qt\app.py:256 msgid "Results" msgstr "Արդյունքներ" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "Ընդհանուր միջերես" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "Արդյունքների աղյուսակ" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "Մանրամասներ Պատուհան" #: qt\preferences_dialog.py:285 msgid "General" msgstr "Գեներալ" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "Ցուցադրման" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" #: qt\app.py:299 msgid "Cache cleared." msgstr "" #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "" #: qt\about_box.py:31 msgid "About {}" msgstr "{}- ի մասին" #: qt\about_box.py:47 msgid "Version {}" msgstr "{}-րդ տարբերակ" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "" #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "իցենզավորված տակ GPLv3" #: qt\about_box.py:68 msgid "No update available." msgstr "" #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "Սխալների մասին հաղորդագրություն" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "Ինչ որ բան այնպես չգնաց. Հաղորդել սխալը?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "Error հաշվետվությունները պետք է հրապարակվեն որպես Github հարցերի շուրջ: Կարող եք վերևում պատճենել սխալի հետևումը և տեղադրել այն նոր համարում:\n" "\n" "Խնդրում ենք համոզվեք, որ նախապես փնտրեք արդեն գոյություն ունեցող ցանկացած խնդիր: Նաեւ համոզվեք, որ ստուգել են հենց վերջին տարբերակը մատչելի շտեմարան, քանի որ Bug դուք ապրում գուցե արդեն patched.\n" "\n" "Սովորաբար այն, ինչ օգնում է իրականում, այն է, եթե ավելացնեք նկարագրությունը, թե ինչպես եք ստացել սխալը: Շնորհակալություն\n" "\n" "Չնայած այս սխալից հետո ծրագիրը պետք է շարունակի գործել, այն կարող է լինել անկայուն վիճակում, ուստի խորհուրդ է տրվում վերագործարկել ծրագիրը:" #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Գնացեք Գիթուբ" #: qt\preferences.py:24 msgid "Czech" msgstr "Չեխերեն" #: qt\preferences.py:25 msgid "German" msgstr "Գերմաներեն" #: qt\preferences.py:26 msgid "Greek" msgstr "հունարեն" #: qt\preferences.py:27 msgid "English" msgstr "Անգլերեն" #: qt\preferences.py:28 msgid "Spanish" msgstr "Իսպաներեն" #: qt\preferences.py:29 msgid "French" msgstr "Ֆրանսերեն" #: qt\preferences.py:30 msgid "Armenian" msgstr "հայերեն" #: qt\preferences.py:31 msgid "Italian" msgstr "Իտալերեն" #: qt\preferences.py:32 msgid "Japanese" msgstr "ճապոներեն" #: qt\preferences.py:33 msgid "Korean" msgstr "կորեերեն" #: qt\preferences.py:34 msgid "Malay" msgstr "Մալայերեն" #: qt\preferences.py:35 msgid "Dutch" msgstr "հոլանդերեն" #: qt\preferences.py:36 msgid "Polish" msgstr "լեհերեն" #: qt\preferences.py:37 msgid "Brazilian" msgstr "բրազիլական" #: qt\preferences.py:38 msgid "Russian" msgstr "ռուսերեն" #: qt\preferences.py:39 msgid "Turkish" msgstr "Թուրքերեն" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "ուկրաիներեն" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "վիետնամերեն" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "Չինարեն (Պարզեցված)" #: qt\recent.py:54 msgid "Clear List" msgstr "Մաքրել ցանկը" #: qt\search_edit.py:78 msgid "Search..." msgstr "Որոնել..." dupeguru-4.3.1/locale/it/000077500000000000000000000000001426171743600152455ustar00rootroot00000000000000dupeguru-4.3.1/locale/it/LC_MESSAGES/000077500000000000000000000000001426171743600170325ustar00rootroot00000000000000dupeguru-4.3.1/locale/it/LC_MESSAGES/columns.po000066400000000000000000000053001426171743600210500ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # msgid "" msgstr "" "Last-Translator: Andrew Senetar , 2021\n" "Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n" "Language: it\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "Percorso del file" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "Messaggio di errore" #: core\me\prioritize.py:23 msgid "Duration" msgstr "Durata" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "Bitrate" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "Campionamento" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "Nome del file" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "Cartella" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "Dimensione (MB)" #: core\me\result_table.py:22 msgid "Time" msgstr "Tempo" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "Campionamento" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "Tipo" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "Modificato" #: core\me\result_table.py:27 msgid "Title" msgstr "Titolo" #: core\me\result_table.py:28 msgid "Artist" msgstr "Artista" #: core\me\result_table.py:29 msgid "Album" msgstr "Album" #: core\me\result_table.py:30 msgid "Genre" msgstr "Genere" #: core\me\result_table.py:31 msgid "Year" msgstr "Anno" #: core\me\result_table.py:32 msgid "Track Number" msgstr "Numero traccia" #: core\me\result_table.py:33 msgid "Comment" msgstr "Commento" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "Somiglianza %" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "Parole usate" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "Conteggio duplicati" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "Dimensioni" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "Dimensione (KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "Data EXIF" #: core\prioritize.py:156 msgid "Size" msgstr "Dimensione" dupeguru-4.3.1/locale/it/LC_MESSAGES/core.po000066400000000000000000000157701426171743600203340ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # Emanuele, 2021 # msgid "" msgstr "" "Last-Translator: Emanuele, 2021\n" "Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n" "Language: it\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "Non ci sono duplicati marcati. Nessuna operazione è stata completata." #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "" "Non ci sono duplicati selezionati. Nessuna operazione è stata completata." #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" "Stai per aprire molti file contemporaneamente. A seconda di quale programma " "li aprirà, potrebbe crearsi un bel casino. Vuoi continuare?" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "Scansione per i duplicati" #: core\app.py:72 msgid "Loading" msgstr "Caricamento" #: core\app.py:73 msgid "Moving" msgstr "Spostamento" #: core\app.py:74 msgid "Copying" msgstr "Copia in corso" #: core\app.py:75 msgid "Sending to Trash" msgstr "Spostamento nel cestino" #: core\app.py:289 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" "Un'azione precedente è ancora in corso. Non puoi cominciarne una nuova. " "Aspetta qualche secondo e quindi riprova." #: core\app.py:300 msgid "No duplicates found." msgstr "Non sono stati trovati dei duplicati." #: core\app.py:315 msgid "All marked files were copied successfully." msgstr "Tutti i file marcati sono stati copiati correttamente." #: core\app.py:317 msgid "All marked files were moved successfully." msgstr "Tutti i file marcati sono stati spostati correttamente." #: core\app.py:319 msgid "All marked files were deleted successfully." msgstr "Tutti i file marcati sono stati cancellati correttamente." #: core\app.py:321 msgid "All marked files were successfully sent to Trash." msgstr "Tutti i file marcati sono stati spostati nel cestino." #: core\app.py:326 msgid "Could not load file: {}" msgstr "Impossibile caricare il file: {}" #: core\app.py:382 msgid "'{}' already is in the list." msgstr "'{}' è già nella lista." #: core\app.py:384 msgid "'{}' does not exist." msgstr "'{}' non esiste." #: core\app.py:392 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "" "Tutti i %d elementi che coincidono verranno ignorati in tutte le scansioni " "successive. Continuare?" #: core\app.py:469 msgid "Select a directory to copy marked files to" msgstr "Seleziona una directory in cui desideri copiare i file contrassegnati" #: core\app.py:471 msgid "Select a directory to move marked files to" msgstr "" "Seleziona una directory in cui desideri spostare i file contrassegnati" #: core\app.py:510 msgid "Select a destination for your exported CSV" msgstr "Seleziona una destinazione per il file CSV" #: core\app.py:516 core\app.py:771 core\app.py:781 msgid "Couldn't write to file: {}" msgstr "Impossibile modificare il file: {}" #: core\app.py:539 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" "Non hai impostato nessun comando personalizzato. Impostalo nelle tue " "preferenze." #: core\app.py:695 core\app.py:707 msgid "You are about to remove %d files from results. Continue?" msgstr "Stai per rimuovere %d file dai risultati. Continuare?" #: core\app.py:743 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} gruppi duplicati sono stati cambiati dalla nuova priorirità" #: core\app.py:790 msgid "The selected directories contain no scannable file." msgstr "Le cartelle selezionate non contengono file da scansionare." #: core\app.py:803 msgid "Collecting files to scan" msgstr "Raccolta file da scansionare" #: core\app.py:850 msgid "%s (%d discarded)" msgstr "%s (%d scartati)" #: core\directories.py:191 msgid "Collected {} files to scan" msgstr "Raccolti {} file da scansionare" #: core\directories.py:207 msgid "Collected {} folders to scan" msgstr "Raccolte {} cartelle da scansionare" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "%d corrispondeze trovate da %d gruppi" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "Stai spostando {} file al Cestino." #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "Espressioni regolari" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "" "Vuoi veramente rimuovere tutti i %d elementi dalla lista dei file da " "ignorare?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "Nome del file" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "Nome file - Campi" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "Nome file - Campi (Nessun Ordine)" #: core\me\scanner.py:23 msgid "Tags" msgstr "Tag" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "Contenuti" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "Analizzate %d/%d immagini" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "Effettuate %d/%d comparazioni sui sottogruppi di immagini" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "Preparazione per la comparazione" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "Verificate %d/%d somiglianze" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "Leggi dati EXIF da %d/%d immagini" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "Timestamp EXIF" #: core\prioritize.py:70 msgid "None" msgstr "Nessuno" #: core\prioritize.py:100 msgid "Ends with number" msgstr "Termina con un numero" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "Non termina con un numero" #: core\prioritize.py:102 msgid "Longest" msgstr "Più lungo" #: core\prioritize.py:103 msgid "Shortest" msgstr "Più corto" #: core\prioritize.py:140 msgid "Highest" msgstr "Il più alto" #: core\prioritize.py:140 msgid "Lowest" msgstr "Il più basso" #: core\prioritize.py:169 msgid "Newest" msgstr "Il più nuovo" #: core\prioritize.py:169 msgid "Oldest" msgstr "Il più vecchio" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) duplicati marcati." #: core\results.py:141 msgid " filter: %s" msgstr " filtro: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "Lettura dimensione di %d/%d file" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "Lettura metadata di %d/%d files" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "Quasi finito! Sto organizzando i risultati..." #: core\se\scanner.py:18 msgid "Folders" msgstr "Cartelle" dupeguru-4.3.1/locale/it/LC_MESSAGES/ui.po000066400000000000000000001072141426171743600200140ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2022 # Emanuele, 2022 # Fuan , 2022 # Giovanni, 2022 # msgid "" msgstr "" "Last-Translator: Giovanni, 2022\n" "Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n" "Language: it\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #: qt/app.py:81 msgid "Quit" msgstr "Esci" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "Opzioni" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Lista elementi ignorati" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Cancella la cache delle immagini" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Aiuto di dupeGuru" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "Informazioni su dupeGuru" #: qt/app.py:87 msgid "Open Debug Log" msgstr "Apri registro eventi" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "" "Vuoi veramente rimuovere tutte le analisi delle immagini memorizzate nella " "cache?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "La cache delle immagini è stata cancellata." #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} file (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Opzioni di eliminazione" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Collega file eliminati" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" "Dopo aver selezionato un duplicato, per sostituire il file eliminato " "posiziona un collegamento che ha come destinazione i file di referenza." #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "Hardlink" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "Symlink" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr " (non supportato)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Elimina file direttamente" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "Invece di spostare file nel cestino, eliminali direttamente. Questa opzione " "di solito è usata come alternativa al sistema di eliminazione standard " "quando non funziona." #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Procedi" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Annulla" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "Attributo" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "Selezionato" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "Riferimento" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "Caricamento risultati..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "Finestra dei risultati" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "Aggiungi Cartella..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "File" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "Visualizzazione" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "Aiuto" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "Carica i risultati recenti" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "Modalità applicazione:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "Musica" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "Foto" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "Standard" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "Tipo di scansione:" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "Più Opzioni" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "Seleziona le cartelle da scansionare e premi \"Scansiona\"." #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "Carica i risultati" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "Scansiona" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "Risultati non salvati" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "Hai dei risultati non salvati. Vuoi veramente chiudere?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "" "Seleziona una cartella da aggiungere alla lista delle cartelle da " "scansionare" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "Seleziona un risultato (file) da caricare" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "Tutti i file (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "Risultati dupeGuru (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "Inizia una nuova scansione" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "Hai dei risultati non salvati. Vuoi veramente continuare?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "Nome" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "Stato" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "Escluso" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "Normale" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "Rimuovi Selezionati" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "Deseleziona" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "Chiudi" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "Dettagli" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "Etichette da scansionare:" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "Traccia" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "Artista" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "Album" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "Titolo" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "Genere" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "Anno" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "'Peso' della parola" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "Fai coincidere parole simili" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "Includi tipi diversi di file" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "Usa le espressioni regolari per filtrare" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "Rimuovi le cartelle vuote dopo aver cancellato o spostato" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "Non considerare gli hardlink come duplicati" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "Modalità 'Debug'(è richiesta la riapertura del programma)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "Includi immagini di dimensione differente" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "Durezza del filtro:" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "Più risultati" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "Meno risultati" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "Dimensione carattere:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "Lingua:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "Copia e sposta:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "Tutti gli elementi in una cartella" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "Ricrea il percorso relativo" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "Ricrea il percorso assoluto" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "" "Comando personalizzato (argomenti: %d per duplicare, %r per riferimento):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "Per cambiare lingua, dupeGuru deve riavviarsi." #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "Cambia la priorità dei duplicati" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "Aggiungi dei criteri di selezione nel riquadro a destra e clicca su 'OK' per" " inviare i duplicati che meglio corrispondono a questi criteri al loro " "gruppo di appartenenza. Per maggiori informazioni leggere il file di 'help'." #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "Problemi!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "Sono stati riscontrati dei problemi elaborando alcuni (o tutti) i file. La " "causa di questi problemi è descritta nella tabella sottostante. Questi file " "non stati rimossi dai vostri risultati." #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "Mostra i selezionati" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "Azioni" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "Visualizza solo i duplicati" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "Visualizza le differenze" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "Sposta elementi marcati nel Cestino..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "Sposta elementi marcati nel..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "Copia elementi evidenziati nel..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "Rimuovi gli elementi marcati dai risultati" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "Cambia la priorità dei risultati..." #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "Rimuovi gli elementi selezionati dai risultati" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "Aggiungi gli elementi selezionati alla lista da ignorare" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "Rendi selezionato un Riferimento" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "Apri gli elementi selezionati con l'applicazione predefinita" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "Apri cartella degli elementi selezionati" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "Rinomina gli elementi selezionati" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "Marca tutti" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "Deseleziona tutti" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "Inverti la selezione" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "Marca i selezionati" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "Esporta in HTML" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "Esporta in CSV" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "Salva i risultati..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "Invoca comando personalizzato" #: qt/result_window.py:102 msgid "Mark" msgstr "Marca" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "Colonne" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "Ripristina impostazioni predefinite" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} Resultati" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "Solo duplicati" #: qt/result_window.py:194 msgid "Delta Values" msgstr "Valori Delta" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "Seleziona un file dove salvare i tuoi risultati" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "Ignora file più piccoli di" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ Resultati" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "Azione" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "Aggiungi una nuova cartella..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "Avanzato" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "Controlla gli aggiornamenti automaticamente" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "Base" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "Porta tutto in primo piano" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "Controlla gli aggiornamenti..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "Chiudi la finestra" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "Copia" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "" "Comando personalizzato (argumenti: %d per duplicare, %r per riferimento):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "Taglia" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Delta" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "Dettagli del file selezionato" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "Scheda dettagliata" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "Cartelle" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "Preferenze di dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "Risultati di dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "Sito Web di dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "Modifica" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "Esporta risultati in CSV" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "Esporta i risultati in formato XHTML" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "Meno risultati" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "Filtro" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "Durezza del filtro:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Filtra Risultati..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "Finestra di selezione della cartella" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "Dimensione Carattere:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "Nascondi dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "Nascondi gli altri" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "Ignora file più piccoli di:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "Carica dal file..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "Minimizza" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "Modo" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "Più risultati" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "Ok" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "Incolla" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "Preferenze..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "Sguardo rapido" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "Chiudi dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "Ripristina le impostazioni predefinite" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "Imposta prefefinite" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "Rivelare" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Mostra gli elementi selezionati nel Finder" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "Seleziona tutto" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Sposta gli elementi marcati nel cestino..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "Servizi" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "Mostra tutto" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "Inizia la scansione dei duplicati" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "Il nome '%@' è già esistente." #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "Finestra" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Zoom" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "Filtri di esclusione" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "Risultati della scansione" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "Carica cartelle..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "Salva cartelle..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "Seleziona un file delle cartelle da caricare" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "cartelle di dupeGuru (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "Seleziona un file in cui salvare le cartelle" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "cartelle di dupeGuru (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "Addizionare" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "Ripristina i valori predefiniti" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "Stringa di prova" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "Digita qui un'espressione regolare Python..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "Digitare un percorso del file system o un nome file qui..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "These (case sensitive) python regular expressions will filter out files during scans.
    I direttori avranno anche il loro stato predefinito impostato su Escluso nella scheda Directory se il loro nome corrisponde a una delle espressioni regolari selezionate.
    Per ogni file raccolto vengono eseguiti due test per determinare se ignorarlo o meno completamente:
  • 1. Le espressioni regolari prive di separatori di percorso verranno confrontate solo con il nome del file.
  • \n" "
  • 2. Le espressioni regolari contenenti almeno un separatore di percorso verranno confrontate con il percorso completo del file.

  • \n" "Esempio: se desideri filtrare i file .PNG solo dalla directory \"Mie Immagini\":
    .*Mie\\sImmagini\\\\.*\\.png

    Puoi testare l'espressione regolare con il pulsante \"stringa di prova\" dopo aver incollato un percorso falso nel campo di prova:
    C:\\\\Utente\\Mie Immagini\\test.png

    \n" "Verranno evidenziate le espressioni regolari corrispondenti.
    Se è presente almeno un'evidenziazione, il percorso o il nome del file testato verrà ignorato durante le scansioni.

    Directory e file che iniziano con un punto \".\" vengono filtrati per impostazione predefinita.

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "Errore di compilazione:" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "Aumenta lo zoom" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "Diminuisci lo zoom" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "Dimensione normale" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "Il più adatto" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "Modalità cache immagini:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "" "Ignora le icone del tema nella barra degli strumenti del visualizzatore" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" "Usa le nostre icone interne invece di quelle fornite dal motore del tema" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "Mostra le barre di scorrimento nei visualizzatori di immagini" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" "Quando l'immagine visualizzata non si adatta alla visualizzazione, mostra le" " barre di scorrimento per estendere la visualizzazione" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "" "Usa la posizione predefinita per la barra di tabulazione (richiede il " "riavvio)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "Posiziona la barra di tabulazione sotto il menu principale anziché accanto ad esso\n" "Su MacOS, la barra delle schede riempirà invece la larghezza della finestra." #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "Usa caratteri in grassetto per i riferimenti" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "Colore di primo piano per i riferimenti:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "Colore di sfondo per i riferimenti:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "colore di primo piano per i delta:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "Mostra la barra del titolo e può essere agganciata" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" "Mentre la barra del titolo è nascosta, usa il tasto modificatore per " "trascinare la finestra mobile" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "" "La barra del titolo può essere disabilitata solo mentre la finestra è " "agganciata" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "Barra del titolo verticale" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "" "Cambia la barra del titolo da orizzontale in alto a verticale sul lato " "sinistro" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "Mostra la barra di tabulazione" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "These (case sensitive) python regular expressions will filter out files during scans.
    I direttori avranno anche il loro stato predefinito impostato su Escluso nella scheda Directory se il loro nome corrisponde a una delle espressioni regolari selezionate.
    Per ogni file raccolto vengono eseguiti due test per determinare se ignorarlo o meno completamente:
  • 1. Le espressioni regolari prive di separatori di percorso verranno confrontate solo con il nome del file.
  • \n" "
  • 2. Le espressioni regolari contenenti almeno un separatore di percorso verranno confrontate con il percorso completo del file.

  • \n" "Esempio: se desideri filtrare i file .PNG solo dalla directory \"Mie Immagini\":
    .*Mie\\sImmagini\\\\.*\\.png

    Puoi testare l'espressione regolare con il pulsante \"stringa di prova\" dopo aver incollato un percorso falso nel campo di prova:
    C:\\\\Utente\\Mie Immagini\\test.png

    \n" "Verranno evidenziate le espressioni regolari corrispondenti.
    Se è presente almeno un'evidenziazione, il percorso o il nome del file testato verrà ignorato durante le scansioni.

    Directory e file che iniziano con un punto \".\" vengono filtrati per impostazione predefinita.

    " #: qt\app.py:256 msgid "Results" msgstr "Risultati" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "Interfaccia generale" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "Tabella dei risultati" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "Finestra Dettagli" #: qt\preferences_dialog.py:285 msgid "General" msgstr "Generale" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "Schermo" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "Calcola hash parziale di file più grandi di" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "MB" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "Usa le finestre di dialogo native del Sistema Operativo" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" "Per azioni come selezione di file/cartelle usa le finestre di dialogo native del Sistema Operativo.\n" "Alcune finestre di dialogo native hanno funzionalità limitate." #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "Ignora file più grandi di" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "Svuota cache" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" "Vuoi davvero svuotare la cache? Ciò rimuoverà tutti gli hash dei file " "memorizzati nella cache e le analisi delle immagini." #: qt\app.py:299 msgid "Cache cleared." msgstr "Cache svuotata" #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "Usa stile scuro" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "Profila l'operazione di scansione" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" "Profila l'operazione di scansione e salva i registri per l'ottimizzazione." #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "I log si trovano in: {}" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "Debug" #: qt\about_box.py:31 msgid "About {}" msgstr "A proposito di {}" #: qt\about_box.py:47 msgid "Version {}" msgstr "Versione {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "Controllo degli aggiornamenti..." #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "Distribuito sotto licenza GPLv3" #: qt\about_box.py:68 msgid "No update available." msgstr "Nessun aggiornamento disponibile." #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "È disponibile la nuova versione {}, scaricabile qui." #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "Rapporto di errore" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "Qualcosa è andato storto. Che ne dici di segnalare l'errore?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "I rapporti di errore dovrebbero essere segnalati come problemi di Github. Puoi copiare il traceback degli errori sopra e incollarlo in un nuovo numero.\n" "\n" "Assicurati di eseguire prima una ricerca per eventuali problemi già esistenti. Assicurati anche di testare l'ultima versione disponibile dal repository, poiché il bug che stai riscontrando potrebbe essere già stato corretto.\n" "\n" "Ciò che di solito aiuta davvero è aggiungere una descrizione di come hai ottenuto l'errore. Grazie!\n" "\n" "Sebbene l'applicazione debba continuare a essere eseguita dopo questo errore, potrebbe essere in uno stato instabile, quindi si consiglia di riavviare l'applicazione." #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Apri in Github" #: qt\preferences.py:24 msgid "Czech" msgstr "Ceco" #: qt\preferences.py:25 msgid "German" msgstr "Tedesco" #: qt\preferences.py:26 msgid "Greek" msgstr "Greco" #: qt\preferences.py:27 msgid "English" msgstr "Inglese" #: qt\preferences.py:28 msgid "Spanish" msgstr "Spagnolo" #: qt\preferences.py:29 msgid "French" msgstr "Francese" #: qt\preferences.py:30 msgid "Armenian" msgstr "Armeno" #: qt\preferences.py:31 msgid "Italian" msgstr "Italiano" #: qt\preferences.py:32 msgid "Japanese" msgstr "Giapponese" #: qt\preferences.py:33 msgid "Korean" msgstr "Coreano" #: qt\preferences.py:34 msgid "Malay" msgstr "Malese" #: qt\preferences.py:35 msgid "Dutch" msgstr "Olandese" #: qt\preferences.py:36 msgid "Polish" msgstr "Polacco" #: qt\preferences.py:37 msgid "Brazilian" msgstr "Brasiliano" #: qt\preferences.py:38 msgid "Russian" msgstr "Russo" #: qt\preferences.py:39 msgid "Turkish" msgstr "Turco" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "Ucraino" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "Vietnamita" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "Cinese (semplificato)" #: qt\recent.py:54 msgid "Clear List" msgstr "Cancellare l'elenco" #: qt\search_edit.py:78 msgid "Search..." msgstr "Ricerca..." dupeguru-4.3.1/locale/ja/000077500000000000000000000000001426171743600152235ustar00rootroot00000000000000dupeguru-4.3.1/locale/ja/LC_MESSAGES/000077500000000000000000000000001426171743600170105ustar00rootroot00000000000000dupeguru-4.3.1/locale/ja/LC_MESSAGES/columns.po000066400000000000000000000054311426171743600210330ustar00rootroot00000000000000# Translators: # Fuan , 2021 # msgid "" msgstr "" "Last-Translator: Fuan , 2021\n" "Language-Team: Japanese (https://www.transifex.com/voltaicideas/teams/116153/ja/)\n" "Language: ja\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "ファイルパス" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "エラーメッセージ" #: core\me\prioritize.py:23 msgid "Duration" msgstr "デュレーション" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "ビットレート" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "サンプルレート" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "ファイル名" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "フォルダ" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "サイズ(MB)" #: core\me\result_table.py:22 msgid "Time" msgstr "デュレーション" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "サンプルレート" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "種類" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "変更" #: core\me\result_table.py:27 msgid "Title" msgstr "タイトル" #: core\me\result_table.py:28 msgid "Artist" msgstr "アーティスト" #: core\me\result_table.py:29 msgid "Album" msgstr "アルバム" #: core\me\result_table.py:30 msgid "Genre" msgstr "ジャンル" #: core\me\result_table.py:31 msgid "Year" msgstr "年" #: core\me\result_table.py:32 msgid "Track Number" msgstr "トラック番号" #: core\me\result_table.py:33 msgid "Comment" msgstr "コメント" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "一致率" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "使用した単語" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "重複カウント" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "寸法" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "サイズ(KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "EXIFタイムスタンプ" #: core\prioritize.py:156 msgid "Size" msgstr "サイズ" dupeguru-4.3.1/locale/ja/LC_MESSAGES/core.po000066400000000000000000000166701426171743600203120ustar00rootroot00000000000000# Translators: # Yuji Sasaki, 2022 # Fuan , 2022 # msgid "" msgstr "" "Last-Translator: Fuan , 2022\n" "Language-Team: Japanese (https://www.transifex.com/voltaicideas/teams/116153/ja/)\n" "Language: ja\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: core\app.py:44 msgid "There are no marked duplicates. Nothing has been done." msgstr "チェックを入れた重複はありません。 何も行われませんでした。" #: core\app.py:45 msgid "There are no selected duplicates. Nothing has been done." msgstr "選択された重複はありません。 何も行われていません。" #: core\app.py:46 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "一度に多くのファイルを開こうとしています。 これらのファイルを開く対象によっては、これを行うとかなり混乱する可能性があります。 継続する?" #: core\app.py:73 msgid "Scanning for duplicates" msgstr "重複のスキャン" #: core\app.py:74 msgid "Loading" msgstr "読み込み中" #: core\app.py:75 msgid "Moving" msgstr "移動します" #: core\app.py:76 msgid "Copying" msgstr "コピー中" #: core\app.py:77 msgid "Sending to Trash" msgstr "ごみ箱に送信します" #: core\app.py:291 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "前のアクションはまだそこにぶら下がっています。 まだ新しいものを始めることはできません。 数秒待ってから、再試行してください。" #: core\app.py:302 msgid "No duplicates found." msgstr "重複は見つかりませんでした。" #: core\app.py:317 msgid "All marked files were copied successfully." msgstr "チェックを入れたファイルをすべてコピーしました。" #: core\app.py:319 msgid "All marked files were moved successfully." msgstr "チェックを入れたファイルをすべて移動しました。" #: core\app.py:321 msgid "All marked files were deleted successfully." msgstr "チェックを入れたファイルをすべて削除しました。" #: core\app.py:323 msgid "All marked files were successfully sent to Trash." msgstr "チェックを入れたファイルをすべてごみ箱に移動しました。" #: core\app.py:328 msgid "Could not load file: {}" msgstr "ファイルを読み込めませんでした:{}" #: core\app.py:384 msgid "'{}' already is in the list." msgstr "「{}」既にリストに含まれています。" #: core\app.py:386 msgid "'{}' does not exist." msgstr "'{}' 存在しません。" #: core\app.py:394 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "選択した%d個の一致は、以降のすべてのスキャンで無視されます。 継続する?" #: core\app.py:471 msgid "Select a directory to copy marked files to" msgstr "マークされたファイルをコピーするディレクトリを選択してください" #: core\app.py:473 msgid "Select a directory to move marked files to" msgstr "マークされたファイルを移動するディレクトリを選択してください" #: core\app.py:512 msgid "Select a destination for your exported CSV" msgstr "エクスポートしたCSVの宛先を選択します。" #: core\app.py:518 core\app.py:773 core\app.py:783 msgid "Couldn't write to file: {}" msgstr "ファイルに書き込めませんでした:{}" #: core\app.py:541 msgid "You have no custom command set up. Set it up in your preferences." msgstr "カスタムコマンドは設定されていません。 お好みで設定してください。" #: core\app.py:697 core\app.py:709 msgid "You are about to remove %d files from results. Continue?" msgstr "結果から%d個のファイルを削除しようとしています。 継続する?" #: core\app.py:745 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{}重複するグループは、再優先順位付けによって変更されました。" #: core\app.py:792 msgid "The selected directories contain no scannable file." msgstr "選択したディレクトリにはスキャン可能なファイルが含まれていません。" #: core\app.py:808 msgid "Collecting files to scan" msgstr "スキャンするファイルを収集しています" #: core\app.py:858 msgid "%s (%d discarded)" msgstr "%s (%d 廃棄)" #: core\directories.py:190 msgid "Collected {} files to scan" msgstr "" #: core\directories.py:206 msgid "Collected {} folders to scan" msgstr "" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "{}個のファイルをゴミ箱に送信しています" #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "正規表現" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "本当に除外リストから%d個の項目を削除しますか?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "ファイル名" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "ファイル名 - フィールド" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "ファイル名 - フィールド(順序なし)" #: core\me\scanner.py:23 msgid "Tags" msgstr "タグ" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "内容" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "%d/%d 枚の写真を分析しました" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "チャンクマッチを%d/%d回実行しました" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "マッチングの準備" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "%d/%d件の一致を確認" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "%d/%d枚の写真のEXIFを読みました" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "EXIFタイムスタンプ" #: core\prioritize.py:70 msgid "None" msgstr "無し" #: core\prioritize.py:102 msgid "Ends with number" msgstr "番号で終わっている" #: core\prioritize.py:103 msgid "Doesn't end with number" msgstr "数字で終わっていない" #: core\prioritize.py:104 msgid "Longest" msgstr "最長" #: core\prioritize.py:105 msgid "Shortest" msgstr "最短" #: core\prioritize.py:142 msgid "Highest" msgstr "最高" #: core\prioritize.py:142 msgid "Lowest" msgstr "最低" #: core\prioritize.py:171 msgid "Newest" msgstr "最新" #: core\prioritize.py:171 msgid "Oldest" msgstr "最古" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s)マークされた重複。" #: core\results.py:141 msgid " filter: %s" msgstr "フィルタ: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "%d/%dファイルのサイズを読み取った" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "%d/%dファイルのメタデータを読み取った" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "ほぼ完了しました! 結果をいじっています..." #: core\se\scanner.py:18 msgid "Folders" msgstr "フォルダー" dupeguru-4.3.1/locale/ja/LC_MESSAGES/ui.po000066400000000000000000001103341426171743600177670ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2022 # Fuan , 2022 # msgid "" msgstr "" "Last-Translator: Fuan , 2022\n" "Language-Team: Japanese (https://www.transifex.com/voltaicideas/teams/116153/ja/)\n" "Language: ja\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: qt/app.py:81 msgid "Quit" msgstr "終了" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "オプション" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "無視リスト" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "画像キャッシュをクリア" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "dupeGuruヘルプ" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "dupeGuruついて" #: qt/app.py:87 msgid "Open Debug Log" msgstr "デバッグログを開く" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "キャッシュされた画像分析をすべて削除しますか?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "画像キャッシュがクリアされました。" #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} ファイル (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "削除オプション" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "削除されたファイルをリンクする" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "重複を削除した後、参照ファイルをターゲットとするリンクを配置して、削除されたファイルを置き換えます。" #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "ハードリンク" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "シンボリックリンク" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr "(非対応)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "ファイルを完全に削除" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "ファイルを直接削除するファイルをゴミ箱に送る代わりに、直接削除します。 このオプションは通常、通常の削除方法が機能しない場合の回避策として使用されます。" #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "続行" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "キャンセル" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "属性" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "選択した" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "参照" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "結果をロード..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "結果ウィンドウ" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "フォルダーを追加" #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "ファイル" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "ビュー" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "ヘルプ" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "最近の結果を読み込む" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "アプリケーションモード:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "音楽" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "画像" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "標準" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "スキャンの種類:" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "詳細設定" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "スキャンするフォルダを選択し、「スキャン」を押してください。" #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "結果を読み込む" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "スキャン" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "未保存の結果" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "保存されていない結果がありますが、本当に終了しますか?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "スキャンリストに追加するフォルダを選択してください" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "ロードする結果ファイルを選択してください" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "すべてのファイル (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "dupeGuruの結果 (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "新しいスキャンを開始" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "保存されていない結果がありますが、本当に続行しますか?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "名" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "状態" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "除外" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "正常" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "選択を削除" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "クリアー" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "閉じる" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "詳細" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "スキャンするタグ:" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "トラック" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "アーティスト" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "アルバム" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "タイトル" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "ジャンル" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "年" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "単語の重み付け" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "類似の単語を一致" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "ファイルの種類を混在" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "フィルタに正規表現を使用" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "削除や移動で空になったフォルダを削除" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "同じファイルへの重複ハードリンクを無視" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "デバッグモード(再起動が必要)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "異なるサイズの写真を一致" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "フィルタの強さ:" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "より多くの結果" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "より少ない結果" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "文字サイズ:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "言語:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "コピーと移動:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "目的地で直接" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "相対パスを再作成" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "絶対パスを再作成" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "カスタムコマンド (引数: %dは重複・%rは参照):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "言語の変更を有効にするには、dupeGuruを再起動する必要があります。" #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "重複を再優先する" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "右側のボックスに基準を追加し、[OK]をクリックして、これらの基準に最もよく対応する複製をそれぞれのグループの参照位置に送信します。 " "詳細については、ヘルプファイルをお読みください。" #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "問題!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "一部(またはすべて)のファイルの処理に問題がありました。 これらの問題の原因を次の表に示します。 これらのファイルは結果から削除されませんでした。" #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "選択したものを明らかにする" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "作用" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "複製のみを表示" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "デルタ値を表示" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "マークされたものをごみ箱に送る..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "マークされたものを移動..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "マークされたものをコピー..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "結果からマークされたものを削除" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "結果を再優先する..." #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "結果から選択したアイテムを削除" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "選択したアイテムを無視リストに追加" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "選択したアイテムを参照にする" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "デフォルトのアプリケーションで選択を開く" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "選択したアイテムのコンテナフォルダを開く" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "選択したアイテムの名前を変更する" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "すべてのアイテムをマークする" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "何もマークしない" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "マーキングを反転" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "選択したアイテムにマークを付け" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "HTMLにエクスポート" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "CSVにエクスポート" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "結果を保存する" #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "カスタムコマンドを呼び出す" #: qt/result_window.py:102 msgid "Mark" msgstr "マーク" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "コラム" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "既定値にリセット" #: qt/result_window.py:185 msgid "{} Results" msgstr "{}結果" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "複製のみ" #: qt/result_window.py:194 msgid "Delta Values" msgstr "デルタ値" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "結果を保存するファイルを選択してください" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "より小さいファイルは無視" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ 結果" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "作用" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "新しいフォルダを追加..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "高度" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "更新を自動的にチェック" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "基本" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "すべてを前面に出す" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "更新を確認..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "ウィンドウを閉じる" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "コピー" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "カスタムコマンド (引数:重複の場合は%d、参照の場合は%r):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "カット" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "デルタ" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "選択したファイルの詳細" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "詳細パネル" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "ディレクトリ" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "dupeGuruの設定" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "dupeGuruの結果" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "dupeGuruウェブサイト" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "編集" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "結果をCSVにエクスポート" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "結果をXHTMLにエクスポート" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "より少ない結果" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "フィルタ" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "フィルター硬度:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "結果のフィルタリング" #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "フォルダ選択ウィンドウ" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "フォントサイズ" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "dupeGuruを隠す" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "他を隠す" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "以下よりも小さいファイルは無視:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "ファイルからロード..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "最小化する" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "モード" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "より多くの結果" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "Ok" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "ペースト" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "環境設定" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "クイックルック" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "dupeGuruを終了" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "デフォルトにリセット" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "既定値にリセット" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "表す" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Finderで選択したものを表示" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "すべて選択" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "マークされたアイテムをゴミ箱に送る..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "サービス" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "すべて表示" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "重複スキャンを開始" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "'%@'名はすでに存在します。" #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "ウィンドウ" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "拡大" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "除外フィルター" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "スキャン結果" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "ディレクトリをロード..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "ディレクトリを保存..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "ロードするディレクトリファイルを選択してください" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "dupeGuruのディレクトリファイル (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "ディレクトリを保存するファイルを選択してください。" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "dupeGuruのディレクトリファイル (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "追加" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "デフォルトに戻す" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "テスト文字列" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "ここではPythonの正規表現を入力して..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "ここにファイルシステムのパスまたはファイル名を入力してください..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "これらの(大文字と小文字を区別する)Python正規表現は、スキャン中にファイルを除外します。
    ディレクターの名前が正規表現の1つと一致する場合、ディレクトリのデフォルト状態は[ディレクトリ]タブで[除外]に設定されます。
    収集されたファイルごとに、2つのテストがそれぞれに対して実行され、それらをフィルターで除外するかどうかが決定されます:
  • 1.パス区切り文字が含まれていない正規表現は、ファイル名のみと比較されます。
  • \n" "
  • 2.パス区切り文字が含まれていない正規表現は、ファイルへのフルパスと比較されます。
  • \n" "例:「My Pictures」ディレクトリからのみ.PNGファイルを除外する場合:
    .*My\\sPictures\\\\.*\\.png

    偽のパスを貼り付けることで、テスト文字列機能を使用して正規表現をテストできます:
    C:\\\\User\\My Pictures\\test.png

    \n" "一致する正規表現が強調表示されます。
    ハイライトが少なくとも1つある場合、テストされたパスはスキャン中に無視されます。

    ピリオド「。」で始まるディレクトリとファイル デフォルトでは除外されます。

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "コンパイルエラー:" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "ズーム増やす" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "ズームを小さく" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "通常サイズ" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "最適" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "画像キャッシュモード:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "ビューアツールバーのテーマアイコンを上書きする" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "テーマエンジンによって提供されるアイコンの代わりに、独自の内部アイコンを使用します。" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "画像ビューアにスクロールバーを表示する" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "表示された画像がビューポートに適合しない場合に、ビュー全体にスクロールバーを表示する。" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "タブバーのデフォルトの位置を使用する(再起動が必要)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "タブバーをメインメニューの横ではなく下に配置する。\n" "MacOSでは、代わりにタブバーがウィンドウの幅を埋める。" #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "参照のために太字を使用する" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "参照の前景色:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "参照の背景色:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "デルタ値の背景色:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "タイトルバーを表示し、ドッキングできます。" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "タイトルバーが非表示になっているときに、修飾キーを使用してフローティングウィンドウをドラッグできます。" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "タイトルバーは、ウィンドウがドッキングされている間のみ無効にできます。" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "垂直タイトルバー" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "タイトルバーを上部の水平から左側の垂直に変更する" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "タブバーを表示" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "これらの(大文字と小文字を区別する)Python正規表現は、スキャン中にファイルを除外します。
    また、ディレクトリの名前が選択した正規表現の1つと一致する場合、ディレクトリのデフォルト状態は[ディレクトリ]タブで「除外」に設定されます。
    収集された各ファイルのために、二つの試験は完全にそれを無視するか否かを決定するために実行される:
  • 1.パス区切り文字が含まれていない正規表現は、ファイル名のみと比較されます。
  • \n" "
  • 2.少なくとも1つのパス区切り文字を含む正規表現は、ファイルへのフルパスと比較されます。
  • \n" "
    例:「My Pictures」ディレクトリからのみ.PNGファイルを除外する場合:
    .*My\\sPictures\\\\.*\\.png

    テストフィールドに偽のパスを貼り付けた後、[テスト文字列]ボタンを使用して正規表現をテストできます:
    C:\\\\User\\My Pictures\\test.png

    \n" "一致する正規表現が強調表示されます。
    ハイライトが少なくとも1つある場合、テストされたパスまたはファイル名はスキャン中に無視されます。

    ピリオド「.」で始まるディレクトリとファイル デフォルトでは除外されます。

    " #: qt\app.py:256 msgid "Results" msgstr "結果" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "一般" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "結果" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "詳細画面" #: qt\preferences_dialog.py:285 msgid "General" msgstr "一般" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "表示" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" #: qt\app.py:299 msgid "Cache cleared." msgstr "" #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "" #: qt\about_box.py:31 msgid "About {}" msgstr "{}について" #: qt\about_box.py:47 msgid "Version {}" msgstr "バージョン {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "" #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "GPLv3のもとでライセンスされています" #: qt\about_box.py:68 msgid "No update available." msgstr "" #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "エラーレポート" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "不明な理由により失敗しました。問題を報告しませんか?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "エラーレポートはGithubの問題として報告する必要があります。 上記のエラートレースバックをコピーして、新しい問題に貼り付けることができます。\n" "\n" "事前に既存の問題を検索してください。 また、発生しているバグにはすでにパッチが適用されている可能性があるため、リポジトリから入手できる最新バージョンをテストしてください。\n" "\n" "通常本当に役立つのは、エラーが発生した方法の説明を追加することです。 ありがとう!\n" "\n" "このエラーの後もアプリケーションは実行を継続するはずですが、不安定な状態になっている可能性があるため、アプリケーションを再起動することをお勧めします。" #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Githubに移動" #: qt\preferences.py:24 msgid "Czech" msgstr "チェコ語" #: qt\preferences.py:25 msgid "German" msgstr "ドイツ語" #: qt\preferences.py:26 msgid "Greek" msgstr "ギリシャ語" #: qt\preferences.py:27 msgid "English" msgstr "英語" #: qt\preferences.py:28 msgid "Spanish" msgstr "スペイン語" #: qt\preferences.py:29 msgid "French" msgstr "フランス語" #: qt\preferences.py:30 msgid "Armenian" msgstr "アルメニア語" #: qt\preferences.py:31 msgid "Italian" msgstr "イタリア語" #: qt\preferences.py:32 msgid "Japanese" msgstr "日本語" #: qt\preferences.py:33 msgid "Korean" msgstr "韓国語" #: qt\preferences.py:34 msgid "Malay" msgstr "マレー語" #: qt\preferences.py:35 msgid "Dutch" msgstr "オランダ語" #: qt\preferences.py:36 msgid "Polish" msgstr "ポーランド語" #: qt\preferences.py:37 msgid "Brazilian" msgstr "ブラジル語" #: qt\preferences.py:38 msgid "Russian" msgstr "ロシア語" #: qt\preferences.py:39 msgid "Turkish" msgstr "トルコ語" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "ウクライナ語" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "ベトナム語" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "中国語(簡体字)" #: qt\recent.py:54 msgid "Clear List" msgstr "リストをクリア" #: qt\search_edit.py:78 msgid "Search..." msgstr "探索..." dupeguru-4.3.1/locale/ko/000077500000000000000000000000001426171743600152425ustar00rootroot00000000000000dupeguru-4.3.1/locale/ko/LC_MESSAGES/000077500000000000000000000000001426171743600170275ustar00rootroot00000000000000dupeguru-4.3.1/locale/ko/LC_MESSAGES/columns.po000066400000000000000000000052751426171743600210600ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Sangdon Lim, 2022 # msgid "" msgstr "" "Last-Translator: Sangdon Lim, 2022\n" "Language-Team: Korean (https://www.transifex.com/voltaicideas/teams/116153/ko/)\n" "Language: ko\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "파일 경로" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "에러 메시지" #: core\me\prioritize.py:23 msgid "Duration" msgstr "길이" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "비트레이트" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "샘플레이트" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:94 #: core\se\result_table.py:19 msgid "Filename" msgstr "폴더명" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "폴더" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "크기 (MB)" #: core\me\result_table.py:22 msgid "Time" msgstr "시간" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "샘플레이트" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "종류" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:165 core\se\result_table.py:23 msgid "Modification" msgstr "수정날짜" #: core\me\result_table.py:27 msgid "Title" msgstr "곡명" #: core\me\result_table.py:28 msgid "Artist" msgstr "아티스트" #: core\me\result_table.py:29 msgid "Album" msgstr "앨범" #: core\me\result_table.py:30 msgid "Genre" msgstr "장르" #: core\me\result_table.py:31 msgid "Year" msgstr "년도" #: core\me\result_table.py:32 msgid "Track Number" msgstr "트랙 번호" #: core\me\result_table.py:33 msgid "Comment" msgstr "주석" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "일치정도" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "사용된 단어수" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "중복파일 갯수" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "치수" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "크기 (KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "EXIF 타임스탬프" #: core\prioritize.py:158 msgid "Size" msgstr "크기" dupeguru-4.3.1/locale/ko/LC_MESSAGES/core.po000066400000000000000000000161541426171743600203260ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # Sangdon Lim, 2022 # msgid "" msgstr "" "Last-Translator: Sangdon Lim, 2022\n" "Language-Team: Korean (https://www.transifex.com/voltaicideas/teams/116153/ko/)\n" "Language: ko\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: core\app.py:44 msgid "There are no marked duplicates. Nothing has been done." msgstr "표시된 중복 항목이 없습니다. 아무것도하지 않았습니다." #: core\app.py:45 msgid "There are no selected duplicates. Nothing has been done." msgstr "선택한 중복 항목이 없습니다. 아무것도하지 않았습니다." #: core\app.py:46 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "한 번에 많은 파일을 열려고 합니다. 시스템 설정에 따라 너무 많은 프로그램이 실행될 수도 있습니다. 진행할까요?" #: core\app.py:73 msgid "Scanning for duplicates" msgstr "중복 검색" #: core\app.py:74 msgid "Loading" msgstr "불러오는중" #: core\app.py:75 msgid "Moving" msgstr "이동중" #: core\app.py:76 msgid "Copying" msgstr "복사중" #: core\app.py:77 msgid "Sending to Trash" msgstr "휴지통으로 보내기" #: core\app.py:291 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "이전 작업이 아직 진행 중이어서 새 작업을 시작할 수 없습니다. 몇 초 후에 다시 시도해 보세요." #: core\app.py:302 msgid "No duplicates found." msgstr "중복 파일이 없습니다." #: core\app.py:317 msgid "All marked files were copied successfully." msgstr "표시된 모든 파일이 성공적으로 복사되었습니다." #: core\app.py:319 msgid "All marked files were moved successfully." msgstr "표시된 모든 파일이 성공적으로 이동되었습니다." #: core\app.py:321 msgid "All marked files were deleted successfully." msgstr "표시된 모든 파일이 성공적으로 제거되었습니다." #: core\app.py:323 msgid "All marked files were successfully sent to Trash." msgstr "표시된 모든 파일이 성공적으로 휴지통으로 전송되었습니다." #: core\app.py:328 msgid "Could not load file: {}" msgstr "파일을로드 할 수 없습니다 : {}" #: core\app.py:384 msgid "'{}' already is in the list." msgstr "'{}' 는 이미 목록에 있습니다." #: core\app.py:386 msgid "'{}' does not exist." msgstr "'{}' 가 존재하지 않습니다." #: core\app.py:394 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "선택된 %d개의 일치 항목은 모든 후속 검색에서 무시됩니다. 계속하다?" #: core\app.py:471 msgid "Select a directory to copy marked files to" msgstr "표시된 파일을 복사 할 디렉토리를 선택하십시오" #: core\app.py:473 msgid "Select a directory to move marked files to" msgstr "표시된 파일을 이동할 디렉토리를 선택하십시오" #: core\app.py:512 msgid "Select a destination for your exported CSV" msgstr "내 보낸 CSV의 대상을 선택하십시오" #: core\app.py:518 core\app.py:773 core\app.py:783 msgid "Couldn't write to file: {}" msgstr "파일에 쓸 수 없습니다 : {}" #: core\app.py:541 msgid "You have no custom command set up. Set it up in your preferences." msgstr "사용자 지정 명령을 설정하지 않았습니다. 기본 설정에서 설정하십시오." #: core\app.py:697 core\app.py:709 msgid "You are about to remove %d files from results. Continue?" msgstr "결과에서 %d 개의 파일을 제거하려고합니다. 실행할까요?" #: core\app.py:745 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} 개의 중복 그룹이 우선 순위 재 지정으로 변경되었습니다." #: core\app.py:792 msgid "The selected directories contain no scannable file." msgstr "선택한 디렉토리에 스캔 가능한 파일이 없습니다." #: core\app.py:808 msgid "Collecting files to scan" msgstr "스캔 할 파일 수집" #: core\app.py:858 msgid "%s (%d discarded)" msgstr "%s (%d 폐기)" #: core\directories.py:190 msgid "Collected {} files to scan" msgstr "파일 목록 생성 중: {}개 파일" #: core\directories.py:206 msgid "Collected {} folders to scan" msgstr "폴더 목록 생성 중: {}개 폴더" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "중복 파일 %d개 확인됨: %d개 그룹" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "{}개 파일을 휴지통으로 보내려고 합니다." #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "정규식" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "무시 목록에서 항목 %d개를 정말로 제거 하시겠습니까?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "파일 이름" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "파일 이름 - 필드" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "파일 이름 - 필드 (순서 없음)" #: core\me\scanner.py:23 msgid "Tags" msgstr "태그" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "내용" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "%d/%d 사진 분석" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "%d/%d 청크 매치 수행" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "매칭 준비" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "%d/%d 일치 확인" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "사진 EXIF 읽는 중: %d/%d" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "EXIF 타임 스탬프" #: core\prioritize.py:70 msgid "None" msgstr "없음" #: core\prioritize.py:102 msgid "Ends with number" msgstr "숫자로 끝남" #: core\prioritize.py:103 msgid "Doesn't end with number" msgstr "숫자로 끝나지 않음" #: core\prioritize.py:104 msgid "Longest" msgstr "최장" #: core\prioritize.py:105 msgid "Shortest" msgstr "최단" #: core\prioritize.py:142 msgid "Highest" msgstr "제일 높은" #: core\prioritize.py:142 msgid "Lowest" msgstr "최저" #: core\prioritize.py:171 msgid "Newest" msgstr "최신" #: core\prioritize.py:171 msgid "Oldest" msgstr "가장 오래된" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) 개의 중복이 표시되었습니다." #: core\results.py:141 msgid " filter: %s" msgstr "필터: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "파일 크기 읽는 중: %d/%d" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "파일 메타데이터 읽는 중: %d/%d" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "거의 완료되었습니다! 결과를 취합하고 있습니다." #: core\se\scanner.py:18 msgid "Folders" msgstr "폴더" dupeguru-4.3.1/locale/ko/LC_MESSAGES/ui.po000066400000000000000000001057331426171743600200150ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2022 # Fuan , 2022 # Sangdon Lim, 2022 # msgid "" msgstr "" "Last-Translator: Sangdon Lim, 2022\n" "Language-Team: Korean (https://www.transifex.com/voltaicideas/teams/116153/ko/)\n" "Language: ko\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: qt/app.py:81 msgid "Quit" msgstr "나가기" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "옵션" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "무시 목록" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "사진 캐시 지우기" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "dupeGuru 도움말" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "dupeGuru에 대하여" #: qt/app.py:87 msgid "Open Debug Log" msgstr "디버그 로그 열기" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "캐시 된 모든 사진 분석을 정말로 제거 하시겠습니까?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "사진 캐시가 삭제되었습니다." #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} 파일 (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "삭제 옵션" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "링크 생성" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "중복 파일들을 삭제한 후 원본 파일을 참조하는 링크로 대체합니다." #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "하드링크" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "심볼릭 링크" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr "(미지원)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "즉시 삭제" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "파일을 휴지통으로 보내지 않고 바로 삭제합니다. 파일을 휴지통으로 보낼 수 없는 경우 등에 사용할 수 있습니다." #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "실행" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "취소" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "속성" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "선택됨" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "참조" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "결과 불러오기..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "결과 창" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "폴더 추가하기..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "파일" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "보기" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "도움말" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "최근 결과 불러오기" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "애플리케이션 모드 :" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "음악" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "그림" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "표준" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "스캔 유형 :" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "더 많은 옵션" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "스캔 할 폴더를 선택하고 \"스캔\"을 누르십시오." #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "결과 불러오기" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "스캔" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "저장되지 않은 결과" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "저장되지 않은 결과가 있습니다. 종료하시겠습니까?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "스캔 목록에 추가 할 폴더를 선택하십시오" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "로드 할 결과 파일을 선택하십시오" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "모든 파일 (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "dupeGuru 결과 (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "새 스캔 시작" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "저장되지 않은 결과가 있습니다. 계속 하시겠습니까?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "이름" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "상태" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "제외된" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "일반" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "선택에서 제거" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "삭제" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "닫기" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "세부사항" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "스캔 할 태그 :" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "트랙" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "아티스트" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "앨범" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "제목" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "장르" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "년" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "단어 가중치" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "유사한 단어와 일치" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "다른 확장자의 파일도 비교에 포함" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "필터링 할 때 정규식 사용" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "삭제 또는 이동시 빈 폴더 제거" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "동일한 파일에 대한 중복 하드링크 무시" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "디버그 모드 (다시 시작 필요)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "가로세로 크기가 다른 이미지도 비교" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "필터 경도 :" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "더 많은 결과" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "더 적은 결과" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "글꼴 크기:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "언어:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "복사 및 이동 :" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "목적지에 직접" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "상대 경로 재생성" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "절대 경로 재생성" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "사용자 지정 명령 (인수: %d 은 중복, %r 은 참조):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "언어 변경 사항을 적용하려면 dupeGuru를 다시 시작해야합니다." #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "중복 우선 순위 재 지정" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "오른쪽 상자에 기준을 추가하고 확인을 클릭하여 이러한 기준에 가장 적합한 복제를 해당 그룹의 참조 위치로 보냅니다. 자세한 정보는 도움말" " 파일을 읽으십시오." #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "문제!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "일부 (또는 전체) 파일을 처리하는 데 문제가 있습니다. 이러한 문제의 원인은 아래 표에 설명되어 있습니다. 해당 파일은 결과에서 " "제거되지 않았습니다." #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "선택한 항목 표시" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "행위" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "중복파일만 보기" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "델타 값만 보기" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "선택 항목을 휴지통으로 보내기..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "선택항목을 이동..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "선택항목을 복사..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "결과에서 표시된 항목을 제거하다." #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "결과 우선 순위 재 지정..." #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "결과에서 선택한 항목 제거" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "무시 목록에 선택 항목 추가" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "선택한 항목을 참조 항목으로 만들기" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "기본 응용 프로그램으로 선택한 항목 열기" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "선택한 항목의 포함 폴더 열기" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "선택한 이름 변경" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "모두 표시" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "없음으로 표시" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "마킹 반전" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "선택한 항목 표시" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "HTML로 내보내기" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "CSV로 내보내기" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "결과 저장 ..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "사용자 지정 명령 호출" #: qt/result_window.py:102 msgid "Mark" msgstr "표시" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "열" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "기본값으로 재설정" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} 결과" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "복제 만" #: qt/result_window.py:194 msgid "Delta Values" msgstr "델타 값" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "결과를 저장할 파일을 선택하십시오." #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "다음보다 작은 파일 무시" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ 결과" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "행위" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "새 폴더 추가..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "고급" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "자동으로 업데이트 확인" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "기본" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "모두 앞으로 가져 오기" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "업데이트를 확인..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "창 닫기" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "복사" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "사용자 지정 명령 (인수: %d 은 중복, %r 은 참조):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "컷" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "델타" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "선택한 파일의 세부 정보" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "세부 정보 패널" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "디렉토리" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "dupeGuru 기본 설정" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "dupeGuru 결과" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "dupeGuru 웹 사이트" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "편집" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "CSV로 결과 내보내기" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "XHTML로 결과 내보내기" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "더 적은 결과" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "필터" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "필터 경도 :" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Filter Results..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "폴더 선택 창" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "글꼴 크기 :" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "dupeGuru 숨기기" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "다른 숨기기" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "이보다 작은 파일 무시 :" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "파일에서로드..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "최소화" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "모드" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "더 많은 결과" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "확인" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "붙여 넣다" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "환경 설정..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "한눈에" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "dupeGuru 종료" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "기본값으로 재설정" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "기본값으로 재설정" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "공개" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Finder에서 선택한 항목 표시" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "모두 선택" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "표시된 항목을 휴지통으로 보내기 ..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "서비스" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "모두 표시" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "중복 스캔 시작" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "'%@' 이름이 이미 존재합니다." #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "창" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "줌" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "제외 필터" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "스캔 결과" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "디렉토리로드 ..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "디렉토리 저장 ..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "로드 할 디렉토리 파일을 선택하십시오" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "dupeguru 디렉토리 파일 (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "디렉토리를 저장할 파일을 선택하십시오" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "dupeguru 디렉토리 파일 (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "추가" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "기본값으로 복원" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "테스트 문자열" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "여기에 파이썬 정규식을 입력하십시오 ..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "여기에 파일 시스템 경로 또는 파일 이름을 입력하십시오 ..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "이러한 (대소 문자 구분) 파이썬 정규식은 스캔 중에 파일을 필터링합니다.
    또한 디렉토리 이름이 선택한 정규식 중 하나와 일치하는 경우 디렉토리 탭에서 기본 상태가 제외됨으로 설정됩니다.
    수집 된 각 파일에 대해 완전히 무시할지 여부를 결정하기 위해 두 가지 테스트가 수행됩니다.
  • 1. 경로 구분자가없는 정규식은 파일 이름과 만 비교됩니다.
  • \n" "
  • 2. 경로 구분자가 하나 이상 포함 된 정규식은 파일의 전체 경로와 비교됩니다.

  • \n" "예 : \"내 그림\"디렉토리에서만 .PNG 파일을 필터링하려는 경우 :
    .*내\\s그림\\\\.*\\.png

    테스트 필드에 가짜 경로를 붙여 넣은 후 \"test string\"버튼으로 정규 표현식을 테스트 할 수 있습니다.
    C:\\\\사용자\\내 그림\\test.png

    \n" "일치하는 정규 표현식이 강조 표시됩니다.
    강조 표시가 하나 이상있는 경우 검사하는 동안 테스트 된 경로 또는 파일 이름이 무시됩니다.

    마침표 '.'로 시작하는 디렉토리 및 파일 기본적으로 필터링됩니다.

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "컴파일 오류 :" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "줌을 증가" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "줌을 감소" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "보통 크기" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "최고로 잘 맞는" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "사진 캐시 모드 :" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "뷰어 도구 모음에서 테마 아이콘 재정의" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "테마 엔진에서 제공하는 아이콘 대신 자체 내부 아이콘 사용" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "이미지 뷰어에 스크롤바 표시" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "표시된 이미지가 뷰포트에 맞지 않으면 스크롤바를 표시하여 뷰를 확장합니다." #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "탭 표시 줄에 기본 위치를 사용 (다시 시작해야 함)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "탭 바를 메인 메뉴 옆이 아닌 아래에 놓습니다.\n" "MacOS에서는 탭 막대가 대신 창의 너비를 채 웁니다." #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "참조 용으로 굵은 글꼴 사용" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "참조 전경색 :" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "참조 배경색 :" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "델타의 전경색 :" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "제목 표시 줄을 표시하고 고정 할 수 있습니다." #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "제목 표시 줄이 숨겨져있는 경우 수정 자 키를 사용하여 부동 창을 주위로 드래그하십시오." #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "제목 표시 줄은 창이 고정되어있는 동안에 만 비활성화 할 수 있습니다." #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "세로 제목 표시 줄" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "제목 표시 줄을 상단 가로에서 왼쪽 세로로 변경" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "탭 표시 줄 표시" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "이러한 (대소 문자 구분) 파이썬 정규식은 스캔 중에 파일을 필터링합니다.
    또한 디렉토리 이름이 선택한 정규식 중 하나와 일치하는 경우 디렉토리 탭에서 기본 상태가 제외됨으로 설정됩니다.
    수집 된 각 파일에 대해 완전히 무시할지 여부를 결정하기 위해 두 가지 테스트가 수행됩니다.
  • 1. 경로 구분자가없는 정규식은 파일 이름과 만 비교됩니다.
  • \n" "
  • 2. 경로 구분자가 하나 이상 포함 된 정규식은 파일의 전체 경로와 비교됩니다.

  • \n" "예 : \"내 그림\"디렉토리에서만 .PNG 파일을 필터링하려는 경우 :
    .*내\\s그림\\\\.*\\.png

    테스트 필드에 가짜 경로를 붙여 넣은 후 \"test string\"버튼으로 정규 표현식을 테스트 할 수 있습니다.
    C:\\\\사용자\\내 그림\\test.png

    \n" "일치하는 정규 표현식이 강조 표시됩니다.
    강조 표시가 하나 이상있는 경우 검사하는 동안 테스트 된 경로 또는 파일 이름이 무시됩니다.

    마침표 '.'로 시작하는 디렉토리 및 파일 기본적으로 필터링됩니다.

    " #: qt\app.py:256 msgid "Results" msgstr "결과" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "일반 인터페이스" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "결과표" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "세부 정보 창" #: qt\preferences_dialog.py:285 msgid "General" msgstr "일반" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "디스플레이" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "다음보다 큰 파일은 일부만 해시" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "MB" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "OS 자체 인터페이스 사용" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "파일 및 폴더 선택에 OS 자체 인터페이스를 사용합니다." #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "다음보다 큰 파일 무시" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "캐시 제거" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "캐시를 제거할까요? 캐시에는 파일 해시 및 이미지 분석 결과가 포함되어 있습니다." #: qt\app.py:299 msgid "Cache cleared." msgstr "캐시를 제거했습니다." #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "다크 모드 사용" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "디버그" #: qt\about_box.py:31 msgid "About {}" msgstr "{} 에 대한정보" #: qt\about_box.py:47 msgid "Version {}" msgstr "버전 {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "업데이트 확인 중..." #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "GPLv3 라이센스" #: qt\about_box.py:68 msgid "No update available." msgstr "새 업데이트가 없습니다." #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "새 버전 {}이 있습니다. 다운로드: 링크" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "오류보고" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "문제가 발생했습니다. 오류를보고하는 것은 어떻습니까?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "오류 보고서는 Github 문제로보고해야합니다. 위의 오류 추적을 복사하여 새 문제에 붙여 넣을 수 있습니다.\n" "\n" "이미 존재하는 문제에 대해 사전에 검색을 실행하십시오. 또한 경험하고있는 버그가 이미 패치되었을 수 있으므로 저장소에서 사용 가능한 최신 버전을 테스트해야합니다.\n" "\n" "일반적으로 실제로 도움이되는 것은 오류가 발생한 방법에 대한 설명을 추가하는 것입니다. 감사!\n" "\n" "이 오류 후에도 응용 프로그램이 계속 실행되어야하지만 불안정한 상태 일 수 있으므로 응용 프로그램을 다시 시작하는 것이 좋습니다." #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Github로 이동" #: qt\preferences.py:24 msgid "Czech" msgstr "체코어" #: qt\preferences.py:25 msgid "German" msgstr "독일어" #: qt\preferences.py:26 msgid "Greek" msgstr "그리스어" #: qt\preferences.py:27 msgid "English" msgstr "영어" #: qt\preferences.py:28 msgid "Spanish" msgstr "스페인어" #: qt\preferences.py:29 msgid "French" msgstr "프랑스어" #: qt\preferences.py:30 msgid "Armenian" msgstr "아르메니아어" #: qt\preferences.py:31 msgid "Italian" msgstr "이탈리아어" #: qt\preferences.py:32 msgid "Japanese" msgstr "일본어" #: qt\preferences.py:33 msgid "Korean" msgstr "한국어" #: qt\preferences.py:34 msgid "Malay" msgstr "말레이어" #: qt\preferences.py:35 msgid "Dutch" msgstr "네덜란드어" #: qt\preferences.py:36 msgid "Polish" msgstr "폴란드어" #: qt\preferences.py:37 msgid "Brazilian" msgstr "브라질 언어" #: qt\preferences.py:38 msgid "Russian" msgstr "러시아어" #: qt\preferences.py:39 msgid "Turkish" msgstr "터키어" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "우크라이나어" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "베트남어" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "중국어 (간체)" #: qt\recent.py:54 msgid "Clear List" msgstr "목록 지우기" #: qt\search_edit.py:78 msgid "Search..." msgstr "검색.." dupeguru-4.3.1/locale/ms/000077500000000000000000000000001426171743600152505ustar00rootroot00000000000000dupeguru-4.3.1/locale/ms/LC_MESSAGES/000077500000000000000000000000001426171743600170355ustar00rootroot00000000000000dupeguru-4.3.1/locale/ms/LC_MESSAGES/columns.po000066400000000000000000000053571426171743600210670ustar00rootroot00000000000000# Translators: # Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) , 2021 # msgid "" msgstr "" "Last-Translator: Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) , 2021\n" "Language-Team: Malay (https://www.transifex.com/voltaicideas/teams/116153/ms/)\n" "Language: ms\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "Laluan Fail" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "Mesej Ralat" #: core\me\prioritize.py:23 msgid "Duration" msgstr "Tempoh" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "Kadar Bit" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "Kadar Sampel" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "Nama Fail" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "Folder" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "Saiz (MB)" #: core\me\result_table.py:22 msgid "Time" msgstr "Masa" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "Kadar Sampel" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "Jenis" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "Pengubahsuaian" #: core\me\result_table.py:27 msgid "Title" msgstr "Tajuk" #: core\me\result_table.py:28 msgid "Artist" msgstr "Artis" #: core\me\result_table.py:29 msgid "Album" msgstr "Album" #: core\me\result_table.py:30 msgid "Genre" msgstr "Genre" #: core\me\result_table.py:31 msgid "Year" msgstr "Tahun" #: core\me\result_table.py:32 msgid "Track Number" msgstr "Nombor Runut" #: core\me\result_table.py:33 msgid "Comment" msgstr "Komen" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "% Padanan" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "Perkataan Diguna" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "Jumlah Duplikasi" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "Dimensi" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "Saiz (KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "Cap Masa EXIF" #: core\prioritize.py:156 msgid "Size" msgstr "Saiz" dupeguru-4.3.1/locale/ms/LC_MESSAGES/core.po000066400000000000000000000157031426171743600203330ustar00rootroot00000000000000# Translators: # Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) , 2021 # msgid "" msgstr "" "Last-Translator: Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) , 2021\n" "Language-Team: Malay (https://www.transifex.com/voltaicideas/teams/116153/ms/)\n" "Language: ms\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "Tiada duplikasi yang ditandai. Tiada apa yang dilakukan." #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "Tiada duplikasi yang dipilih. Tiada apa yang dilakukan." #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" "Anda bakal membuka banyak fail serentak. Bergantung kepada apa yang " "digunakan untuk membuka fail tersebut, ia mungkin menyebabkan sepah. Ingin " "teruskan?" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "Mengimbas untuk duplikasi" #: core\app.py:72 msgid "Loading" msgstr "Memuatkan" #: core\app.py:73 msgid "Moving" msgstr "Memindahkan" #: core\app.py:74 msgid "Copying" msgstr "Menyalinkan" #: core\app.py:75 msgid "Sending to Trash" msgstr "Menghantarkan ke Tong Sampah" #: core\app.py:289 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" "Tindakan sebelum ini masih tergantung. Anda tidak boleh mulakan yang baharu " "lagi. Tunggu beberapa saat, kemudian cuba lagi." #: core\app.py:300 msgid "No duplicates found." msgstr "Tiada duplikasi dijumpai." #: core\app.py:315 msgid "All marked files were copied successfully." msgstr "Semua fail yang ditandai telah berjaya disalin." #: core\app.py:317 msgid "All marked files were moved successfully." msgstr "Semua fail yang ditandai telah berjaya dipindah." #: core\app.py:319 msgid "All marked files were deleted successfully." msgstr "Semua fail yang ditandai telah berjaya dipadam." #: core\app.py:321 msgid "All marked files were successfully sent to Trash." msgstr "Semua fail yang ditandai telah berjaya dihantar ke Tong Sampah." #: core\app.py:326 msgid "Could not load file: {}" msgstr "Tidak mampu memuatkan fail: {}" #: core\app.py:382 msgid "'{}' already is in the list." msgstr "'{}' sudah ada dalam senarai." #: core\app.py:384 msgid "'{}' does not exist." msgstr "'{}' tidak wujud." #: core\app.py:392 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "" "Kesemua %d padanan yang dipilih akan diabaikan dalam semua imbasan " "terkemudian. Ingin teruskan?" #: core\app.py:469 msgid "Select a directory to copy marked files to" msgstr "Pilih direktori dituju untuk salin fail yang ditandai" #: core\app.py:471 msgid "Select a directory to move marked files to" msgstr "Pilih direktori dituju untuk pindah fail yang ditandai" #: core\app.py:510 msgid "Select a destination for your exported CSV" msgstr "Pilih tempat tujuan untuk eksport CSV anda" #: core\app.py:516 core\app.py:771 core\app.py:781 msgid "Couldn't write to file: {}" msgstr "Tidak mampu menulis ke fail: {}" #: core\app.py:539 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" "Anda tidak ada perintah tersuai ditetapkan. Tetapkannya melalui menu " "keutamaan anda." #: core\app.py:695 core\app.py:707 msgid "You are about to remove %d files from results. Continue?" msgstr "Anda bakal mengalih keluar %d fail dari keputusan. Ingin teruskan?" #: core\app.py:743 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} kumpulan duplikasi telah diubah oleh pengutamaan semula." #: core\app.py:790 msgid "The selected directories contain no scannable file." msgstr "Direktori yang dipilih tidak mempunyai fail yang boleh diimbas." #: core\app.py:803 msgid "Collecting files to scan" msgstr "Mengumpulkan fail untuk diimbas" #: core\app.py:850 msgid "%s (%d discarded)" msgstr "%s (%d dibuang)" #: core\directories.py:191 msgid "Collected {} files to scan" msgstr "{} fail dikumpulkan untuk diimbas" #: core\directories.py:207 msgid "Collected {} folders to scan" msgstr "{} folder dikumpulkan untuk diimbas" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "%d padanan dijumpai dari %d kumpulan" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "Anda menghantar {} fail ke Tong Sampah." #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "Ungkapan Nalar" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "" "Adakah anda pasti anda ingin alih keluar kesemua %d item dari senarai abai?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "Nama Fail" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "Nama Fail - Medan" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "Nama Fail - Medan (Tiada Tertib)" #: core\me\scanner.py:23 msgid "Tags" msgstr "Tag" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "Kandungan" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "%d / %d gambar dianalisis" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "%d / %d padanan ketulan dilaksanakan" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "Membuat persediaan untuk pemadanan" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "%d / %d padanan disahkan" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "EXIF bagi %d / %d gambar dibaca" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "Cap masa EXIF" #: core\prioritize.py:70 msgid "None" msgstr "Tiada" #: core\prioritize.py:100 msgid "Ends with number" msgstr "Tamat dengan nombor" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "Tidak tamat dengan nombor" #: core\prioritize.py:102 msgid "Longest" msgstr "Terpanjang" #: core\prioritize.py:103 msgid "Shortest" msgstr "Terpendek" #: core\prioritize.py:140 msgid "Highest" msgstr "Tertinggi" #: core\prioritize.py:140 msgid "Lowest" msgstr "Terendah" #: core\prioritize.py:169 msgid "Newest" msgstr "Terbaru" #: core\prioritize.py:169 msgid "Oldest" msgstr "Terlama" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) duplikasi ditandai." #: core\results.py:141 msgid " filter: %s" msgstr "penapis: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "Saiz bagi %d / %d gambar dibaca" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "Metadata bagi %d / %d gambar dibaca" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "Hampir selesai! Menyusun keputusan..." #: core\se\scanner.py:18 msgid "Folders" msgstr "Folder" dupeguru-4.3.1/locale/ms/LC_MESSAGES/ui.po000066400000000000000000001051231426171743600200140ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2022 # Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) , 2022 # msgid "" msgstr "" "Last-Translator: Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) , 2022\n" "Language-Team: Malay (https://www.transifex.com/voltaicideas/teams/116153/ms/)\n" "Language: ms\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: qt/app.py:81 msgid "Quit" msgstr "Keluar" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "Pilihan" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Senarai Abai" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Kosongkan Cache Gambar" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Bantuan dupeGuru" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "Mengenai dupeGuru" #: qt/app.py:87 msgid "Open Debug Log" msgstr "Buka Log Nyahpepijat" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "" "Adakah anda pasti anda ingin alih keluar kesemua analisis gambar cache anda?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "Cache gambar dikosongkan." #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} fail (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Pilihan Pemadaman" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Pautkan fail dipadam" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" "Setelah duplikasi dipadam, letak pautan menuju fail rujukan untuk " "menggantikan fail dipadam." #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "Pautan Keras" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "Pautan Bersimbol" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr " (tidak disokong)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Padam fail secara terus" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "Padam fail secara terus dan bukannya hantar fail ke tong sampah. Pilihan ini" " selalunya digunakan sebagai penyelesaian apabila kaedah pemadaman biasa " "tidak berjaya." #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Teruskan" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Batal" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "Atribut" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "Dipilih" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "Rujukan" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "Muatkan Keputusan..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "Tetingkap Keputusan" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "Tambah Folder..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "Fail" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "Lihat" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "Bantuan" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "Muatkan Keputusan Baru-baru Ini" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "Mod Aplikasi:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "Muzik" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "Gambar" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "Piawai" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "Jenis Imbasan:" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "Pilihan Lanjutan" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "Pilih folder untuk imbas dan tekan \"Imbas\"." #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "Muatkan Keputusan" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "Imbas" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "Keputusan belum disimpan" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "" "Anda mempunyai keputusan yang belum disimpan, adakah anda pasti anda ingin " "keluar?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "Pilih folder untuk tambah ke senarai imbasan" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "Pilih fail keputusan untuk dimuatkan" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "Semua Fail (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "Keputusan dupeGuru (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "Mulakan imbasan baharu" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "" "Anda mempunyai keputusan yang belum disimpan, adakah anda pasti anda ingin " "teruskan?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "Nama" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "Keadaan" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "Dikecualikan" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "Biasa" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "Alih Keluar yang Dipilih" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "Kosongkan" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "Tutup" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "Maklumat" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "Tag untuk diimbas:" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "Runut" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "Artis" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "Album" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "Tajuk" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "Genre" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "Tahun" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "Pemberatan perkataan" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "Padan perkataan serupa" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "Boleh campur jenis fail" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "Guna ungkapan nalar ketika menapis" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "Alih keluar folder kosong semasa pemadaman atau pemindahan" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "Abaikan duplikasi yang paut keras ke fail yang sama" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "Mod nyahpepijat (perlu mula semula)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "Padan gambar dengan dimensi berlainan" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "Kekuatan Penapis:" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "Lebihkan Keputusan" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "Kurang Keputusan" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "Saiz fon:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "Bahasa:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "Salin dan Pindah:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "Dalam tempat tujuan" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "Cipta semula laluan relatif" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "Cipta semula laluan mutlak" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "Perintah Tersuai (argumen: %d untuk duplikasi, %r untuk rujukan):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "dupeGuru perlu mula semula untuk menerima kesan perubahan bahasa." #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "Pengutamaan semula duplikasi" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "Tambah kriteria di kotak kanan dan klik OK untuk hantar duplikasi yang " "paling sepadan dengan kriteria tersebut ke kedudukan rujukan kumpulan " "masing-masing. Baca fail bantuan untuk maklumat lanjut." #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "Masalah!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "Terdapat masalah semasa memproses sesetengah (atau kesemua) fail. Penyebab " "masalah ini diterangkan dalam jadual di bawah. Fail tersebut tidak dialih " "keluar dari keputusan anda." #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "Dedahkan yang Dipilih" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "Tindakan" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "Tunjuk Duplikasi Sahaja" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "Tunjuk Nilai Delta" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "Hantar yang Ditandai ke Tong Sampah..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "Pindah yang Ditandai ke..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "Salin yang Ditandai ke..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "Alih Keluar yang Ditandai dari Keputusan" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "Pengutamaan Semula Keputusan..." #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "Alih Keluar yang Dipilih dari Keputusan" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "Tambah yang Dipilih ke Senarai Abai" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "Jadikan yang Dipilih menjadi Rujukan" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "Buka yang Dipilih dengan Aplikasi Lalai" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "Buka Folder yang Mengandungi yang Dipilih" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "Namakan Semula yang Dipilih" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "Tanda Semua" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "Tanda Kosong" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "Terbalikkan Penandaan" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "Tanda yang Dipilih" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "Eksport ke HTML" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "Eksport ke CSV" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "Simpan Keputusan..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "Guna Perintah Tersuai" #: qt/result_window.py:102 msgid "Mark" msgstr "Tanda" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "Lajur" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "Tetap Semula ke Lalai" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} Keputusan" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "Duplikasi Sahaja" #: qt/result_window.py:194 msgid "Delta Values" msgstr "Nilai Delta" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "Pilih fail untuk simpan keputusan anda" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "Abaikan fail lebih kecil dari" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ Keputusan" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "Tindakan" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "Tambah Folder Baharu..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "Lanjutan" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "Periksa kemas kini secara automatik" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "Asas" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "Bawa Semua ke Hadapan" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "Periksa kemas kini..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "Tutup Tetingkap" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "Salin" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "Perintah tersuai (argumen: %d untuk duplikasi, %r untuk rujukan):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "Potong" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Delta" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "Maklumat Fail yang Dipilih" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "Panel Maklumat" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "Direktori" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "Keutamaan dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "Keputusan dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "Laman Sesawang dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "Sunting" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "Eksport Keputusan ke CSV" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "Eksport Keputusan ke XHTML" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "Kurangkan keputusan" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "Penapis" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "Kekuatan penapisan:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Tapis Keputusan..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "Tetingkap Pemilihan Folder" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "Saiz Fon:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "Sembunyikan dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "Sembunyikan yang Lain" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "Abaikan fail lebih kecil dari:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "Muatkan dari fail..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "Meminimumkan" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "Mod" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "Lebihkan keputusan" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "Ok" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "Tampal" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "Keutamaan..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "Lihat Segera" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "Keluar dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "Tetap Semula ke Lalai" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "Tetap Semula ke Lalai" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "Dedah" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Dedah yang Dipilih dalam Pencari" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "Pilih Semua" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Hantar yang Ditandai ke Tong Sampah..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "Perkhidmatan" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "Tunjuk Semua" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "Mulakan Imbasan Duplikasi" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "Nama '%@' sudah wujud." #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "Tetingkap" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Zum" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "Penapis Pengecualian" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "Keputusan Imbasan" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "Muatkan Direktori..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "Simpan Direktori..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "Pilih fail direktori untuk dimuatkan" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "Keputusan dupeGuru (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "Pilih fail untuk simpan direktori anda" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "Direktori dupeGuru (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "Tambah" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "Tetap semula lalai" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "Cuba rentetan" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "Taipkan ungkapan nalar python di sini..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "Taipkan laluan sistem fail atau nama fail di sini..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Ungkapan nalar python (sensitif huruf) ini akan menapis keluar fail ketika imbasan.
    Direktori juga akan ada keadaan lalaisendiri ditetapkan kepada Dikecualikan dalam tab Direktori jika nama tersebut sepadan dengan salah satu ungkapan nalar.
    Untuk setiap fail yang terhimpun, dua percubaan akan dilaksanakan bagi setiap satu fail tersebut untuk menentukan sama ada fail tersebut perlu ditapis keluar:
  • 1. Ungkapan nalar tanpa pemisah laluan di dalamnya akan dibandingkan dengan nama fail sahaja.
  • \n" "
  • 2. Ungkapan nalar tanpa pemisah laluan di dalamnya akan dibandingkan dengan laluan penuh ke fail.

  • \n" "Contoh: jika anda ingin tapis keluar fail .PNG dari direktori \"My Pictures\" sahaja:
    .*My\\sPictures\\\\.*\\.png

    Anda boleh cuba ungkapan nalar dengan fungsi cuba rentetan dengan menampal laluan palsu di dalamnya:
    C:\\\\User\\My Pictures\\test.png

    \n" "Ungkapan nalar yang terpadan akan ditonjolkan.
    Sekiranya ada sekurang-kurangnya satu tonjolan, laluan yang dicuba akan diabaikan ketika imbasan.

    Direktori dan fail yang bermula dengan tanda titik '.' ditapis keluar secara lalainya.

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "Ralat pengkompilan:" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "Naikkan zum" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "Turunkan zum" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "Saiz biasa" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "Suaian terbaik" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "Mod cache gambar:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "Mengataskan ikon tema dalam bar alat pemidang" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" "Guna ikon dalaman kami sendiri menggantikan apa yang disediakan oleh enjin " "tema" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "Tunjuk bar tatal dalam pemidang imej" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" "Apabila imej yang dipaparkan tidak muat dalam port pandang, tunjuk bar tatal" " untuk menggerakkan pemidang" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "Guna kedudukan lalai untuk bar tab (perlu mula semula)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "Letak bar tab di bawah menu utama dan bukannya di sebelahnya\n" "Di MacOS, bar tab akan mengisi lebar tetingkap." #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "Guna fon tebal untuk rujukan" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "Warna latar depan rujukan:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "Warna latar belakang rujukan:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "Warna latar depan delta:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "Tunjuk bar tajuk dan boleh dilimbungkan" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" "Apabila bar tajuk disembunyikan, guna kekunci pengubah suai untuk seret " "tetingkap terapung" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "Bar tajuk hanya boleh dilumpuhkan ketika tetingkap dilimbungkan" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "Bar tajuk menegak" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "" "Ubah bar tajuk daripada melintang di atas, kepada menegak di sisi kiri" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "Tunjuk bar tab" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Ungkapan nalar python (sensitif huruf) ini akan menapis keluar fail ketika imbasan.
    Direktori juga akan ada keadaan lalai sendiri ditetapkan kepada Dikecualikan dalam tab Direktori jika nama tersebut sepadan dengan salah satu daripada ungkapan nalar yang dipilih.
    Untuk setiap fail yang terhimpun, dua percubaan akan dilaksanakan untuk menentukan sama ada fail tersebut perlu diabaikan sepenuhnya:
  • 1. Ungkapan nalar tanpa pemisah laluan di dalamnya akan dibandingkan dengan nama fail sahaja.
  • \n" "
  • 2. Ungkapan nalar dengan sekurang-kurangnya satu pemisah laluan di dalamnya akan dibandingkan dengan laluan penuh ke fail.

  • \n" "Contoh: jika anda ingin tapis keluar fail .PNG dari direktori \"My Pictures\" sahaja:
    .*My\\sPictures\\\\.*\\.png

    Anda boleh cuba ungkapan nalar dengan butang \"cuba rentetan\" selepas menampal laluan palsi dalam medan percubaan:
    C:\\\\User\\My Pictures\\test.png

    \n" "Ungkapan nalar yang terpadan akan ditonjolkan.
    Sekiranya ada sekurang-kurangnya satu tonjolan, laluan atau nama fail yang dicuba akan diabaikan ketika imbasan.

    Direktori dan fail yang bermula dengan tanda titik '.' ditapis keluar secara lalainya.

    " #: qt\app.py:256 msgid "Results" msgstr "Keputusan" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "Antara Muka Am" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "Jadual Keputusan" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "Tetingkap Maklumat" #: qt\preferences_dialog.py:285 msgid "General" msgstr "Am" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "Paparan" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "Fail cincang separa lebih besar dari" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "MB" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "Guna dialog OS natif" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" "Gunakan dialog natif OS untuk tindakan seperti pemilihan fail/folder.\n" "Sesetengah dialog natif mempunyai kefungsian yang terhad." #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "Abaikan fail lebih besar dari" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "Kosongkan Cache" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" "Adakah anda pasti anda ingin kosongkan cache? Ini akan alih keluar semua " "cincang fail dan analisis gambar yang tercache." #: qt\app.py:299 msgid "Cache cleared." msgstr "Cache dikosongkan." #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "Guna gaya gelap" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "Bukah operasi imbasan" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "Membukah operasi imbasan dan simpan log untuk pengoptimuman." #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "Log terletak di: {}" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "Nyahpepijat" #: qt\about_box.py:31 msgid "About {}" msgstr "Mengenai {}" #: qt\about_box.py:47 msgid "Version {}" msgstr "Versi {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "Memeriksa kemas kini..." #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "Dilesenkan bawah GPLv3" #: qt\about_box.py:68 msgid "No update available." msgstr "Tiada kemas kini tersedia." #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "Versi baharu {} tersedia, muat turun di sini." #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "Laporan Ralat" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "Terdapat kesulitan yang terjadi. Apa kata laporkan ralat tersebut?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "Laporan ralat patut dilaporkan sebagai isu Github. Anda boleh salin runut balik ralat di atas dan tampal dalam isu baharu.\n" "\n" "Sila pastikan anda menggelintar dahulu kalau-kalau isu sudah wujud. Juga pastikan untuk cuba versi paling terbaharu yang disediakan dari repositori, kerana pepijat yang anda alami mungkin sudah ditampung.\n" "\n" "Ia sangat membantu sekiranya anda tambah keterangan mengenai bagaimana anda dapat ralat tersebut. Terima kasih!\n" "\n" "Walaupun aplikasi sepatutnya masih boleh digunakan selepas ralat ini, ia mungkin berada dalam keadaan tidak stabil, jadi anda digalakkan untuk memulakan semula aplikasi ini." #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Pergi ke Github" #: qt\preferences.py:24 msgid "Czech" msgstr "Czech" #: qt\preferences.py:25 msgid "German" msgstr "Jerman" #: qt\preferences.py:26 msgid "Greek" msgstr "Yunani" #: qt\preferences.py:27 msgid "English" msgstr "Inggeris" #: qt\preferences.py:28 msgid "Spanish" msgstr "Sepanyol" #: qt\preferences.py:29 msgid "French" msgstr "Perancis" #: qt\preferences.py:30 msgid "Armenian" msgstr "Armenia" #: qt\preferences.py:31 msgid "Italian" msgstr "Itali" #: qt\preferences.py:32 msgid "Japanese" msgstr "Jepun" #: qt\preferences.py:33 msgid "Korean" msgstr "Korea" #: qt\preferences.py:34 msgid "Malay" msgstr "Melayu" #: qt\preferences.py:35 msgid "Dutch" msgstr "Belanda" #: qt\preferences.py:36 msgid "Polish" msgstr "Poland" #: qt\preferences.py:37 msgid "Brazilian" msgstr "Brazil" #: qt\preferences.py:38 msgid "Russian" msgstr "Rusia" #: qt\preferences.py:39 msgid "Turkish" msgstr "Turki" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "Ukraine" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "Vietnam" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "Cina (Ringkas)" #: qt\recent.py:54 msgid "Clear List" msgstr "Kosongkan Senarai" #: qt\search_edit.py:78 msgid "Search..." msgstr "Cari..." dupeguru-4.3.1/locale/nl/000077500000000000000000000000001426171743600152425ustar00rootroot00000000000000dupeguru-4.3.1/locale/nl/LC_MESSAGES/000077500000000000000000000000001426171743600170275ustar00rootroot00000000000000dupeguru-4.3.1/locale/nl/LC_MESSAGES/columns.po000066400000000000000000000052741426171743600210570ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Bas , 2021 # msgid "" msgstr "" "Last-Translator: Bas , 2021\n" "Language-Team: Dutch (https://www.transifex.com/voltaicideas/teams/116153/nl/)\n" "Language: nl\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "Bestandspad" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "Foutmelding" #: core\me\prioritize.py:23 msgid "Duration" msgstr "Tijdsduur" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "Bitrate" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "Sample frequentie" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "Bestandsnaam" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "Map" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "Grootte (MB)" #: core\me\result_table.py:22 msgid "Time" msgstr "Tijd" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "Sample Frequentie" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "Soort" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "Aanpassing" #: core\me\result_table.py:27 msgid "Title" msgstr "Titel" #: core\me\result_table.py:28 msgid "Artist" msgstr "Artiest" #: core\me\result_table.py:29 msgid "Album" msgstr "Album" #: core\me\result_table.py:30 msgid "Genre" msgstr "Genre" #: core\me\result_table.py:31 msgid "Year" msgstr "Jaar" #: core\me\result_table.py:32 msgid "Track Number" msgstr "Track nummer" #: core\me\result_table.py:33 msgid "Comment" msgstr "Commentaar" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "Zekerheid %" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "Woorden gebruikt" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "Dubbel telling" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "Afmetingen" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "Grootte (KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "EXIF Tijdstip" #: core\prioritize.py:156 msgid "Size" msgstr "Grootte" dupeguru-4.3.1/locale/nl/LC_MESSAGES/core.po000066400000000000000000000160121426171743600203170ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # Bas , 2021 # msgid "" msgstr "" "Last-Translator: Bas , 2021\n" "Language-Team: Dutch (https://www.transifex.com/voltaicideas/teams/116153/nl/)\n" "Language: nl\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "Er zijn geen gemarkeerde dubbelingen. Er is niks gedaam" #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "Er zijn geen dubelingen geselecteerd. Er is niks gedaan" #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" "Je staat op het punt om zeer veel bestanden tegelijkertijd te openen. " "Afhankelijk met welke applicaties die bestanden worden geopened kan het best" " een rommeltje worden. Doorgaan?" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "Dubbelingen aan het opsporen" #: core\app.py:72 msgid "Loading" msgstr "Laden" #: core\app.py:73 msgid "Moving" msgstr "Verplaatsen" #: core\app.py:74 msgid "Copying" msgstr "Kopiëren" #: core\app.py:75 msgid "Sending to Trash" msgstr "Naar de prullebak verplaatsen" #: core\app.py:289 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" "Er is nog een vorige actie bezig. Je kan nu nog geen nieuwe actie starten. " "Wacht een paar seconden en probeer het opnieuw" #: core\app.py:300 msgid "No duplicates found." msgstr "Geen dubbelingen gevonden" #: core\app.py:315 msgid "All marked files were copied successfully." msgstr "Alle gemarkeerde bestanden zijn succesvol gekopieerd." #: core\app.py:317 msgid "All marked files were moved successfully." msgstr "Alle gemarkeerde bestanden zijn succesvol verplaatst." #: core\app.py:319 msgid "All marked files were deleted successfully." msgstr "" #: core\app.py:321 msgid "All marked files were successfully sent to Trash." msgstr "Alle gemarkeerde bestanden zijn met succes in de prullenbak gedaan." #: core\app.py:326 msgid "Could not load file: {}" msgstr "Kan bestand niet laden: {}" #: core\app.py:382 msgid "'{}' already is in the list." msgstr "'{}' staat al in de lijst." #: core\app.py:384 msgid "'{}' does not exist." msgstr "'{}' bestaat niet." #: core\app.py:392 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "" "Alle geselecteerde %d overeenkomsten zullen in toekomstige onderzoeken " "worden overgslagen. Doorgaan?" #: core\app.py:469 msgid "Select a directory to copy marked files to" msgstr "" "Selecteer een map waar u de gemarkeerde bestanden naartoe wilt kopiëren" #: core\app.py:471 msgid "Select a directory to move marked files to" msgstr "" "Selecteer een map waar u de gemarkeerde bestanden naartoe wilt verplaatsen" #: core\app.py:510 msgid "Select a destination for your exported CSV" msgstr "Selecteer een locatie voor de CSV export" #: core\app.py:516 core\app.py:771 core\app.py:781 msgid "Couldn't write to file: {}" msgstr "Kan niet schrijven naar bestand: {}" #: core\app.py:539 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" "Er is nog geen \"aangepaste opdracht\" ingericht. Je kan dit doen bij de " "voorkeuren." #: core\app.py:695 core\app.py:707 msgid "You are about to remove %d files from results. Continue?" msgstr "" "Je staat op het punt om %d bestanden te verwijderen uit de resultaten. " "Doorgaan?" #: core\app.py:743 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "" "{} dubbelingen groepen waren veranderd door de prioriteits verschuiving." #: core\app.py:790 msgid "The selected directories contain no scannable file." msgstr "" "De geselecteerde folders bevatten geen bestanden die onderzocht kunnen " "worden." #: core\app.py:803 msgid "Collecting files to scan" msgstr "Bestanden aan het verzamelen om te onderzoeken" #: core\app.py:850 msgid "%s (%d discarded)" msgstr "%s (%d weggelaten)" #: core\directories.py:191 msgid "Collected {} files to scan" msgstr "" #: core\directories.py:207 msgid "Collected {} folders to scan" msgstr "" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "Je verplaatst {} bestand(en) naar de prullenbak" #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "Normale Uitdrukkingen" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "" "Weet je zeker dat je alle %d regels uit de overslaan lijst wilt verwijderen?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "Bestandsnaam" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "Bestandsnaam - Velden" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "Bestandsnaam - Velden (geen volgorde)" #: core\me\scanner.py:23 msgid "Tags" msgstr "Tags" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "Inhoud" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "%d van de %d afbeeldingen aan het analyseren" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "%d van de %d bulk overeenkomsten uitgevoerd" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "Voorbereiden voor dubbelingen bepaling" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "%d van de %d overeenkomsten nagekeken" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "EXIF informatie van %d van de %d afbeeldingen gelezen" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "EXIF-tijdstempel" #: core\prioritize.py:70 msgid "None" msgstr "Geen" #: core\prioritize.py:100 msgid "Ends with number" msgstr "Eindigt met nummer" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "Eindigt niet met een nummer" #: core\prioritize.py:102 msgid "Longest" msgstr "langste" #: core\prioritize.py:103 msgid "Shortest" msgstr "kortste" #: core\prioritize.py:140 msgid "Highest" msgstr "hoogste" #: core\prioritize.py:140 msgid "Lowest" msgstr "laagste" #: core\prioritize.py:169 msgid "Newest" msgstr "nieuwste" #: core\prioritize.py:169 msgid "Oldest" msgstr "oudste" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s /%s) dubbelingen gemarkeerd" #: core\results.py:141 msgid " filter: %s" msgstr "filter: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "Bestandsgrootte van %d/%d bestanden aan het lezen." #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "Metadata van %d/%d bestanden gelezen" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "Bijna klaar! Gehannes met resultaten..." #: core\se\scanner.py:18 msgid "Folders" msgstr "Mappen" dupeguru-4.3.1/locale/nl/LC_MESSAGES/ui.po000066400000000000000000001046741426171743600200200ustar00rootroot00000000000000# Translators: # Bas , 2022 # Andrew Senetar , 2022 # Fuan , 2022 # msgid "" msgstr "" "Last-Translator: Fuan , 2022\n" "Language-Team: Dutch (https://www.transifex.com/voltaicideas/teams/116153/nl/)\n" "Language: nl\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: qt/app.py:81 msgid "Quit" msgstr "Afsluiten" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "Opties" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Overslaan lijst" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Afbeelding cache leegmaken" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "dupeGuru Help" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "Over dupeGuru" #: qt/app.py:87 msgid "Open Debug Log" msgstr "Debug Log openen" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "Weet je zeker dat je de afbeeldings-analyse cache wilt verwijderen" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "Afbeelding cache leeggemaakt." #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} bestand (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Verwijderopties" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "link verwijderde bestanden" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" "Na het verwijderen van een dubbeling, een link leggen tussen het verwijderde" " bestand en het referentie bestand" #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "harde link" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "symbolische link" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr "(niet ondersteund)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Bestanden direct verwijderen" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "In plaats van de bestanden naar de prullenbak te verplaats worden ze direct " "verwijderd. Deze optie wordt normaal alleen gekozen als een oplossing als de" " normale verwijderings methode niet werkt." #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Doorgaan" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Afbreken" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "Attributen" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "Geselecteerd" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "Referentie" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "Resultaten inlezen..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "Resultaten venster" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "Folder toevoegen..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "Bestand" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "Beeld" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "Help" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "Recente resultaten inlezen" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "Toepassingsmodus:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "Muziek" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "Beeld" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "Standaard" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "Onderzoeks type" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "Meer Opties" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "" "Selecteer de folders die onderzocht moeten worden en druk op \"onderzoeken\"" #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "Resultaten laden" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "onderzoeken" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "niet opgeslagen resultaten" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "" "Je hebt nog niet opgeslagen resultaten, weet je zeker dat je wilt afsluiten?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "Selecteer een folder die wil toevoegen aan de onderzoekslijst" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "Selecteer een resultaat bestand om te openen" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "Alle bestanden (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "dupeGuru resultaten (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "Nieuw onderzoek starten" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "" "Je hebt nog niet opgeslagen resultaten, weet je zeker dat je verder wilt?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "Naam" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "Status" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "Uitgesloten" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "Normaal" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "Verwijder selectie" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "opheffen" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "sluiten" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "details" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "Labels die onderzocht moeten worden" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "Nummer" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "Artiest" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "Album" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "Titel" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "Genre" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "Jaar" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "Word gewicht" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "Vergelijk gelijkwaardige woorden" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "Bestandsformaten mogen doorelkaar gebruikt worden" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "Gebruik \"reguliere expressies\" tijdens het filteren" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "Verwijder lege folders tijdens weggooien of verplaatsen" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "Negeer dubbelingen die hard gelinkt zijn aan het zelfde bestand" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "Debug stand (herstart is nodig)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "Afbeeldingen met afwijkende afmetingen toch gebruiken bij onderzoek" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "Filter sterkte" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "Meer resultaten" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "Minder resultaten" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "Font grote:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "Taal:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "Kopieeren en verplaatsen" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "Direct in bestemming" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "Hercreeer relatieve bestandslocatie" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "Creer volledige bestands locatie" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "Aangepaste opdracht (opties: %d voor dubbeling, %r voor referentie):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "dupeGuru moet herstart worden om de taal wijziging door te voeren" #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "Her-priotarisatie van dubbelingen" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "Voeg criteria toe aan de rechter kant en klik op OK om dubbeligen die het " "meeste overeenkomen met deze criteria te verplaatsen naar de overeenkomende " "referentie groep. Lees het help bestand voor meer informatie. " #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "Problemen!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "Er waren problemen bij het verwerken van sommige (of alle) bestanden. De " "oorzaak hiervan staat beschreven in de tabel hieronder. Deze bestanden " "bestanden zijn niet verwijderd uit de resultaten." #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "Toon selectie" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "Acties" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "Alleen dubbelingen tonen" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "Alleen het verschil tonen" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "Verplaats de gemarkeerde bestanden naar de prullenbak" #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "Verplaats gemarkeerde naar ..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "Kopieer gemarkeerde naar ..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "Verwijder gemarkeerde regels uit het resultaat" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "Resultaat her-priotariseren" #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "Verwijder geselecteerde uit het resultaat" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "Voeg geselecteerde toe aan de overslaan lijst" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "Maak van de selectie een referentie" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "Open de selectie met de standaard toepassing" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "Open de folder van de selectie" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "Hernoem de selectie" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "Alles markeren" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "Niks markeren" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "Markering omdraaien" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "Geselecteerde markeren" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "Exporteer naar HTML" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "Exporteer naar CSV" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "Resultaten opslaan..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "Voer aangepaste opdracht uit" #: qt/result_window.py:102 msgid "Mark" msgstr "Markeer" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "Kolommen" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "Terug naar standaard instellingen" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} resultaten" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "Alleen dubbelingen" #: qt/result_window.py:194 msgid "Delta Values" msgstr "Verschillen" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "Selecteerd een bestand om het resultaat op te slaan" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "Negeer bestanden kleiner dan" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ resultaten" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "Actie" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "Nieuwe folder toevoegen..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "Geavanceerd" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "automatisch controleren voor nieuwere versie" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "Basis" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "Alles naar voren halen" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "Controleer nieuwere versie" #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "Venster sluiten" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "kopieer" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "Aangepaste opdracht (opties: %d voor dubbeling, %r voor referentie):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "verwijder" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "verschil" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "details van het geselecteerde bestand" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "details paneel" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "Folders" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "dupeGuru voorkeuren" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "dupeGuru resultaten" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "dupeGuru website" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "Bewerken" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "Exporteer resultaat naar CSV" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "Exporteer resultaat naar XHTML" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "Minder resultaten" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "Filter" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "Filter sterkte" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Filter resultaten ..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "Folder selectie venster" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "Grootte lettertype:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "dupeGuru verbergen" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "Overige verbergen" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "Negeer bestanden kleiner dan" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "laden vanuit bestand..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "Minimaliseren" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "Modus" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "Meer resultaten" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "Ok" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "Plakken" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "Voorkeuren ..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "Snel kijken" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "dupeGuru afsluiten" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "Terug naar standaard instellingen" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "Terug naar standaard instellingen" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "Toon bestand" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Toon folder van bestand" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "Alles selecteren" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Verplaats de gemarkeerde bestanden naar de prullenbak" #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "Services" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "Alles tonen" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "Start dubbelingen onderzoek" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "de naam '%@\" bestaat al." #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "Venster" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Zoom" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "Uitsluitingsfilters" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "Scanresultaten" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "Mappen laden ..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "Mappen opslaan ..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "Selecteer een directory-bestand om te laden" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "dupeGuru-mappen (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "Selecteer een bestand om uw mappen in op te slaan" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "dupeGuru-mappen (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "Toevoegen" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "Herstel de standaardinstellingen" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "Test tekenreeks" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "Typ hier een reguliere expressie voor python..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "Typ hier een bestandssysteempad of bestandsnaam ..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Deze (hoofdlettergevoelige) reguliere expressies van Python filteren bestanden uit tijdens scans.
    Mappen hebben ook hun standaardstatus ingesteld op \"Uitgesloten\" in het tabblad Mappen als hun naam overeenkomt met een van de reguliere expressies.
    Voor elk verzameld bestand worden twee tests uitgevoerd op elk van hen om te bepalen of ze al dan niet moeten worden uitgefilterd:
  • 1. Reguliere expressies zonder padscheidingsteken worden alleen vergeleken met de bestandsnaam.
  • \n" "
  • 2. Reguliere expressies zonder padscheidingsteken worden vergeleken met het volledige pad naar het bestand.

  • \n" "Voorbeeld: als u de PNG-bestanden alleen uit de map \"My Pictures\" wilt filteren:
    .*My\\sPictures\\\\.*\\.png

    U kunt de reguliere expressie testen met de functie teststring door er een neppad in te plakken:
    C:\\\\User\\My Pictures\\test.png

    \n" "Overeenkomende reguliere expressies worden gemarkeerd.
    Als er ten minste één highlight is, wordt het geteste pad genegeerd tijdens scans.

    Mappen en bestanden die beginnen met een punt '.' worden standaard uitgefilterd.

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "Compilatiefout:" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "Zoom vergroten" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "Zoom verkleinen" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "Normale grootte" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "Beste pasvorm" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "Fotocachemodus:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "Overschrijf themapictogrammen in de werkbalk van de viewer" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" "Gebruik onze eigen interne pictogrammen in plaats van die van de thema-" "engine" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "Toon schuifbalken in afbeeldingsviewers" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" "Als de weergegeven afbeelding niet in de viewport past, laat dan " "schuifbalken zien om de weergave rondom te omspannen" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "" "Gebruik de standaardpositie voor de tabbalk (opnieuw opstarten vereist)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "Plaats de tabbalk onder het hoofdmenu in plaats van ernaast.\n" "Op MacOS vult de tabbalk in plaats daarvan de breedte van het venster." #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "Gebruik vet lettertype voor referenties" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "Referentie voorgrondkleur:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "Referentie achtergrondkleur:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "Delta voorgrondkleur:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "Toon de titelbalk en kan worden gedokt" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" "Terwijl de titelbalk verborgen is, gebruik maken van de speciale toets op " "het zwevende venster slepen" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "" "De titelbalk kan alleen worden uitgeschakeld terwijl het venster is gedokt" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "Verticale titelbalk" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "Wijzig de titelbalk van horizontaal geplaatst, verticale links" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "Tabbladbalk weergeven" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Deze (hoofdlettergevoelige) reguliere expressies van Python filteren bestanden uit tijdens scans.
    Mappen hebben ook hun standaardstatus ingesteld op Uitgesloten in het tabblad Mappen als hun naam toevallig overeenkomt met een van de geselecteerde reguliere expressies.
    Voor elk verzameld bestand worden twee tests uitgevoerd om te bepalen of het al dan niet volledig moet worden genegeerd:
  • 1. Reguliere expressies zonder padscheidingsteken worden alleen vergeleken met de bestandsnaam.
  • \n" "
  • 2. Reguliere expressies met ten minste één padscheidingsteken worden vergeleken met het volledige pad naar het bestand.

  • \n" "
    Voorbeeld: als u .PNG-bestanden alleen uit de map \"My Pictures\" wilt filteren:.*My\\sPictures\\\\.*\\.png

    U kunt de reguliere expressie testen met de \"test string\" -knop nadat u een neppad in het testveld hebt geplakt:
    C:\\\\User\\My Pictures\\test.png

    \n" "Overeenkomende reguliere expressies worden gemarkeerd.
    Als er ten minste één markering is, wordt het geteste pad of de bestandsnaam genegeerd tijdens het scannen.

    Mappen en bestanden die beginnen met een punt '.' worden standaard uitgefilterd.

    " #: qt\app.py:256 msgid "Results" msgstr "Resultaten" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "Algemene interface" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "Resultaattabel" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "Details Venster" #: qt\preferences_dialog.py:285 msgid "General" msgstr "Algemeen" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "Scherm" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" #: qt\app.py:299 msgid "Cache cleared." msgstr "" #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "" #: qt\about_box.py:31 msgid "About {}" msgstr "Over {}" #: qt\about_box.py:47 msgid "Version {}" msgstr "Versie {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "" #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "Licentie verleend onder GPLv3" #: qt\about_box.py:68 msgid "No update available." msgstr "" #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "Foutenrapport" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "Er is iets fout gegaan. Hoe zit het met het melden van de fout?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "Foutrapporten moeten worden gerapporteerd als Github-problemen. U kunt de bovenstaande foutopsporing kopiëren en in een nieuwe uitgave plakken.\n" "\n" "Zorg ervoor dat u van tevoren een zoekopdracht uitvoert naar reeds bestaande problemen. Zorg er ook voor dat u de allernieuwste versie uit de repository test, aangezien de bug die u ondervindt mogelijk al gepatcht is.\n" "\n" "Wat meestal echt helpt, is als je een beschrijving toevoegt van hoe je de fout hebt gekregen. Bedankt!\n" "\n" "Hoewel de toepassing na deze fout zou moeten blijven werken, kan deze in een onstabiele toestand verkeren, dus het wordt aanbevolen de toepassing opnieuw te starten." #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Ga naar Github" #: qt\preferences.py:24 msgid "Czech" msgstr "Tsjechisch" #: qt\preferences.py:25 msgid "German" msgstr "Duits" #: qt\preferences.py:26 msgid "Greek" msgstr "Grieks" #: qt\preferences.py:27 msgid "English" msgstr "Engels" #: qt\preferences.py:28 msgid "Spanish" msgstr "Spaans" #: qt\preferences.py:29 msgid "French" msgstr "Frans" #: qt\preferences.py:30 msgid "Armenian" msgstr "Armeens" #: qt\preferences.py:31 msgid "Italian" msgstr "Italiaans" #: qt\preferences.py:32 msgid "Japanese" msgstr "Japans" #: qt\preferences.py:33 msgid "Korean" msgstr "Koreaans" #: qt\preferences.py:34 msgid "Malay" msgstr "Maleis" #: qt\preferences.py:35 msgid "Dutch" msgstr "Nederlands" #: qt\preferences.py:36 msgid "Polish" msgstr "Pools" #: qt\preferences.py:37 msgid "Brazilian" msgstr "Braziliaans" #: qt\preferences.py:38 msgid "Russian" msgstr "Russisch" #: qt\preferences.py:39 msgid "Turkish" msgstr "Turks" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "Oekraïens" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "Vietnamees" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "(Versimpeld) Chinees" #: qt\recent.py:54 msgid "Clear List" msgstr "Lijst leegmaken" #: qt\search_edit.py:78 msgid "Search..." msgstr "Zoeken..." dupeguru-4.3.1/locale/pl_PL/000077500000000000000000000000001426171743600156375ustar00rootroot00000000000000dupeguru-4.3.1/locale/pl_PL/LC_MESSAGES/000077500000000000000000000000001426171743600174245ustar00rootroot00000000000000dupeguru-4.3.1/locale/pl_PL/LC_MESSAGES/columns.po000066400000000000000000000055321426171743600214510ustar00rootroot00000000000000# Translators: # Fuan , 2021 # msgid "" msgstr "" "Last-Translator: Fuan , 2021\n" "Language-Team: Polish (Poland) (https://www.transifex.com/voltaicideas/teams/116153/pl_PL/)\n" "Language: pl_PL\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "Ścieżka pliku" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "Komunikat o błędzie" #: core\me\prioritize.py:23 msgid "Duration" msgstr "Trwanie" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "Szybkość transmisji" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "Samplerate" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "Nazwa pliku" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "Katalog" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "Rozmiar (MB)" #: core\me\result_table.py:22 msgid "Time" msgstr "Czas" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "Częstotliwość próbkowania" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "Rodzaj" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "Modyfikacja" #: core\me\result_table.py:27 msgid "Title" msgstr "Tytuł" #: core\me\result_table.py:28 msgid "Artist" msgstr "Artysta" #: core\me\result_table.py:29 msgid "Album" msgstr "Album" #: core\me\result_table.py:30 msgid "Genre" msgstr "Gatunek" #: core\me\result_table.py:31 msgid "Year" msgstr "Rok" #: core\me\result_table.py:32 msgid "Track Number" msgstr "Numer utworu" #: core\me\result_table.py:33 msgid "Comment" msgstr "Komentarz" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "Mecz %" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "Użyte słowa" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "Liczba duplikatów" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "Wymiary" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "Rozmiar (KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "Sygnatura czasowa EXIF" #: core\prioritize.py:156 msgid "Size" msgstr "Rozmiar" dupeguru-4.3.1/locale/pl_PL/LC_MESSAGES/core.po000066400000000000000000000156531426171743600207260ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # msgid "" msgstr "" "Last-Translator: Fuan , 2021\n" "Language-Team: Polish (Poland) (https://www.transifex.com/voltaicideas/teams/116153/pl_PL/)\n" "Language: pl_PL\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "Brak wykrytych duplikatów. Nic nie zrobiono." #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "Brak wybranych duplikatów. Nic nie zrobiono." #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" "Masz zamiar otworzyć wiele plików jednocześnie. W zależności od tego, za " "pomocą czego te pliki są otwierane, może to spowodować spory bałagan. " "Kontyntynuj?" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "Wyszukiwanie duplikatów" #: core\app.py:72 msgid "Loading" msgstr "Ładuję" #: core\app.py:73 msgid "Moving" msgstr "Przenoszę" #: core\app.py:74 msgid "Copying" msgstr "Kopiowanie" #: core\app.py:75 msgid "Sending to Trash" msgstr "Wysyłam do kosza" #: core\app.py:289 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" "Wciąż wisi tam poprzednia akcja. Nie możesz jeszcze rozpocząć nowego. " "Poczekaj kilka sekund i spróbuj ponownie." #: core\app.py:300 msgid "No duplicates found." msgstr "Nie znaleziono duplikatów." #: core\app.py:315 msgid "All marked files were copied successfully." msgstr "Wszystkie zaznaczone pliki zostały pomyślnie skopiowane." #: core\app.py:317 msgid "All marked files were moved successfully." msgstr "Wszystkie zaznaczone pliki zostały pomyślnie przeniesione." #: core\app.py:319 msgid "All marked files were deleted successfully." msgstr "" #: core\app.py:321 msgid "All marked files were successfully sent to Trash." msgstr "Wszystkie zaznaczone pliki zostały pomyślnie wysłane do kosza." #: core\app.py:326 msgid "Could not load file: {}" msgstr "Nie udało się załadować pliku: {}" #: core\app.py:382 msgid "'{}' already is in the list." msgstr "'{}' jest już na liście." #: core\app.py:384 msgid "'{}' does not exist." msgstr "'{}' nie istnieje." #: core\app.py:392 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "" "Wszystkie zaznaczone %d duplikaty będą ignorowane w kolejnych skanach. " "Kontynuować?" #: core\app.py:469 msgid "Select a directory to copy marked files to" msgstr "Wybierz katalog, do którego chcesz skopiować zaznaczone pliki" #: core\app.py:471 msgid "Select a directory to move marked files to" msgstr "Wybierz katalog, do którego chcesz przenieść zaznaczone pliki" #: core\app.py:510 msgid "Select a destination for your exported CSV" msgstr "Wybierz miejsce docelowe dla eksportowanego pliku CSV" #: core\app.py:516 core\app.py:771 core\app.py:781 msgid "Couldn't write to file: {}" msgstr "Nie udało się zapisać do pliku: {}" #: core\app.py:539 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" "Nie masz skonfigurowanego polecenia niestandardowego. Ustaw to w swoich " "preferencjach." #: core\app.py:695 core\app.py:707 msgid "You are about to remove %d files from results. Continue?" msgstr "Zamierzasz usunąć %d plików z wyników. Kontyntynuj?" #: core\app.py:743 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} zduplikowanych grup zmieniono przez ponowne ustalenie priorytetów." #: core\app.py:790 msgid "The selected directories contain no scannable file." msgstr "Wybrane katalogi nie zawierają plik skanowalną." #: core\app.py:803 msgid "Collecting files to scan" msgstr "Zbieranie plików do skanowania" #: core\app.py:850 msgid "%s (%d discarded)" msgstr "%s(%d odrzucone)" #: core\directories.py:191 msgid "Collected {} files to scan" msgstr "" #: core\directories.py:207 msgid "Collected {} folders to scan" msgstr "" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "Wysyłasz {} plików do Kosza" #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "Wyrażenia regularne" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "Czy na pewno chcesz usunąć wszystkie %d pozycji z listy ignorowanych?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "Nazwa pliku" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "Nazwa pliku - pola" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "Nazwa pliku - pola (bez kolejności)" #: core\me\scanner.py:23 msgid "Tags" msgstr "Tagi" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "Treść" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "Analizowane %d/%d zdjęć" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "Wykonano %d/%d dopasowań fragmentów" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "Przygotowanie do dopasowania" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "Zweryfikowane %d/%d meczów" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "Przeczytaj EXIF z %d/%d zdjęć" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "Sygnatura czasowa EXIF" #: core\prioritize.py:70 msgid "None" msgstr "Nie" #: core\prioritize.py:100 msgid "Ends with number" msgstr "Kończy się numerem" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "Nie kończy się liczbą" #: core\prioritize.py:102 msgid "Longest" msgstr "Najdłużej" #: core\prioritize.py:103 msgid "Shortest" msgstr "Najkrótsza" #: core\prioritize.py:140 msgid "Highest" msgstr "Najwyższa" #: core\prioritize.py:140 msgid "Lowest" msgstr "Najniższa" #: core\prioritize.py:169 msgid "Newest" msgstr "Najnowsza" #: core\prioritize.py:169 msgid "Oldest" msgstr "Najstarszy" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) duplikaty oznakowane." #: core\results.py:141 msgid " filter: %s" msgstr " filtr: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "Odczytaj rozmiar %d/%d plików" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "Przeczytaj metadane %d/%d plików" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "Prawie skończone! Porządkowanie wyników..." #: core\se\scanner.py:18 msgid "Folders" msgstr "Katalogi" dupeguru-4.3.1/locale/pl_PL/LC_MESSAGES/ui.po000066400000000000000000001046121426171743600204050ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2022 # Fuan , 2022 # msgid "" msgstr "" "Last-Translator: Fuan , 2022\n" "Language-Team: Polish (Poland) (https://www.transifex.com/voltaicideas/teams/116153/pl_PL/)\n" "Language: pl_PL\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" #: qt/app.py:81 msgid "Quit" msgstr "Wyjdź" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "Opcje" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Lista ignorowanych" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Wyczyść pamięć podręczną obrazów" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Pomoc dupeGuru" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "O dupeGuru" #: qt/app.py:87 msgid "Open Debug Log" msgstr "Otwórz Log aplikacji" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "Czy na pewno chcesz usunąć całą analizę obrazów z pamięci podręcznej?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "Obraz cache wyczyszczone." #: qt/app.py:251 msgid "{} file (*.{})" msgstr "Plik {} (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Opcje usuwania" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Twórz dowiązania dla usuniętych plików" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" "Po usunięciu duplikatu, utwórz link wskazujący na plik referencyjny w " "miejsce usuniętego pliku." #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "Twardy link" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "Symlink" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr "(niepodparta)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Usuwaj pliki zamiast przenosić do kosza" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "Zamiast przenosić pliki do kosza, usuwaj je. Ta opcja może służyć za " "obejście, jeśli przenoszenie do kosza nie działa." #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Kontynuuj" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Anuluj" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "Właściwość" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "Zaznaczony" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "Odwołanie" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "Wczytaj wyniki..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "Okno wyników" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "Dodaj folder..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "Plik" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "Widok" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "Pomoc" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "Wczytaj ostatnie wyniki" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "Tryb aplikacji" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "Muzyka" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "Obrazek" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "Standard" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "Rodzaj skanowania:" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "Więcej opcji" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "Wybierz foldery do przeskanowania i wciśnij \"Skanuj\"." #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "Wczytaj wyniki" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "Skanuj" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "Niezapisane wyniki" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "Masz niezapisane wyniki wyszukiwania, czy na pewno chcesz wyjść?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "Wybierz folder aby dodać go do listy do przeskanowania" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "Wybierz plik wyniku skanowania do wczytania" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "Wszystkie pliki (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "Wyniki dupeGuru (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "Rozpocznij nowe skanowanie" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "" "Masz niezapisane wyniki wyszukiwania, czy na pewno chcesz kontynuować?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "Nazwa" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "Stan" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "Wykluczony" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "Normalny" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "Usuń zaznaczone" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "Wyczyść" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "Zamknij" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "Szczegóły" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "Tagi do skanowania" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "ścieżka" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "Artysta" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "Album" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "Tytuł" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "Gatunek" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "Rok" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "Ważenie słów" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "Dopasuj podobne słowa" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "Może mieszać rodzaj pliku" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "Podczas filtrowania używaj wyrażeń regularnych" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "Usuń puste foldery podczas usuwania lub przenoszenia" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "Ignoruj duplikaty dowiązujące do tego samego pliku" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "tryb debugowania (wymaga restartu)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "Zgadzają zdjęć z różnych wymiarach" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "Siła filtru:" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "Więcej wyników" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "Mniej wyników" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "Wielkość czcionki:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "Język:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "Kopiuj i przenieś:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "W miejscu przeznaczenia" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "Ponowne utworzenie ścieżki względnej" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "Ponownie utworzyć ścieżkę bezwzględną" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "" "Własne polecenie (argumenty: %d dla duplikatu, %r dla pliku referencyjnego):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "" "dupeGuru musi zostać ponownie uruchomiona, aby zmiany języka zaczęły " "obowiązywać." #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "Zmień priorytet duplikatów" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "Dodaj kryteria w prawym polu i kliknij OK, aby wysłać duplikaty, które " "najlepiej odpowiadają tym kryteriom, do pozycji odniesienia ich odpowiedniej" " grupy. Przeczytaj plik pomocy, aby uzyskać więcej informacji." #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "Problemy!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "Wystąpiły problemy podczas przetwarzania niektórych (lub wszystkich) plików." " Przyczyny tych problemów opisano w poniższej tabeli. Te pliki nie zostały " "usunięte z wyników." #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "Pokaż wybrane" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "Działania" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "Tylko pokaż Duplikaty" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "Pokaż wartości delta" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "Wyślij Oznaczono do Kosza..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "Przenieś zaznaczone do ..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "Kopiuj zaznaczone do ..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "Usuń Oznaczono z wyników" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "Zmień priorytety wyników..." #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "Usuń wybrane z wyników" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "Dodaj wybrane do listy ignorowanych" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "Zmień wybrany element w element referencyjny" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "Otwórz wybrane z domyślną aplikacją" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "Otwórz folder zawierający wybrany element" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "Zmień nazwę wybranych" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "Zaznacz wszystko" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "Zaznacz brak" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "Odwróć znakowanie" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "Zaznacz wybrane" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "Eksportuj do HTML" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "Eksportuj do CSV" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "Zapisz wyniki..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "Wywołaj polecenie niestandardowe" #: qt/result_window.py:102 msgid "Mark" msgstr "Zaznacz" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "Kolumny" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "Przywróć domyślne" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} Wyniki" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "Tylko duplikaty" #: qt/result_window.py:194 msgid "Delta Values" msgstr "Wartości delta" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "Wybierz plik, aby zapisać wyniki do" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "Ignoruj pliki mniejsze niż" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ Wyniki" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "Akcję" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "Dodaj nowy folder..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "Zaawansowane" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "Automatycznie sprawdzać aktualizacje" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "Podstawowe" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "Przenieś wszystko na wierzch" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "Sprawdź aktualizacje..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "Zamknij okno" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "Kopia" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "" "Własne polecenie (argumenty: %d dla duplikatu, %r dla pliku referencyjnego):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "Wytnij" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Delta" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "Szczegóły wybranego pliku" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "Panel szczegółów" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "Katalogi" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "dupeGuru Preferencje" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "dupeGuru Wyniki" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "dupeGuru Witryna" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "Edytuj" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "Eksportuj wyniki do CSV" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "Eksportuj wyniki do XHTML" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "Mniej wyników" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "Filtr" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "Twardość filtra:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Filtruj wyniki..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "Okno wyboru folderu" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "Rozmiar czcionki:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "Ukryj dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "Ukryj inne" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "Ignoruj pliki mniejsze niż:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "Wczytaj z pliku..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "Zminimalizować" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "Tryb" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "Więcej wyników" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "Ok" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "Wklej" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "Preferencje..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "Szybkie spojrzenie" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "Zamknij dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "Przywróć domyślne" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "Przywróć domyślne" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "Odsłonić" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Pokaż wybrane w Finderze" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "Zaznacz wszystko" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Wyślij Oznaczono do Kosza..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "Usługi" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "Pokaż wszystkie" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "Rozpocznij skanowanie duplikatów" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "Nazwa '%@' już istnieje." #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "Okno" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Powiększenie" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "Filtry wykluczenia" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "Wyniki skanowania" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "Wczytaj katalogi..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "Zapisz katalogi..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "Wybierz plik katalogów do załadowania" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "dupeGuru Katalogi (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "Wybierz plik, w którym chcesz zapisać swoje katalogi" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "dupeGuru Katalogi (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "Dodaj" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "Przywróć domyślne" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "Ciąg testowy" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "Wpisz python wyrażenie regularne tutaj..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "Wpisz ścieżkę systemu plików lub nazwę pliku tutaj..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Te wyrażenia regularne Pythona (uwzględniające wielkość liter) odfiltrują pliki podczas skanowania.
    Katalogi będą miały również domyślny stan ustawiony na Wykluczone na karcie Katalogi, jeśli ich nazwa będzie pasować do jednego z wybranych wyrażeń regularnych.
    Dla każdego zebranego pliku wykonywane są dwa testy, aby określić, czy należy go całkowicie zignorować:
  • 1. Wyrażenia regularne bez separatora ścieżek będą porównywane tylko z nazwą pliku.
  • \n" "
  • 2. Wyrażenia regularne zawierające przynajmniej jeden separator ścieżki zostaną porównane z pełną ścieżką do pliku.

  • \n" "Przykład: jeśli chcesz odfiltrować pliki PNG tylko z katalogu \"My Pictures\":
    .*My\\sPictures\\\\.*\\.png

    Możesz przetestować wyrażenie regularne przyciskiem „ciąg testowy” po wklejeniu fałszywej ścieżki w polu testowym:
    C:\\\\User\\My Pictures\\test.png

    \n" "Pasujące wyrażenia regularne zostaną podświetlone.
    Jeśli istnieje co najmniej jedno podświetlenie, testowana ścieżka lub nazwa pliku zostanie zignorowana podczas skanowania.

    Katalogi i pliki zaczynające się od kropki \".\" są domyślnie odfiltrowywane.

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "Błąd kompilacji:" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "Zwiększ zoom" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "Zmniejsz zoom" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "Normalny rozmiar" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "Najlepiej dopasowana" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "Tryb pamięci podręcznej obrazów:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "Zastąp ikony motywów na pasku narzędzi przeglądarki." #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" "Użyj naszych własnych wewnętrznych ikon zamiast ikon dostarczonych przez " "silnik motywu." #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "Pokaż paski przewijania w widzów obrazu" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" "Jeśli wyświetlany obraz nie mieści się w rzutni, pokaż paski przewijania, " "aby przesunąć widok" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "Użyj domyślnej pozycji paska kart (wymaga ponownego uruchomienia)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "Umieść pasek kart poniżej menu głównego zamiast obok.\n" "W systemie MacOS pasek kart będzie zamiast tego wypełniał szerokość okna." #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "Użyj pogrubioną czcionką o referencje" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "Kolor pierwszego planu dla referencyjnego:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "Kolor tła dla referencyjnego:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "Kolor pierwszego planu delta:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "Pokaż pasek tytułu i można go zadokować" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" "Gdy pasek tytułu jest ukryty, użyj klawisza modyfikującego, aby przeciągnąć " "pływające okno" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "Pasek tytułu można wyłączyć tylko wtedy, gdy okno jest zadokowane" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "Pionowy pasek tytułu" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "Zmień pasek tytułu z poziomego u góry na pionowy po lewej stronie" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "Pokaż pasek kart" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Te wyrażenia regularne Pythona (uwzględniające wielkość liter) odfiltrują pliki podczas skanowania.
    Katalogi będą miały również domyślny stan ustawiony na Wykluczone na karcie Katalogi, jeśli ich nazwa będzie pasować do jednego z wybranych wyrażeń regularnych.
    Dla każdego zebranego pliku wykonywane są dwa testy, aby określić, czy należy go całkowicie zignorować:
  • 1. Wyrażenia regularne bez separatora ścieżek będą porównywane tylko z nazwą pliku.
  • \n" "
  • 2. Wyrażenia regularne zawierające przynajmniej jeden separator ścieżki zostaną porównane z pełną ścieżką do pliku.

  • \n" "Przykład: jeśli chcesz odfiltrować pliki PNG tylko z katalogu \"My Pictures\":
    .*My\\sPictures\\\\.*\\.png

    Możesz przetestować wyrażenie regularne przyciskiem „ciąg testowy” po wklejeniu fałszywej ścieżki w polu testowym:
    C:\\\\User\\My Pictures\\test.png

    \n" "Pasujące wyrażenia regularne zostaną podświetlone.
    Jeśli istnieje co najmniej jedno podświetlenie, testowana ścieżka lub nazwa pliku zostanie zignorowana podczas skanowania.

    Katalogi i pliki zaczynające się od kropki \".\" są domyślnie odfiltrowywane.

    " #: qt\app.py:256 msgid "Results" msgstr "Wyniki" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "Interfejs ogólny" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "Tabela wyników" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "Okno szczegółów" #: qt\preferences_dialog.py:285 msgid "General" msgstr "Generał" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "Pokaz" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" #: qt\app.py:299 msgid "Cache cleared." msgstr "" #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "" #: qt\about_box.py:31 msgid "About {}" msgstr "O {}" #: qt\about_box.py:47 msgid "Version {}" msgstr "Wersja {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "" #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "Licencjonowany w ramach GPLv3" #: qt\about_box.py:68 msgid "No update available." msgstr "" #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "Raport błędów" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "Coś poszło nie tak. Co powiesz na zgłoszenie błędu?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "Raporty o błędach powinny być zgłaszane jako problemy z Github. Możesz skopiować powyższy opis błędu i wkleić go w nowym zgłoszeniu.\n" "\n" "Upewnij się, że wcześniej wyszukałeś już istniejący bilet. Upewnij się również, że przetestowałeś najnowszą wersję dostępną w repozytorium, ponieważ napotkany błąd mógł już zostać załatany.\n" "\n" "To, co zwykle naprawdę pomaga, to dodanie opisu tego, w jaki sposób wystąpił błąd. Dzięki!\n" "\n" "Chociaż aplikacja powinna nadal działać po tym błędzie, może być w stanie niestabilnym, dlatego zaleca się ponowne uruchomienie aplikacji." #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Przejdź do Github" #: qt\preferences.py:24 msgid "Czech" msgstr "Czech" #: qt\preferences.py:25 msgid "German" msgstr "Niemiecki" #: qt\preferences.py:26 msgid "Greek" msgstr "Grecki" #: qt\preferences.py:27 msgid "English" msgstr "Angielski" #: qt\preferences.py:28 msgid "Spanish" msgstr "Hiszpański" #: qt\preferences.py:29 msgid "French" msgstr "Francuski" #: qt\preferences.py:30 msgid "Armenian" msgstr "Ormiański" #: qt\preferences.py:31 msgid "Italian" msgstr "Włoski" #: qt\preferences.py:32 msgid "Japanese" msgstr "Japońsku" #: qt\preferences.py:33 msgid "Korean" msgstr "Koreański" #: qt\preferences.py:34 msgid "Malay" msgstr "Malajski" #: qt\preferences.py:35 msgid "Dutch" msgstr "Holenderski" #: qt\preferences.py:36 msgid "Polish" msgstr "Polskie" #: qt\preferences.py:37 msgid "Brazilian" msgstr "Brazylijski" #: qt\preferences.py:38 msgid "Russian" msgstr "Rosyjski" #: qt\preferences.py:39 msgid "Turkish" msgstr "Turecki" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "Ukraiński" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "Wietnamski" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "Chiński (uproszczony)" #: qt\recent.py:54 msgid "Clear List" msgstr "Wyczyść listę" #: qt\search_edit.py:78 msgid "Search..." msgstr "Szukaj..." dupeguru-4.3.1/locale/pt_BR/000077500000000000000000000000001426171743600156375ustar00rootroot00000000000000dupeguru-4.3.1/locale/pt_BR/LC_MESSAGES/000077500000000000000000000000001426171743600174245ustar00rootroot00000000000000dupeguru-4.3.1/locale/pt_BR/LC_MESSAGES/columns.po000066400000000000000000000052771426171743600214570ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # msgid "" msgstr "" "Last-Translator: Andrew Senetar , 2021\n" "Language-Team: Portuguese (Brazil) (https://www.transifex.com/voltaicideas/teams/116153/pt_BR/)\n" "Language: pt_BR\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "Caminho" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "Mensagem de Erro" #: core\me\prioritize.py:23 msgid "Duration" msgstr "Duração" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "Taxa de Bits" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "Amostragem" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "Nome do Arquivo" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "Pasta" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "Tamanho" #: core\me\result_table.py:22 msgid "Time" msgstr "Duração" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "Tamanho da Amostra" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "Tipo" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "Modificado" #: core\me\result_table.py:27 msgid "Title" msgstr "Nome" #: core\me\result_table.py:28 msgid "Artist" msgstr "Artista" #: core\me\result_table.py:29 msgid "Album" msgstr "Álbum" #: core\me\result_table.py:30 msgid "Genre" msgstr "Gênero" #: core\me\result_table.py:31 msgid "Year" msgstr "Ano" #: core\me\result_table.py:32 msgid "Track Number" msgstr "Número da Faixa" #: core\me\result_table.py:33 msgid "Comment" msgstr "Comentário" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "% Precisão" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "Palavras Usadas" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "Duplicatas" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "Dimensões" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "Tamanho" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "Timestamp EXIF" #: core\prioritize.py:156 msgid "Size" msgstr "Tamanho" dupeguru-4.3.1/locale/pt_BR/LC_MESSAGES/core.po000066400000000000000000000153011426171743600207140ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # msgid "" msgstr "" "Last-Translator: Fuan , 2021\n" "Language-Team: Portuguese (Brazil) (https://www.transifex.com/voltaicideas/teams/116153/pt_BR/)\n" "Language: pt_BR\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "Não há duplicatas marcadas. Nada foi feito." #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "Não há duplicatas selecionadas. Nada foi feito." #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" "Você está prestes a abrir muitos arquivos de uma vez. Problemas podem surgir" " dependendo de qual app seja usado para abri-los. Deseja continuar?" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "Buscando por duplicatas" #: core\app.py:72 msgid "Loading" msgstr "Carregando" #: core\app.py:73 msgid "Moving" msgstr "Movendo" #: core\app.py:74 msgid "Copying" msgstr "Copiando" #: core\app.py:75 msgid "Sending to Trash" msgstr "Movendo para o Lixo" #: core\app.py:289 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" "Ainda há uma ação em andamento. Não é possível iniciar outra agora. Espere " "alguns segundos e tente novamente." #: core\app.py:300 msgid "No duplicates found." msgstr "Nenhuma duplicata encontrada." #: core\app.py:315 msgid "All marked files were copied successfully." msgstr "Todos os arquivos marcados foram copiados corretamente." #: core\app.py:317 msgid "All marked files were moved successfully." msgstr "Todos os arquivos marcados foram relocados corretamente." #: core\app.py:319 msgid "All marked files were deleted successfully." msgstr "" #: core\app.py:321 msgid "All marked files were successfully sent to Trash." msgstr "Todos os arquivos marcados foram movidos para o Lixo corretamente." #: core\app.py:326 msgid "Could not load file: {}" msgstr "Não foi possível carregar o arquivo: {}" #: core\app.py:382 msgid "'{}' already is in the list." msgstr "‘{}’ já está na lista." #: core\app.py:384 msgid "'{}' does not exist." msgstr "‘{}’ não existe." #: core\app.py:392 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "Excluir %d duplicata(s) selecionada(s) de escaneamentos posteriores?" #: core\app.py:469 msgid "Select a directory to copy marked files to" msgstr "Selecione um diretório onde deseja copiar os arquivos marcados" #: core\app.py:471 msgid "Select a directory to move marked files to" msgstr "Selecione um diretório para onde deseja mover os arquivos marcados" #: core\app.py:510 msgid "Select a destination for your exported CSV" msgstr "Selecione uma pasta para o CSV exportado" #: core\app.py:516 core\app.py:771 core\app.py:781 msgid "Couldn't write to file: {}" msgstr "Não foi possível gravar no arquivo: {}" #: core\app.py:539 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" "Você não possui nenhum comando personalizado. Crie um nas preferências." #: core\app.py:695 core\app.py:707 msgid "You are about to remove %d files from results. Continue?" msgstr "Remover %d arquivo(s) dos resultados?" #: core\app.py:743 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} grupos de duplicatas alterados ao repriorizar." #: core\app.py:790 msgid "The selected directories contain no scannable file." msgstr "As pastas selecionadas não contém arquivos escaneáveis." #: core\app.py:803 msgid "Collecting files to scan" msgstr "Juntando arquivos para escanear" #: core\app.py:850 msgid "%s (%d discarded)" msgstr "%s (%d rejeitado(s))" #: core\directories.py:191 msgid "Collected {} files to scan" msgstr "" #: core\directories.py:207 msgid "Collected {} folders to scan" msgstr "" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "Você está movendo {} arquivo(s) para o Lixo." #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "Expressões regulares" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "Tem certeza de que deseja remover todos os %d itens da lista Ignorar?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "Nome do arquivo" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "Nome do arquivo - campos" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "Nome do arquivo - campos (sem pedido)" #: core\me\scanner.py:23 msgid "Tags" msgstr "Tags" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "Conteúdo" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "%d/%d fotos analizadas" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "%d/%d resultados em blocos executados" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "Preparando para comparação" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "%d/%d resultados verificados" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "EXIF lido em %d/%d fotos" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "Timestamp EXIF" #: core\prioritize.py:70 msgid "None" msgstr "Nenhum" #: core\prioritize.py:100 msgid "Ends with number" msgstr "Termina com número" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "Não termina com número" #: core\prioritize.py:102 msgid "Longest" msgstr "Mais longo" #: core\prioritize.py:103 msgid "Shortest" msgstr "Mais curto" #: core\prioritize.py:140 msgid "Highest" msgstr "Maior" #: core\prioritize.py:140 msgid "Lowest" msgstr "Menor" #: core\prioritize.py:169 msgid "Newest" msgstr "Mais recente" #: core\prioritize.py:169 msgid "Oldest" msgstr "Mais antigo" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) duplicatas marcadas." #: core\results.py:141 msgid " filter: %s" msgstr " filtro: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "Tamanho lido em %d/%d arquivos" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "Metadados lidos em %d/%d arquivos" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "Quase pronto! Mexendo nos resultados ..." #: core\se\scanner.py:18 msgid "Folders" msgstr "Pastas" dupeguru-4.3.1/locale/pt_BR/LC_MESSAGES/ui.po000066400000000000000000001043541426171743600204100ustar00rootroot00000000000000# Translators: # Fuan , 2022 # Andrew Senetar , 2022 # msgid "" msgstr "" "Last-Translator: Andrew Senetar , 2022\n" "Language-Team: Portuguese (Brazil) (https://www.transifex.com/voltaicideas/teams/116153/pt_BR/)\n" "Language: pt_BR\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: qt/app.py:81 msgid "Quit" msgstr "Encerrar" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "Opções" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Lista Ignorar" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Apagar Cache de Fotos" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Ajuda dupeGuru" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "Sobre o dupeGuru" #: qt/app.py:87 msgid "Open Debug Log" msgstr "Abrir Registro de Depuração" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "Deseja remover todo o cache das fotos já analizadas?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "Cache de fotos apagado." #: qt/app.py:251 msgid "{} file (*.{})" msgstr "Arquivo {} (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Opções de Apagamento" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Criar link dos arquivos apagados" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" "Após apagar uma duplicata, cria um link direcionado ao arquivo original para" " substituir o arquivo apagado." #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "Hardlink" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "Symlink" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr "(incompatível)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Apagar arquivos imediatamente" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "Apaga os arquivos imediatamente ao invés de movê-los para o Lixo. Essa opção" " é usada como alternativa para quando o método normal falha." #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Continuar" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Cancelar" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "Atributo" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "Seleção" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "Referência" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "Carregar…" #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "Janela de Resultados" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "Adicionar Pasta…" #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "Arquivo" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "Visualização" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "Ajuda" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "Carregar Resultados Recentes" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "Modo de Aplicação:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "Música" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "Imagem" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "Padrão" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "Tipo de Scan:" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "Mais opções" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "Selecione as pastas desejadas e clique em “Escanear”." #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "Carregar" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "Escanear" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "Resultados não salvos" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "Você possui resultados não salvos, deseja encerrar assim mesmo?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "Selecione uma pasta para adicionar ao escaneamento" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "Selecione um resultado para carregar" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "Todos os Arquivos (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "Resultados do dupeGuru (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "Iniciar um novo escaneamento" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "Você possui resultados não salvos, deseja continuar mesmo assim?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "Nome" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "Estado" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "Excluído" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "Normal" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "Remover Seleção" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "Limpar" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "Fechar" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "Detalhes" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "Escanear Tags:" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "Faixa" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "Artista" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "Álbum" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "Nome" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "Gênero" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "Ano" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "Importância da palavra" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "Coincidir palavras similares" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "Pode misturar tipo de arquivo" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "Usar expressões regulares ao filtrar" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "Remover pastas vazias ao apagar ou mover" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "Ignorar duplicatas de hardlink a um mesmo arquivo" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "Modo de Depuração (requer reinício)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "Coincidir fotos de dimensões diferentes" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "Pressão do Filtro:" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "+ Resultados" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "- Resultados" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "Tam. fonte:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "Idioma:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "Copiar e Mover:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "Exatamente no destino" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "Recriar caminho relativo" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "Recriar caminho absoluto" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "Comando personalizado (argumentos: %d (dup), %r (ref)):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "" "É necessário reiniciar o dupeGuru para que as mudanças de idioma surtam " "efeito." #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "Repriorizar duplicatas" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "Adicione critérios à caixa da direita e clique em OK para elevar as " "duplicatas à posição de referência em seus respectivos grupos, baseado nos " "critérios escolhidos. Leia a Ajuda para maiores informações." #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "Problemas!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "Problemas ao processar alguns (ou todos) os arquivos. A causa dos problemas " "está detalhada abaixo. Esses arquivos não foram removidos dos seus " "resultados." #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "Mostrar Seleção" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "Ações" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "Mostrar Somente Duplicatas" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "Mostrar Valores Delta" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "Mover Marcados para o Lixo…" #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "Mover Marcados para…" #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "Copiar Marcados para…" #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "Remover Marcados dos Resultados" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "Repriorizar Resultados…" #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "Remover Seleção dos Resultados" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "Adicionar Seleção à Lista Ignorar" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "Fazer da Seleção Referência" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "Abrir Seleção com Aplicativo Padrão" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "Abrir Pasta da Seleção" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "Renomear Seleção" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "Marcar Tudo" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "Desmarcar Tudo" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "Inverter Marcação" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "Marcar Seleção" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "Exportar como HTML" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "Exportar como CSV" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "Salvar Resultados…" #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "Executar Comando Personalizado" #: qt/result_window.py:102 msgid "Mark" msgstr "Marcar" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "Colunas" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "Restaurar Padrões" #: qt/result_window.py:185 msgid "{} Results" msgstr "Resultados do {}" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "Duplicatas" #: qt/result_window.py:194 msgid "Delta Values" msgstr "Valores Delta" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "Selecione onde salvar seus resultados" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "Ignorar arquivos menores que" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "Resultados do %@" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "Ação" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "Adicionar Pasta…" #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "Avançado" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "Buscar atualizações automaticamente" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "Básico" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "Trazer Todas para Frente" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "Buscar Atualizações…" #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "Fechar Janela" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "Copiar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "Comando personalizado (argumentos: %d (dup), %r (ref)):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "Cortar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Delta" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "Detalhes do Arquivo Selecionado" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "Painel de Detalhes" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "Pastas" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "Preferências do dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "Resultados do dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "Site do dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "Editar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "Exportar Resultados para CSV" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "Exportar Resultados para XHTML" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "- resultados" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "Filtrar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "Pressão do filtro:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Filtrar Resultados…" #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "Janela de Seleção de Pasta" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "Tam. Fonte:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "Ocultar dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "Ocultar Outros" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "Ignorar arquivos menores que:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "Carregar do arquivo…" #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "Minimizar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "Modo" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "+ resultados" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "Ok" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "Colar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "Preferências…" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "Visualização Rápida" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "Encerrar dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "Restaurar Padrões" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "Restaurar Padrões" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "Mostrar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Mostrar Seleção no Finder" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "Selecionar Tudo" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Mover Marcados para o Lixo…" #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "Serviços" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "Mostrar Tudo" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "Iniciar Escaneamento de Duplicata" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "O nome ‘%@’ já existe." #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "Janela" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Ampliar/Reduzir" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "Filtros de Exclusão" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "Resultados da varredura" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "Carregar diretórios..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "Salvar diretórios..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "Selecione um arquivo de diretórios para carregar" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "Diretórios de dupeGuru (* .dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "Selecione um arquivo para salvar seus diretórios" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "Diretórios de dupeGuru (* .dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "Adicionar" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "Restaurar padrões" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "String de teste" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "Digite uma expressão regular python aqui..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "Digite um caminho ou nome de arquivo do sistema de arquivos aqui..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Essas expressões regulares Python (diferenciando maiúsculas e minúsculas) filtrarão os arquivos durante as varreduras.
    Directores também terão seu estado padrão definido como \"Excluído\" na guia Diretórios se seu nome corresponder a uma das expressões regulares selecionadas.
    Para cada arquivo coletado, dois testes são realizados para determinar se deve ou não ignorá-lo completamente:
  • \n" "1. Expressões regulares sem separador de caminho serão comparadas apenas ao nome do arquivo.
  • 2. Expressões regulares com pelo menos um separador de caminho serão comparadas ao caminho completo para o arquivo.

  • \n" "Exemplo: se você deseja filtrar arquivos .PNG apenas do diretório \"Minhas imagens\":
    *Minhas\\sImagens\\\\.*\\.png

    Você pode testar a expressão regular com o botão \"string de teste\" após colar um caminho falso no campo de teste:
    C:\\\\Usuário\\Minhas Imagens\\test.png

    \n" "As expressões regulares correspondentes serão destacadas.
    Se houver pelo menos um destaque, o caminho ou nome do arquivo testado será ignorado durante as varreduras.

    Diretórios e arquivos que começam com um ponto '.' são filtrados por padrão.

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "Erro de compilação:" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "Aumentar zoom" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "Diminuir zoom" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "Tamanho normal" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "Melhor ajuste" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "Modo de cache de imagem:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "Substituir os ícones do tema na barra de ferramentas do visualizador" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" "Use nossos próprios ícones internos em vez dos fornecidos pelo mecanismo de " "tema" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "Mostrar barras de rolagem em visualizadores de imagens" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" "Quando a imagem exibida não couber na janela de visualização, mostre as " "barras de rolagem para abranger a visualização" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "Use a posição padrão para a barra de guias (requer reinicialização)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "Coloque a barra de guias abaixo do menu principal em vez de ao lado dele\n" "No MacOS, a barra de guias preencherá a largura da janela." #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "Use fonte em negrito para referências" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "Cor de primeiro plano de referência:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "Cor de fundo de referência:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "Cor de primeiro plano do delta:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "Mostra a barra de título e pode ser encaixada" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" "Enquanto a barra de título está oculta, use a tecla modificadora para " "arrastar a janela flutuante" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "" "A barra de título só pode ser desativada enquanto a janela está encaixada" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "Barra de título vertical" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "" "Altere a barra de título de horizontal na parte superior para vertical no " "lado esquerdo" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "Mostrar barra de abas" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Essas expressões regulares Python (diferenciando maiúsculas e minúsculas) filtrarão os arquivos durante as varreduras.
    Directores também terão seu estado padrão definido como \"Excluído\" na guia Diretórios se seu nome corresponder a uma das expressões regulares selecionadas.
    Para cada arquivo coletado, dois testes são realizados para determinar se deve ou não ignorá-lo completamente:
  • \n" "1. Expressões regulares sem separador de caminho serão comparadas apenas ao nome do arquivo.
  • 2. Expressões regulares com pelo menos um separador de caminho serão comparadas ao caminho completo para o arquivo.

  • \n" "Exemplo: se você deseja filtrar arquivos .PNG apenas do diretório \"Minhas imagens\":
    *Minhas\\sImagens\\\\.*\\.png

    Você pode testar a expressão regular com o botão \"string de teste\" após colar um caminho falso no campo de teste:
    C:\\\\Usuário\\Minhas Imagens\\test.png

    \n" "As expressões regulares correspondentes serão destacadas.
    Se houver pelo menos um destaque, o caminho ou nome do arquivo testado será ignorado durante as varreduras.

    Diretórios e arquivos que começam com um ponto '.' são filtrados por padrão.

    " #: qt\app.py:256 msgid "Results" msgstr "Resultados" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "Interface Geral" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "Tabela de Resultados" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "Janela de Detalhes" #: qt\preferences_dialog.py:285 msgid "General" msgstr "Geral" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "Exibição" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" #: qt\app.py:299 msgid "Cache cleared." msgstr "" #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "" #: qt\about_box.py:31 msgid "About {}" msgstr "Sobre o {}" #: qt\about_box.py:47 msgid "Version {}" msgstr "Versão {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "" #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "Licenciado sob GPLv3" #: qt\about_box.py:68 msgid "No update available." msgstr "" #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "Relatório de Erro" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "Algo deu errado. Deseja relatar o erro?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "Os relatórios de erros devem ser relatados como problemas do Github. Você pode copiar o rastreamento do erro acima e colá-lo em uma nova edição.\n" "\n" "Por favor, certifique-se de executar uma pesquisa de qualquer problema já existente com antecedência. Certifique-se também de testar a versão mais recente disponível no repositório, uma vez que o bug que você está enfrentando pode já ter sido corrigido.\n" "\n" "O que geralmente ajuda muito é adicionar uma descrição de como o erro ocorreu. Obrigado!\n" "\n" "Embora o aplicativo deva continuar a ser executado após esse erro, ele pode estar em um estado instável, portanto, é recomendável reiniciar o aplicativo." #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Ir para o Github" #: qt\preferences.py:24 msgid "Czech" msgstr "Tcheco" #: qt\preferences.py:25 msgid "German" msgstr "Alemão" #: qt\preferences.py:26 msgid "Greek" msgstr "Grega" #: qt\preferences.py:27 msgid "English" msgstr "Inglês" #: qt\preferences.py:28 msgid "Spanish" msgstr "Espanhol" #: qt\preferences.py:29 msgid "French" msgstr "Francês" #: qt\preferences.py:30 msgid "Armenian" msgstr "Armênio" #: qt\preferences.py:31 msgid "Italian" msgstr "Italiano" #: qt\preferences.py:32 msgid "Japanese" msgstr "Japonês" #: qt\preferences.py:33 msgid "Korean" msgstr "Coreano" #: qt\preferences.py:34 msgid "Malay" msgstr "Malaio" #: qt\preferences.py:35 msgid "Dutch" msgstr "Holandês" #: qt\preferences.py:36 msgid "Polish" msgstr "Polonês" #: qt\preferences.py:37 msgid "Brazilian" msgstr "Português Brasileiro" #: qt\preferences.py:38 msgid "Russian" msgstr "Russo" #: qt\preferences.py:39 msgid "Turkish" msgstr "Turco" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "Ucraniano" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "Vietnamita" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "Chinês (Simplificado)" #: qt\recent.py:54 msgid "Clear List" msgstr "Limpar Lista" #: qt\search_edit.py:78 msgid "Search..." msgstr "Buscar…" dupeguru-4.3.1/locale/qtlib.pot000066400000000000000000000001461426171743600164710ustar00rootroot00000000000000 msgid "" msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: utf-8\n" dupeguru-4.3.1/locale/ru/000077500000000000000000000000001426171743600152575ustar00rootroot00000000000000dupeguru-4.3.1/locale/ru/LC_MESSAGES/000077500000000000000000000000001426171743600170445ustar00rootroot00000000000000dupeguru-4.3.1/locale/ru/LC_MESSAGES/columns.po000066400000000000000000000061361426171743600210720ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # msgid "" msgstr "" "Last-Translator: Andrew Senetar , 2021\n" "Language-Team: Russian (https://www.transifex.com/voltaicideas/teams/116153/ru/)\n" "Language: ru\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "Путь к файлу" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "Сообщение об ошибке" #: core\me\prioritize.py:23 msgid "Duration" msgstr "Продолжительность" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "Битрейт" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "Частота оцифровки" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "Имя файла" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "Каталог" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "Размер (МБ)" #: core\me\result_table.py:22 msgid "Time" msgstr "Время" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "Частота" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "Тип" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "Время изменения" #: core\me\result_table.py:27 msgid "Title" msgstr "Название" #: core\me\result_table.py:28 msgid "Artist" msgstr "Исполнитель" #: core\me\result_table.py:29 msgid "Album" msgstr "Альбом" #: core\me\result_table.py:30 msgid "Genre" msgstr "Жанр" #: core\me\result_table.py:31 msgid "Year" msgstr "Год" #: core\me\result_table.py:32 msgid "Track Number" msgstr "Номер дорожки" #: core\me\result_table.py:33 msgid "Comment" msgstr "Комментарий" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "Совпадение %" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "Использованные слова" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "Количество дубликатов" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "Размеры" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "Размер (КБ)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "Временная отметка EXIF" #: core\prioritize.py:156 msgid "Size" msgstr "Размер" dupeguru-4.3.1/locale/ru/LC_MESSAGES/core.po000066400000000000000000000205431426171743600203400ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # msgid "" msgstr "" "Last-Translator: Fuan , 2021\n" "Language-Team: Russian (https://www.transifex.com/voltaicideas/teams/116153/ru/)\n" "Language: ru\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "Дубликаты не отмечены. Нечего выполнять." #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "Дубликаты не выбраны. Нечего выполнять." #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" "Вы собираетесь открыть много файлов за один раз. В зависимости от того, чем " "файлы будут открыты, это действие может создать настоящий беспорядок. " "Продолжать?" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "Проверка на наличие дубликатов" #: core\app.py:72 msgid "Loading" msgstr "Загрузка" #: core\app.py:73 msgid "Moving" msgstr "Перемещение" #: core\app.py:74 msgid "Copying" msgstr "Копирование" #: core\app.py:75 msgid "Sending to Trash" msgstr "Перемещение в Корзину" #: core\app.py:289 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" "Предыдущее действие до сих пор выполняется. Вы не можете начать новое. " "Подождите несколько секунд, затем повторите попытку." #: core\app.py:300 msgid "No duplicates found." msgstr "Дубликаты не найдены." #: core\app.py:315 msgid "All marked files were copied successfully." msgstr "Все отмеченные файлы были скопированы успешно." #: core\app.py:317 msgid "All marked files were moved successfully." msgstr "Все отмеченные файлы были перемещены успешно." #: core\app.py:319 msgid "All marked files were deleted successfully." msgstr "" #: core\app.py:321 msgid "All marked files were successfully sent to Trash." msgstr "Все отмеченные файлы были успешно отправлены в Корзину." #: core\app.py:326 msgid "Could not load file: {}" msgstr "Не удалось загрузить файл: {}" #: core\app.py:382 msgid "'{}' already is in the list." msgstr "'{}' уже присутствует в списке." #: core\app.py:384 msgid "'{}' does not exist." msgstr "'{}' не существует." #: core\app.py:392 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "" "Все выбранные %d совпадений будут игнорироваться при всех последующих " "проверках. Продолжить?" #: core\app.py:469 msgid "Select a directory to copy marked files to" msgstr "Выберите каталог, в который вы хотите скопировать отмеченные файлы" #: core\app.py:471 msgid "Select a directory to move marked files to" msgstr "Выберите каталог, в который вы хотите переместить отмеченные файлы" #: core\app.py:510 msgid "Select a destination for your exported CSV" msgstr "Выберите назначение для экспортируемого " #: core\app.py:516 core\app.py:771 core\app.py:781 msgid "Couldn't write to file: {}" msgstr "Не удалось записать в файл: {}" #: core\app.py:539 msgid "You have no custom command set up. Set it up in your preferences." msgstr "Вы не создали пользовательскую команду. Задайте её в настройках." #: core\app.py:695 core\app.py:707 msgid "You are about to remove %d files from results. Continue?" msgstr "Вы собираетесь удалить %d файлов из результата поиска. Продолжить?" #: core\app.py:743 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} групп дубликатов было изменено при реприоритезации." #: core\app.py:790 msgid "The selected directories contain no scannable file." msgstr "Выбранные каталоги не содержат файлов для сканирования." #: core\app.py:803 msgid "Collecting files to scan" msgstr "Сбор файлов для сканирования" #: core\app.py:850 msgid "%s (%d discarded)" msgstr "%s. (%d отменено)" #: core\directories.py:191 msgid "Collected {} files to scan" msgstr "" #: core\directories.py:207 msgid "Collected {} folders to scan" msgstr "" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "Вы перемещаете {} файлов в Корзину." #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "Обычные выражения" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "" "Вы действительно хотите удалить все элементы %d из списка игнорирования?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "Имя файла" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "Имя файла - Поля" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "Имя файла - поля (без порядка)" #: core\me\scanner.py:23 msgid "Tags" msgstr "Теги" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "Содержание" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "Анализируется %d/%d изображений" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "Выполнено %d/%d совпадений блоков" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "Подготовка для сравнения" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "Проверено %d/%d совпадений" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "Прочитана EXIF-информация %d/%d фотографий" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "Метка времени EXIF" #: core\prioritize.py:70 msgid "None" msgstr "Ни один" #: core\prioritize.py:100 msgid "Ends with number" msgstr "Заканчивается номером" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "Не заканчивается номером" #: core\prioritize.py:102 msgid "Longest" msgstr "Самый длинный" #: core\prioritize.py:103 msgid "Shortest" msgstr "Самый короткий" #: core\prioritize.py:140 msgid "Highest" msgstr "Наивысший" #: core\prioritize.py:140 msgid "Lowest" msgstr "Самый низкий" #: core\prioritize.py:169 msgid "Newest" msgstr "Новейший" #: core\prioritize.py:169 msgid "Oldest" msgstr "Старейшие" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) дубликатов отмечено." #: core\results.py:141 msgid " filter: %s" msgstr "фильтр: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "Подсчитан размер %d/%d файлов" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "Прочитаны метаданные %d/%d файлов" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "Почти готово! Возиться с результатами..." #: core\se\scanner.py:18 msgid "Folders" msgstr "Папки" dupeguru-4.3.1/locale/ru/LC_MESSAGES/ui.po000066400000000000000000001230361426171743600200260ustar00rootroot00000000000000# Translators: # Fuan , 2022 # Andrew Senetar , 2022 # msgid "" msgstr "" "Last-Translator: Andrew Senetar , 2022\n" "Language-Team: Russian (https://www.transifex.com/voltaicideas/teams/116153/ru/)\n" "Language: ru\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" #: qt/app.py:81 msgid "Quit" msgstr "Выйти" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "Параметры" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Список игнорирования" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Очистить кэш изображений" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Справка dupeGuru" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "О dupeGuru" #: qt/app.py:87 msgid "Open Debug Log" msgstr "Открыть журнал отладки" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "Вы действительно хотите удалить все кэшированные данные изображений?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "Кэш изображений очищен." #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} файл (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Параметры удаления" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Создать ссылку вместо удалённого файла" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" "После удаления дубликата создать ссылку на эталонный файл на месте " "удалённого." #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "Жёсткая ссылка" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "Символьная ссылка" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr "(не поддерживается)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Удалить файл с диска" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "Вместо отправки файлов в Корзину удалить их с диска. Этот параметр обычно " "используется как обходной путь, когда нормальный метод удаления не работает." #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Выполняется" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Отменить" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "Атрибут" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "Выбранный" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "Эталон" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "Загрузка результатов…" #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "Окно результатов" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "Добавить каталог…" #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "Файл" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "Вид" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "Справка" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "Загрузка последних результатов" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "Режим приложения:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "Музыка" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "Рисунок" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "Стандарт" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "Тип поиска:" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "Больше вариантов" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "Выберите каталоги для поиска и нажмите \"Сканирование\"." #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "Загрузить результаты" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "Сканирование" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "Несохранённые результаты" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "Имеются несохранённые результаты, вы действительно хотите выйти?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "Выберите каталог для добавления в список сканирования" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "Выберите файл результатов для загрузки" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "Все файлы (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "Результаты dupeGuru (*. dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "Начать новую проверку" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "Имеются несохранённые результаты, вы действительно хотите продолжить?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "Имя" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "Состояние" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "Исключённые" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "Нормальный" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "Удалить выбранные" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "Очистить" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "Закрыть" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "Детали" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "Теги для проверки:" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "Трек" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "Исполнитель" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "Альбом" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "Название" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "Жанр" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "Год" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "Вес слова" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "Совпадение похожих слов" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "Можно смешивать типы файлов" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "Использование регулярных выражений при фильтрации" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "Удалять пустые каталоги при удалении или перемещении" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "Игнорировать жёсткие ссылки на тот же файл" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "Режим отладки (требуется перезапуск)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "Совпадение рисунков разных размеров" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "Уровень фильтрации:" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "Дополнительные результаты" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "Меньше результатов" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "Размер шрифта:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "Язык:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "Копирование и перемещение:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "Прямо в каталог назначения" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "Восстановить относительный путь" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "Восстановить абсолютный путь" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "" "Пользовательская команда (аргументы: %d для дубликата, %r для эталона):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "" "dupeGuru необходимо перезапустить, чтобы языковые изменения вступили в силу." #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "Изменить приоритеты дубликатов" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "Добавьте критерий в правое поле и нажмите кнопку OK, чтобы поместить " "дубликаты, которые соответствуют лучшим из этих критериев, в эталонную " "позицию соответствующих групп. Прочитайте справку для получения " "дополнительной информации." #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "Проблемы!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "Были проблемы обработки некоторых (или всех) файлов. Причины этих проблем " "описаны в таблице ниже. Эти файлы не были удалены из результатов поиска." #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "Показать выбранное" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "Действия" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "Показать только дубликаты" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "Показать значения разницы" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "Переместить отмеченные в Корзину…" #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "Переместить отмеченные в…" #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "Скопировать отмеченные в…" #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "Удалить отмеченные из результатов" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "Изменить приоритеты результатов…" #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "Удалить выбранные из результатов" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "Добавить выбранные в список игнорирования" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "Сделать выбранные эталоном" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "Открыть выбранные в приложении по умолчанию" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "Открыть каталог с выбранными" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "Переименовать выбранные" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "Отметить все" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "Снять отметки" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "Обратить отметки" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "Отметить выбранные" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "Экспорт в HTML" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "Экспорт в " #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "Сохранить результаты…" #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "Выполнить пользовательскую команду" #: qt/result_window.py:102 msgid "Mark" msgstr "Отметить" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "Колонки" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "Восстановить значения по умолчанию" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} Результаты" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "Только дубликаты" #: qt/result_window.py:194 msgid "Delta Values" msgstr "Значения разницы" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "Выберите файл, чтобы сохранить ваши результаты" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "Игнорировать файлы меньше чем" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "КБ" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ Результаты" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "Действие" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "Добавить новый каталог…" #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "Расширенные" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "Автоматически проверять наличие обновлений" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "Основной" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "Все на передний план" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "Проверка обновлений…" #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "Закрыть окно" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "Копировать" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "" "Пользовательская команда (аргументы: %d для дубликата, %r для эталона):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "Вырезать" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Разница" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "Информация о выбранном файле" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "Панель деталей" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "Каталоги" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "Настройки dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "Результаты dupeGuru " #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "Вебсайт dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "Редактировать" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "Экспорт результатов в CSV" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "Экспорт результатов в XHTML" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "Меньше результатов" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "Фильтр" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "Уровень фильтрации:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Отфильтровать результаты…" #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "Окно выбора каталога" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "Размер шрифта:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "Скрыть dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "Скрыть остальные" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "Игнорировать файлы меньше чем:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "Загрузить из файла…" #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "Минимизировать" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "Режим" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "Доп. результаты" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "OK" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "Вставить" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "Настройки…" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "Быстрый просмотр" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "Выйти из dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "Восстановить значения по умолчанию" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "Восстановить значения по умолчанию" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "Показать" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Показать выбранное в Finder-е" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "Выбрать все" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Переместить отмеченные в Корзину…" #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "Сервисы" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "Показать все" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "Начать поиск дубликатов" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr " Имя '%@' уже существует." #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "Окно" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Увеличить" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "Фильтры исключения" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "Результаты сканирования" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "Загрузить каталоги..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "Сохранить каталоги..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "Выберите файл каталогов для загрузки" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "каталоги dupeGuru (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "Выберите файл для сохранения каталогов" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "каталоги dupeGuru (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "добавлять" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "Восстановить значения по умолчанию" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "Тестовая строка" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "Введите здесь регулярное выражение Python..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "Введите здесь путь к файловой системе или имя файла..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Эти (чувствительные к регистру) регулярные выражения Python будут отфильтровывать файлы во время сканирования.
    Директорам также будет установлено состояние по умолчанию «Исключено» на вкладке «Каталоги», если их имя совпадает с одним из выбранных регулярных выражений.
    Для каждого собранного файла выполняется два теста, чтобы определить, следует ли его полностью игнорировать:
  • 1. Регулярные выражения без разделителя пути будут сравниваться только с именем файла.
  • \n" "
  • 2. Регулярные выражения, содержащие хотя бы один разделитель пути, будут сравниваться с полным путем к файлу.

  • \n" "Пример: если вы хотите отфильтровать файлы .PNG только из каталога «Мои изображения»:
    .*Мои\\sизображения\\\\.*\\.png

    Вы можете проверить регулярное выражение с помощью кнопки «тестовая строка» после вставки поддельного пути в тестовое поле:
    C:\\\\Пользователь\\Мои изображения\\test.png

    \n" "Соответствующие регулярные выражения будут выделены.
    Если есть хотя бы одно выделение, проверенный путь или имя файла будет проигнорирован во время сканирования.

    Каталоги и файлы, начинающиеся с точки \".\" по умолчанию отфильтрованы.

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "Ошибка компиляции:" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "Увеличить масштаб" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "Уменьшить масштаб" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "Нормальный размер" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "Наиболее подходящий" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "Режим кеширования изображений:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "Переопределить значки темы на панели инструментов средства просмотра" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" "Используйте наши собственные внутренние значки вместо тех, которые " "предоставляются движком темы." #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "Показывать полосы прокрутки в средствах просмотра изображений" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" "Когда отображаемое изображение не умещается в области просмотра, покажите " "полосы прокрутки, чтобы охватить область просмотра" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "" "Использовать позицию по умолчанию для панели вкладок (требуется перезапуск)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "Разместите панель вкладок под главным меню, а не рядом с ним.\n" "В MacOS панель вкладок заполнит ширину окна." #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "Используйте жирный шрифт для ссылок" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "Эталонный цвет переднего плана:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "Цвет фона справки:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "Цвет переднего плана дельты:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "Показать строку заголовка и может быть закреплен" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" "Пока строка заголовка скрыта, используйте клавишу-модификатор, чтобы " "перетащить плавающее окно вокруг" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "Строку заголовка можно отключить, только когда окно закреплено" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "Вертикальная строка заголовка" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "" "Измените строку заголовка с горизонтальной сверху на вертикальную слева" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "Показать панель вкладок" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Эти (чувствительные к регистру) регулярные выражения Python будут отфильтровывать файлы во время сканирования.
    Директорам также будет установлено состояние по умолчанию «Исключено» на вкладке «Каталоги», если их имя совпадает с одним из выбранных регулярных выражений.
    Для каждого собранного файла выполняется два теста, чтобы определить, следует ли его полностью игнорировать:
  • 1. Регулярные выражения без разделителя пути будут сравниваться только с именем файла.
  • \n" "
  • 2. Регулярные выражения, содержащие хотя бы один разделитель пути, будут сравниваться с полным путем к файлу.

  • \n" "Пример: если вы хотите отфильтровать файлы .PNG только из каталога «Мои изображения»:
    .*Мои\\sизображения\\\\.*\\.png

    Вы можете проверить регулярное выражение с помощью кнопки «тестовая строка» после вставки поддельного пути в тестовое поле:
    C:\\\\Пользователь\\Мои изображения\\test.png

    \n" "Соответствующие регулярные выражения будут выделены.
    Если есть хотя бы одно выделение, проверенный путь или имя файла будет проигнорирован во время сканирования.

    Каталоги и файлы, начинающиеся с точки \".\" по умолчанию отфильтрованы.

    " #: qt\app.py:256 msgid "Results" msgstr "Результаты" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "Общий интерфейс" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "Таблица результатов" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "Окно деталей" #: qt\preferences_dialog.py:285 msgid "General" msgstr "Общий" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "Отображать" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" #: qt\app.py:299 msgid "Cache cleared." msgstr "" #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "" #: qt\about_box.py:31 msgid "About {}" msgstr "О {}" #: qt\about_box.py:47 msgid "Version {}" msgstr "Версия {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "" #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "Под лицензией GPLv3" #: qt\about_box.py:68 msgid "No update available." msgstr "" #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "Сообщение об ошибке" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "Что-то пошло не так. Хотите отправить отчёт об ошибке?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "Отчеты об ошибках следует сообщать как о проблемах Github. Вы можете скопировать трассировку ошибки выше и вставить ее в новый выпуск.\n" "\n" "Обязательно заранее выполните поиск любых уже существующих проблем. Также не забудьте протестировать самую последнюю версию, доступную в репозитории, поскольку ошибка, с которой вы столкнулись, могла уже быть исправлена.\n" "\n" "Что обычно действительно помогает, так это то, что вы добавляете описание того, как вы получили ошибку. Благодаря!\n" "\n" "Хотя приложение должно продолжить работу после этой ошибки, оно может находиться в нестабильном состоянии, поэтому рекомендуется перезапустить приложение." #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Перейти на Github" #: qt\preferences.py:24 msgid "Czech" msgstr "Чешский" #: qt\preferences.py:25 msgid "German" msgstr "Немецкий" #: qt\preferences.py:26 msgid "Greek" msgstr "Греческий" #: qt\preferences.py:27 msgid "English" msgstr "Английский" #: qt\preferences.py:28 msgid "Spanish" msgstr "Испанский" #: qt\preferences.py:29 msgid "French" msgstr "Французский" #: qt\preferences.py:30 msgid "Armenian" msgstr "Армянский" #: qt\preferences.py:31 msgid "Italian" msgstr "Итальянский" #: qt\preferences.py:32 msgid "Japanese" msgstr "Японский" #: qt\preferences.py:33 msgid "Korean" msgstr "Корейский" #: qt\preferences.py:34 msgid "Malay" msgstr "Малайский" #: qt\preferences.py:35 msgid "Dutch" msgstr "Голландский" #: qt\preferences.py:36 msgid "Polish" msgstr "Польский" #: qt\preferences.py:37 msgid "Brazilian" msgstr "Бразильский" #: qt\preferences.py:38 msgid "Russian" msgstr "Русский" #: qt\preferences.py:39 msgid "Turkish" msgstr "Турецкий" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "Украинский" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "Вьетнамский" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "Китайский (упрощенный)" #: qt\recent.py:54 msgid "Clear List" msgstr "Очистить список" #: qt\search_edit.py:78 msgid "Search..." msgstr "Искать..." dupeguru-4.3.1/locale/tr/000077500000000000000000000000001426171743600152565ustar00rootroot00000000000000dupeguru-4.3.1/locale/tr/LC_MESSAGES/000077500000000000000000000000001426171743600170435ustar00rootroot00000000000000dupeguru-4.3.1/locale/tr/LC_MESSAGES/columns.po000066400000000000000000000053531426171743600210710ustar00rootroot00000000000000# Translators: # Ahmet Haydar Işık , 2021 # Emin Tufan Çetin , 2021 # msgid "" msgstr "" "Last-Translator: Emin Tufan Çetin , 2021\n" "Language-Team: Turkish (https://www.transifex.com/voltaicideas/teams/116153/tr/)\n" "Language: tr\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "Dosya Konumu" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "Hata İletisi" #: core\me\prioritize.py:23 msgid "Duration" msgstr "Süre" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "Bit Oranı" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "Örnek Hızı" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "Dosya Adı" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "Klasör" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "Boyut (MB)" #: core\me\result_table.py:22 msgid "Time" msgstr "Zaman" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "Örnek Hızı" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "Tür" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "Değişiklik" #: core\me\result_table.py:27 msgid "Title" msgstr "Başlık" #: core\me\result_table.py:28 msgid "Artist" msgstr "Sanatçı" #: core\me\result_table.py:29 msgid "Album" msgstr "Albüm" #: core\me\result_table.py:30 msgid "Genre" msgstr "Tür" #: core\me\result_table.py:31 msgid "Year" msgstr "Yıl" #: core\me\result_table.py:32 msgid "Track Number" msgstr "Parça Numarası" #: core\me\result_table.py:33 msgid "Comment" msgstr "Yorum" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "Eşleşme %" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "Kullanılan Sözcükler" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "Kopya Sayısı" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "Ölçüler" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "Boyut (KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "EXIF Zaman Damgası" #: core\prioritize.py:156 msgid "Size" msgstr "Boyut" dupeguru-4.3.1/locale/tr/LC_MESSAGES/core.po000066400000000000000000000153621426171743600203420ustar00rootroot00000000000000# Translators: # Ahmet Haydar Işık , 2021 # Emin Tufan Çetin , 2021 # msgid "" msgstr "" "Last-Translator: Emin Tufan Çetin , 2021\n" "Language-Team: Turkish (https://www.transifex.com/voltaicideas/teams/116153/tr/)\n" "Language: tr\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "İmlenen kopya yok. İşlem yapılmadı." #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "Seçilen kopya yok. İşlem yapılmadı." #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" "Birden çok dosyayı aynı anda açmaya çalışıyorsunuz. Dosyaların açıldığı " "programlara bağlı olarak, bu sorun yaratabilir. Sürdürülsün mü?" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "Kopyalar aranıyor" #: core\app.py:72 msgid "Loading" msgstr "Yükleniyor" #: core\app.py:73 msgid "Moving" msgstr "Taşınıyor" #: core\app.py:74 msgid "Copying" msgstr "Kopyalanıyor" #: core\app.py:75 msgid "Sending to Trash" msgstr "Çöpe Gönderiliyor" #: core\app.py:289 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" "Önceki işlem hala sürüyor. Yenisini başlatamazsınız. Birkaç saniye bekleyip " "yeniden deneyin." #: core\app.py:300 msgid "No duplicates found." msgstr "Kopya bulunamadı." #: core\app.py:315 msgid "All marked files were copied successfully." msgstr "İmlenen tüm dosyalar başarıyla kopyalandı." #: core\app.py:317 msgid "All marked files were moved successfully." msgstr "İmlenen tüm dosyalar başarıyla taşındı." #: core\app.py:319 msgid "All marked files were deleted successfully." msgstr "İmlenen tüm dosyalar başarıyla silindi." #: core\app.py:321 msgid "All marked files were successfully sent to Trash." msgstr "İmlenen tüm dosyalar başarıyla çöpe gönderildi." #: core\app.py:326 msgid "Could not load file: {}" msgstr "Dosya yüklenemedi: {}" #: core\app.py:382 msgid "'{}' already is in the list." msgstr "'{}' çoktan listede." #: core\app.py:384 msgid "'{}' does not exist." msgstr "'{}' bulunamadı." #: core\app.py:392 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "" "Seçilen %d eşleşmenin tümü sonraki taramalarda göz ardı edilecek. Sürdür?" #: core\app.py:469 msgid "Select a directory to copy marked files to" msgstr "İmlenen dosyaları kopyalamak için dizin seç" #: core\app.py:471 msgid "Select a directory to move marked files to" msgstr "İmlenen dosyaları taşımak için dizin seç" #: core\app.py:510 msgid "Select a destination for your exported CSV" msgstr "Dışa aktarılan CSV'niz için hedef seçin" #: core\app.py:516 core\app.py:771 core\app.py:781 msgid "Couldn't write to file: {}" msgstr "Dosyaya yazılamadı: {}" #: core\app.py:539 msgid "You have no custom command set up. Set it up in your preferences." msgstr "Ayarlı özel komutunuz yok. Tercihlerinizden ayarlayınız." #: core\app.py:695 core\app.py:707 msgid "You are about to remove %d files from results. Continue?" msgstr "%d ögeyi sonuçlardan kaldırıyorsunuz. Sürdür?" #: core\app.py:743 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} kopya küme, yeniden önceliklendirme tarafından değiştirildi." #: core\app.py:790 msgid "The selected directories contain no scannable file." msgstr "Seçili dizinler taranabilir dosya içermiyor." #: core\app.py:803 msgid "Collecting files to scan" msgstr "Taranacak dosyalar toplanıyor" #: core\app.py:850 msgid "%s (%d discarded)" msgstr "%s (%d göz ardı edilen)" #: core\directories.py:191 msgid "Collected {} files to scan" msgstr "Taranacak {} dosya toplandı" #: core\directories.py:207 msgid "Collected {} folders to scan" msgstr "Taranacak {} klasör toplandı" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "%d eşleşme, %d kümeden" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "{} dosyayı Çöp'e gönderiyorsunuz." #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "Düzenli İfadeler" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "" "%d ögenin tümünü göz ardı edilenler listesinden kaldırmak istiyor musunuz?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "Dosya adı" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "Dosya adı - Alanlar" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "Dosya Adı - Alanlar (Düzen Yok)" #: core\me\scanner.py:23 msgid "Tags" msgstr "Etiketler" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "İçindekiler" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "%d/%d fotoğraf incelendi" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "%d/%d yığın eşleşmesi gerçekleştirildi" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "Eşlemeye hazırlanıyor" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "%d/%d eşleşme doğrulandı" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "%d/%d fotorğafın EXIF'i okundu" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "EXIF Zaman damgası" #: core\prioritize.py:70 msgid "None" msgstr "Hiçbiri" #: core\prioritize.py:100 msgid "Ends with number" msgstr "Sayıyla bitenler" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "Sayıyla bitmeyenler" #: core\prioritize.py:102 msgid "Longest" msgstr "En uzun" #: core\prioritize.py:103 msgid "Shortest" msgstr "En kısa" #: core\prioritize.py:140 msgid "Highest" msgstr "En yüksek" #: core\prioritize.py:140 msgid "Lowest" msgstr "En düşük" #: core\prioritize.py:169 msgid "Newest" msgstr "En yeni" #: core\prioritize.py:169 msgid "Oldest" msgstr "En eski" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) kopya imlendi." #: core\results.py:141 msgid " filter: %s" msgstr "süz: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "%d/%d dosyanın boyutu okundu" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "%d/%d dosyanın üst verisi okundu" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "Neredeyse bitti! Sonuçlarla uğraşılıyor..." #: core\se\scanner.py:18 msgid "Folders" msgstr "Dizinler" dupeguru-4.3.1/locale/tr/LC_MESSAGES/ui.po000066400000000000000000001053721426171743600200300ustar00rootroot00000000000000# Translators: # Emin Tufan Çetin , 2022 # Ahmet Haydar Işık , 2022 # msgid "" msgstr "" "Last-Translator: Ahmet Haydar Işık , 2022\n" "Language-Team: Turkish (https://www.transifex.com/voltaicideas/teams/116153/tr/)\n" "Language: tr\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: qt/app.py:81 msgid "Quit" msgstr "Çık" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "Seçenekler" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Göz Ardı Edilenler Listesi" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Fotoğraf Önbelleğini Temizle" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "dupeGuru Yardımı" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "dupeGuru Hakkında" #: qt/app.py:87 msgid "Open Debug Log" msgstr "Hata Ayıklama Günlüğünü Aç" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "" "Tüm önbelleklenen fotoğraf incelemenizi kaldırmak istediğinize emin misiniz?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "Fotoğraf önbelleği temizlendi." #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} dosyası (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Silme Seçenekleri" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Silinen dosyaları bağlantıla" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" "Kopyayı sildikten sonra, silinen dosyayının yerine kaynak dosyayı hedefleyen" " bağlantı yerleştir." #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "Katı bağlantı" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "Simgesel bağlantı" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr "(desteklenmiyor)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Doğrudan dosyaları sil" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "Dosyaları çöpe göndermek yerine doğrudan sil. Bu seçenek, olağan silme " "yöntemi çalışmadığında geçici çözüm olarak kullanılır." #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Sürdür" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "İptal" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "Öznitelik" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "Seçili" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "Kaynak" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "Sonuçları Yükle..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "Sonuç Penceresi" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "Klasör Ekle..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "Dosya" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "Görünüm" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "Yardım" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "Son Sonuçları Yükle" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "Uygulama Kipi:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "Müzik" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "Resim" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "Standart" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "Tarama Türü:" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "Daha Çok Seçenek" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "Taranacak klasörleri seç ve \"Tara\"ya bas." #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "Sonuçları Yükle" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "Tara" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "Kaydedilmemiş sonuçlar" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "Kaydedilmemiş sonuçlarınız var, çıkmak istediğinizden emin misiniz?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "Tarama listesine eklenecek klasör seç" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "Yüklenecek sonuç dosyası seç" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "Tüm Dosyalar (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "dupeGuru Sonuçları (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "Yeni tarama başlat" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "" "Kaydedilmemiş sonuçlarınız var, sürdürmek istediğinizden emin misiniz?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "Ad" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "Durum" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "Dışlandı" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "Olağan" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "Seçileni Kaldır" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "Temizle" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "Kapat" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "Ayrıntılar" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "Taranacak etiketler:" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "Parça" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "Sanatçı" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "Albüm" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "Başlık" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "Tür" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "Yıl" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "Sözcük ağırlıklandırması" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "Benzer sözcükleri eşle" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "Dosya türü karışık olabilir" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "Süzerken düzenli ifadeler kullan" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "Silme veya taşımada boş klasörleri kaldır" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "Aynı dosyaya katı bağlantısı olan kopyaları göz ardı et" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "Hata ayıklama kipi (yeniden başlatılmalıdır)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "Başka boyutlardaki fotoğrafları eşle" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "Süzme Katılığı:" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "Daha Çok Sonuç" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "Daha Az Sonuç" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "Yazı tipi boyutu:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "Dil:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "Kopyala ve Taşı:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "Hedefin içine" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "Göreceli yolu yeniden yarat" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "Mutlak yolu yeniden yarat" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "Özel Komut (argümanlar: dupe için %d, ref için %r):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "Dil değişimlerinin gerçekleşmesi için dupeGuru yeniden başlamalıdır." #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "Kopyaları yeniden önceliklendir" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "Sağ kutuya ölçüt ekle ve bu ölçütlere en iyi uyan kopyaları, kendi ilgili " "kümelerinin kaynak konumuna gönder. Daha çok bilgi için yardım dosyasını " "okuyun." #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "Sorunlar!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "Bazı (veya tüm) dosyaların işlenmesinde sorun var. Bu sorunların nedeni " "aşağıdaki tabloda açıklanmıştır. Bu dosyalar sonuçlarınızdan " "kaldırılmamıştır." #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "Seçiliyi Göster" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "Eylemler" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "Yalnızca Kopyaları Göster" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "Delta Değerleri Göster" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "İmliyi Geri Dönüşüm Kutusuna Gönder..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "İmliyi Şuraya Taşı..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "İmliyi Şuraya Kopyala..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "İmliyi Sonuçlardan Kaldır" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "Sonuçları Yeniden Önceliklendir..." #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "Seçiliyi Sonuçlardan Kaldır" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "Seçiliyi Göz Ardı Edilenler Listesine Ekle" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "İmliyi Kaynak Yap" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "Seçiliyi Öntanımlı Uygulamayla Aç" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "Seçiliyi İçeren Klasörü Aç" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "Seçiliyi Yeniden Adlandır" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "Tümünü İmle" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "Hiçbirini İmleme" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "İmlemeyi Ters Çevir" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "Seçiliyi İmle" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "HTML'ye Aktar" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "CSV'ye Aktar" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "Sonuçları Kaydet..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "Özel Komut Çalıştır" #: qt/result_window.py:102 msgid "Mark" msgstr "İmle" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "Sütunlar" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "Öntanımlılara Sıfırla" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} Sonuçları" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "Yalnızca Kopyalar" #: qt/result_window.py:194 msgid "Delta Values" msgstr "Delta Değerler" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "Sonuçlarınızın kaydedileceği dosyayı seçin" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "Şundan küçük dosyaları göz ardı et:" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ Sonuçları" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "Eylem" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "Yeni Klasör Ekle..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "Gelişmiş" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "Güncellemeleri kendiliğinden denetle" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "Temel" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "Tümünü Öne Çıkar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "Güncellemeleri denetle..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "Pencereyi Kapat" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "Kopyala" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "Özel komut (argümanlar: dupe için %d , ref için %r )" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "Kes" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Delta" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "Seçili Dosyanın Ayrıntıları" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "Ayrıntılar Bölmesi" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "Dizinler" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "dupeGuru Tercihleri" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "dupeGuru Sonuçları" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "dupeGuru Web Sitesi" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "Düzenle" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "Sonuçları CSV'ye Aktar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "Sonuçları XHTML'ye Aktar" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "Daha az sonuç" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "Süz" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "Süzme katılığı:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Sonuçları Süz..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "Klasör Seçim Penceresi" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "Yazı Tipi Boyutu:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "dupeGuru'yu Gizle" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "Diğerlerini Gizle" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "Şundan küçük dosyaları göz ardı et:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "Dosyadan yükle..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "Küçült" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "Kip" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "Daha çok sonuç" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "Tamam" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "Yapıştır" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "Tercihler..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "Hızlı Bakış" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "dupeGuru'dan Çık" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "Öntanımlıya Sıfırla" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "Öntanımlılara Sıfırla" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "Göster" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Seçiliyi Finder'da Göster" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "Tümünü Seç" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "İmliyi Çöpe Gönder..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "Servisler" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "Tümünü Göster" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "Kopya Taramayı Başlat" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "Halihazırda '%@' adı var." #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "Pencere" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Yakınlaş" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "Dışlama Süzgeçleri" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "Tarama Sonuçları" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "Dizinleri Yükle..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "Dizinleri Kaydet..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "Yüklenecek bir dizin dosyası seçin" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "dupeGuru Sonuçları (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "Dizinlerinizi kaydetmek için bir dosya seçin" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "dupeGuru Dizinleri (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "Ekle" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "Öntanımlıları geri yükle" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "Dizgeyi sına" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "Buraya Python düzenli ifadesi yaz..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "Buraya dosya sistemi yolu veya dosya adı yaz..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Bu (büyük/küçük harfe duyarlı) python düzenli ifadeleri taramalar sırasında dosyaları süzecektir.
    Ayrıca dizinlerin adları, seçilen düzenli ifadelerden biriyle eşleşirse, Dizinler sekmesindeöntanımlı durumlarıDışlandı olarak ayarlanır.
    Toplanan her dosya, tümüyle göz ardı edilip edilmeyeceğiyle ilgili iki kez sınanır:
  • 1. İçinde yol ayracı olmayan düzenli ifadeler yalnızca dosya adıyla karşılaştırılacaktır.
  • \n" "
  • 2. İçinde yol ayracı olmayan düzenli ifadeler, dosyanın tam yolu ile karşılaştırılacaktır.

  • \n" "Örneğin: \"Benim Resimlerim\" dizininden yalnızca .PNG dosyalarını süzmek istiyorsanız:
    .*Benim\\sResimlerim\\\\.*\\.png

    Düzenli ifadeyi, dizgeyi sınama özelliğinin içine sahte bir yol yapıştırarak sınayabilirsiniz:
    C:\\\\Kullanıcı\\Benim Resimlerim\\sına.png

    \n" "Eşleşen düzenli ifadeler vurgulanacaktır.
    En az bir vurgu varsa, sınanan yol taramalar sırasında yok sayılacaktır.

    Nokta '.' ile başlayan dizinler ve dosyalar öntanımlı olarak süzülür.

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "Derleme hatası:" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "Yakınlaştırmayı arttır" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "Yakınlaştırmayı azalt" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "Olağan boyut" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "En uygun" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "Resim önbellek kipi:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "Görüntüleyici araç çubuğundaki gövde simgelerini geçersiz kıl" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "Gövde motorunca sağlanan yerine kendi iç simgelerimizi kullanın" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "Resim görüntüleyicilerde kaydırma çubuklarını göster" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" "Görüntülenen görüntü görünüm alanına sığmadığında, görünümü etrafa yaymak " "için kaydırma çubuklarını göster" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "" "Sekme çubuğu için varsayılan konumu kullan (yeniden başlatma gerektirir)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "Sekme çubuğunu ana menünün yanına değil altına yerleştirin\n" "MacOS'ta sekme çubuğu bunun yerine pencerenin genişliğini dolduracaktır." #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "Referanslar için kalın yazı tipi kullanın" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "Referans ön plan rengi:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "Referans arka plan rengi:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "Delta ön plan rengi:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "Başlık çubuğunu görüntüleyebilir ve sabitleyebilirsiniz." #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" "Başlık çubuğu gizliyken, kayan pencereyi çevrede sürüklemek için değiştirici" " tuşu kullan" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "" "Başlık çubuğu yalnızca pencere sabitlendiğinde devre dışı bırakılabilir" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "Dikey başlık çubuğu" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "Başlık çubuğunu üstte yataydan sol tarafta dikey olarak değiştirin" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "Sekme çubuğunu göster" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Bu (büyük/küçük harfe duyarlı) python düzenli ifadeleri taramalar sırasında dosyaları süzecektir.
    Ayrıca dizinlerin adları, seçilen düzenli ifadelerden biriyle eşleşirse, Dizinler sekmesinde öntanımlı durumları Dışlandı olarak ayarlanır.
    Toplanan her dosya için, tümüyle göz ardı edilip edilmeyeceğini belirlemek için iki kez sınanır:
  • 1. İçinde yol ayracı olmayan düzenli ifadeler yalnızca dosya adıyla karşılaştırılacaktır.
  • \n" "
  • 2. İçinde en az bir yol ayracı bulunan düzenli ifadeler, dosyanın tam yolu ile karşılaştırılacaktır.

  • \n" "
    Örneğin: \"Benim Resimlerim\" dizininden yalnızca .PNG dosyalarını süzmek istiyorsanız:.*Benim\\sResimlerim\\\\.*\\.png

    Sınama alanına sahte bir yol yapıştırdıktan sonra düzenli ifadeyi \"Dizgeyi sına\" düğmesiyle sınayabilirsiniz:
    C:\\\\Kullanıcı\\Benim Resimlerim\\sınama.png

    \n" "Eşleşen düzenli ifadeler vurgulanacaktır.
    En az bir vurgu varsa, sınanan yol veya dosya adı taramalar sırasında yok sayılacaktır.

    Nokta '.' ile başlayan dizinler ve dosyalar öntanımlı olarak süzülür.

    " #: qt\app.py:256 msgid "Results" msgstr "Sonuçlar" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "Genel Arayüz" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "Sonuç Tablosu" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "Ayrıntı Penceresi" #: qt\preferences_dialog.py:285 msgid "General" msgstr "Genel" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "Görüntüle" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "Şundan büyük dosyaları kısmen özetle:" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "MB" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "İşletim sistemi yerel iletişim kutularını kullan" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" "Dosya/klasör seçimi gibi eylemlerde işletim sistemi yerel iletişim kutularını kullan.\n" "Bazı yerel iletişim kutuları sınırlı işlevselliktedir." #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "Şundan büyük dosyaları göz ardı et:" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "Önbelleği Temizle" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" "Önbelleği gerçekten temizlemek istiyor musunuz? Bu, tüm önbelleklenen dosya " "özetleri ve resim incelemelerini kaldıracak." #: qt\app.py:299 msgid "Cache cleared." msgstr "Önbellek temizlendi." #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "Karanlık biçem kullan" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "" #: qt\about_box.py:31 msgid "About {}" msgstr "{} Hakkında" #: qt\about_box.py:47 msgid "Version {}" msgstr "Sürüm {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "" #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "GPLv3 altında lisanslanmıştır." #: qt\about_box.py:68 msgid "No update available." msgstr "" #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "Hata Raporu" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "Bir şeyler yanlış gitti. Hatayı raporlamak ister misin?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "Hata raporları Github'da sorun (issue) olarak bildirilmelidir. Yukarıdaki hata kaynağını kopyalayabilir ve yeni sorun bildirimine yapıştırabilirsiniz\n" "\n" "Lütfen yeni sorun bildirimi oluşturmadan önce var olan sorunları aradığınızdan emin olun. Ayrıca depoda bulunan en son sürümü sınadığınızdan emin olun, karşılaştığınız hata hâlihazırda düzeltilmiş olabilir.\n" "\n" "Hatayı nasıl aldığınızın açıklamasını eklemeniz gerçekten yardımcı olabilir. Teşekkürler!\n" "\n" "Bu hatadan sonra uygulama çalışmaya sürdürebilse de kararsız durumda olabilir, bu nedenle uygulamayı yeniden başlatmanız önerilir." #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Github'a Git" #: qt\preferences.py:24 msgid "Czech" msgstr "Çekçe" #: qt\preferences.py:25 msgid "German" msgstr "Almanca" #: qt\preferences.py:26 msgid "Greek" msgstr "Yunanca" #: qt\preferences.py:27 msgid "English" msgstr "İngilizce" #: qt\preferences.py:28 msgid "Spanish" msgstr "İspanyolca" #: qt\preferences.py:29 msgid "French" msgstr "Fransızca" #: qt\preferences.py:30 msgid "Armenian" msgstr "Ermenice" #: qt\preferences.py:31 msgid "Italian" msgstr "İtalyanca" #: qt\preferences.py:32 msgid "Japanese" msgstr "Japonca" #: qt\preferences.py:33 msgid "Korean" msgstr "Korece" #: qt\preferences.py:34 msgid "Malay" msgstr "Malayca" #: qt\preferences.py:35 msgid "Dutch" msgstr "Felemenkçe" #: qt\preferences.py:36 msgid "Polish" msgstr "Lehçe" #: qt\preferences.py:37 msgid "Brazilian" msgstr "Brezilya Portekizcesi" #: qt\preferences.py:38 msgid "Russian" msgstr "Rusça" #: qt\preferences.py:39 msgid "Turkish" msgstr "Türkçe" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "Ukraynaca" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "Vietnamca" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "Çince (Basitleştirilmiş)" #: qt\recent.py:54 msgid "Clear List" msgstr "Listeyi Temizle" #: qt\search_edit.py:78 msgid "Search..." msgstr "Ara..." dupeguru-4.3.1/locale/ui.pot000066400000000000000000000621431426171743600160000ustar00rootroot00000000000000# msgid "" msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: utf-8\n" #: qt/app.py:81 msgid "Quit" msgstr "" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "" #: qt/app.py:87 msgid "Open Debug Log" msgstr "" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "" #: qt/app.py:251 msgid "{} file (*.{})" msgstr "" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr "" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "" #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "" #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "" #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "" #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "" #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "" #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "" #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "" #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "" #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "" #: qt/result_window.py:102 msgid "Mark" msgstr "" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "" #: qt/result_window.py:185 msgid "{} Results" msgstr "" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "" #: qt/result_window.py:194 msgid "Delta Values" msgstr "" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "" #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "" #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "" #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" #: qt\app.py:256 msgid "Results" msgstr "" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "" #: qt\preferences_dialog.py:285 msgid "General" msgstr "" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" #: qt\app.py:299 msgid "Cache cleared." msgstr "" #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "" #: qt\about_box.py:31 msgid "About {}" msgstr "" #: qt\about_box.py:47 msgid "Version {}" msgstr "" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "" #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "" #: qt\about_box.py:68 msgid "No update available." msgstr "" #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "" #: qt\preferences.py:24 msgid "Czech" msgstr "" #: qt\preferences.py:25 msgid "German" msgstr "" #: qt\preferences.py:26 msgid "Greek" msgstr "" #: qt\preferences.py:27 msgid "English" msgstr "" #: qt\preferences.py:28 msgid "Spanish" msgstr "" #: qt\preferences.py:29 msgid "French" msgstr "" #: qt\preferences.py:30 msgid "Armenian" msgstr "" #: qt\preferences.py:31 msgid "Italian" msgstr "" #: qt\preferences.py:32 msgid "Japanese" msgstr "" #: qt\preferences.py:33 msgid "Korean" msgstr "" #: qt\preferences.py:34 msgid "Malay" msgstr "" #: qt\preferences.py:35 msgid "Dutch" msgstr "" #: qt\preferences.py:36 msgid "Polish" msgstr "" #: qt\preferences.py:37 msgid "Brazilian" msgstr "" #: qt\preferences.py:38 msgid "Russian" msgstr "" #: qt\preferences.py:39 msgid "Turkish" msgstr "" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "" #: qt\recent.py:54 msgid "Clear List" msgstr "" #: qt\search_edit.py:78 msgid "Search..." msgstr "" dupeguru-4.3.1/locale/uk/000077500000000000000000000000001426171743600152505ustar00rootroot00000000000000dupeguru-4.3.1/locale/uk/LC_MESSAGES/000077500000000000000000000000001426171743600170355ustar00rootroot00000000000000dupeguru-4.3.1/locale/uk/LC_MESSAGES/columns.po000077500000000000000000000062721426171743600210670ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # msgid "" msgstr "" "Last-Translator: Fuan , 2021\n" "Language-Team: Ukrainian (https://www.transifex.com/voltaicideas/teams/116153/uk/)\n" "Language: uk\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "Шлях до файлу" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "Повідомлення про помилку" #: core\me\prioritize.py:23 msgid "Duration" msgstr "Тривалість" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "Якість звуку" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "Частота оцифровки" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "Ім’я файлу" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "Папка" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "Розмір (Мб)" #: core\me\result_table.py:22 msgid "Time" msgstr "Час" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "Частота дискретизації" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "Тип" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "Дата модифікації" #: core\me\result_table.py:27 msgid "Title" msgstr "Назва" #: core\me\result_table.py:28 msgid "Artist" msgstr "Виконавець" #: core\me\result_table.py:29 msgid "Album" msgstr "Альбом" #: core\me\result_table.py:30 msgid "Genre" msgstr "Жанр" #: core\me\result_table.py:31 msgid "Year" msgstr "Рік" #: core\me\result_table.py:32 msgid "Track Number" msgstr "Номер доріжки" #: core\me\result_table.py:33 msgid "Comment" msgstr "Коментар" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "Збіг (%)" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "Використані слова" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "Кількість дублікатів" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "Виміри" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "Розмір (KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "Відмітка часу EXIF" #: core\prioritize.py:156 msgid "Size" msgstr "Розмір" dupeguru-4.3.1/locale/uk/LC_MESSAGES/core.po000077500000000000000000000204661426171743600203400ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # msgid "" msgstr "" "Last-Translator: Fuan , 2021\n" "Language-Team: Ukrainian (https://www.transifex.com/voltaicideas/teams/116153/uk/)\n" "Language: uk\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "Немає позначених дублікатів - нічого робити." #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "Немає обраних дублікатів - нічого робити." #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" "Ви збираєтеся відкрити багато файлів одночасно.\n" "Залежно від того, з чим відкриваються ці файли, це може створити неабияку халепу. Продовжити?" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "Пошук дублікатів" #: core\app.py:72 msgid "Loading" msgstr "Завантаження" #: core\app.py:73 msgid "Moving" msgstr "Переміщення" #: core\app.py:74 msgid "Copying" msgstr "Копіювання" #: core\app.py:75 msgid "Sending to Trash" msgstr "Відправка до кошику" #: core\app.py:289 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" "Попередню дію ще не закінчено. Ви покищо не можете розпочаті нову. Зачекайте" " кілька секунд, потім повторіть спробу." #: core\app.py:300 msgid "No duplicates found." msgstr "Не знайдено жодного дублікату." #: core\app.py:315 msgid "All marked files were copied successfully." msgstr "Усі позначені файли були скопійовані успішно." #: core\app.py:317 msgid "All marked files were moved successfully." msgstr "Усі позначені файли були переміщені успішно." #: core\app.py:319 msgid "All marked files were deleted successfully." msgstr "" #: core\app.py:321 msgid "All marked files were successfully sent to Trash." msgstr "Усі позначені файли були успішно відправлені до кошика." #: core\app.py:326 msgid "Could not load file: {}" msgstr "Не вдалося завантажити файл: {}" #: core\app.py:382 msgid "'{}' already is in the list." msgstr "'{}' вже є в списку." #: core\app.py:384 msgid "'{}' does not exist." msgstr "'{}' не існує." #: core\app.py:392 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "" "Усі обрані %d результатів будуть ігноруватися під час усіх наступних " "пошуків. Продовжити?" #: core\app.py:469 msgid "Select a directory to copy marked files to" msgstr "Виберіть каталог для копіювання позначених файлів" #: core\app.py:471 msgid "Select a directory to move marked files to" msgstr "Виберіть каталог, куди ви хочете перемістити позначені файли" #: core\app.py:510 msgid "Select a destination for your exported CSV" msgstr "Виберіть каталог, куди потрібно скопіювати позначені файли" #: core\app.py:516 core\app.py:771 core\app.py:781 msgid "Couldn't write to file: {}" msgstr "Не вдалося записати у файл: {}" #: core\app.py:539 msgid "You have no custom command set up. Set it up in your preferences." msgstr "Власна команда не встановлена. Встановіть її у налаштуваннях." #: core\app.py:695 core\app.py:707 msgid "You are about to remove %d files from results. Continue?" msgstr "Ви збираєтеся видалити %d файлів з результату пошуку. Продовжити?" #: core\app.py:743 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "" "{} повторюваних груп було змінено шляхом повторного встановлення " "пріоритетів." #: core\app.py:790 msgid "The selected directories contain no scannable file." msgstr "Обрані папки не містять файлів придатних для пошуку." #: core\app.py:803 msgid "Collecting files to scan" msgstr "Збір файлів для пошуку" #: core\app.py:850 msgid "%s (%d discarded)" msgstr "%s (%d відкинуто)" #: core\directories.py:191 msgid "Collected {} files to scan" msgstr "" #: core\directories.py:207 msgid "Collected {} folders to scan" msgstr "" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "Ви надсилаєте {} файлів у Кошик." #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "Регулярні вирази" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "Ви дійсно хочете видалити всі %d елементів з чорного списку?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "Ім'я файлу" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "Назва файлу - поля" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "Назва файлу - поля (без замовлення)" #: core\me\scanner.py:23 msgid "Tags" msgstr "Теги" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "Зміст" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "Проаналізовано %d/%d фотографій" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "Виконано %d/%d порівнянь шматків" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "Підготовка до порівняння" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "Перевірено %d/%d результатів" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "Прочитано EXIF з %d/%d фотографій" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "Відмітка часу EXIF" #: core\prioritize.py:70 msgid "None" msgstr "Жоден" #: core\prioritize.py:100 msgid "Ends with number" msgstr "Закінчується номером" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "Не закінчується номером" #: core\prioritize.py:102 msgid "Longest" msgstr "Найдовший" #: core\prioritize.py:103 msgid "Shortest" msgstr "Найкоротший" #: core\prioritize.py:140 msgid "Highest" msgstr "Найвища" #: core\prioritize.py:140 msgid "Lowest" msgstr "Найнижча" #: core\prioritize.py:169 msgid "Newest" msgstr "Найновіші" #: core\prioritize.py:169 msgid "Oldest" msgstr "Найдавніший" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) дублікатів позначено." #: core\results.py:141 msgid " filter: %s" msgstr "фільтр: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "Прочитано розмір %d/%d файлів" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "Прочитано метаданих з %d/%d файлів" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "Майже зроблено! Возився з результатами..." #: core\se\scanner.py:18 msgid "Folders" msgstr "Папки" dupeguru-4.3.1/locale/uk/LC_MESSAGES/ui.po000077500000000000000000001224001426171743600200140ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2022 # Fuan , 2022 # msgid "" msgstr "" "Last-Translator: Fuan , 2022\n" "Language-Team: Ukrainian (https://www.transifex.com/voltaicideas/teams/116153/uk/)\n" "Language: uk\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n" #: qt/app.py:81 msgid "Quit" msgstr "Вихід" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "Опції" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Чорний список" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Очистити кеш зображень" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Довідка dupeGuru" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "Про dupeGuru" #: qt/app.py:87 msgid "Open Debug Log" msgstr "Відкрити журнал налагодження" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "Ви дійсно хочете видалити всі кешовані результати аналізу зображень?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "Кеш зображень очищено." #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} файл (*. {})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "варіанти делеций" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Посилання на видалені файли" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" "Після того, як віддаляється дублікат, розмістити посилання таргетування " "посилального файлу, щоб замінити віддалений файл." #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "Hardlink" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "Симлінк" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr "(не підтримується)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Безпосередньо видаляйте файли" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "Замість надсилання файлів у кошик, видаліть їх безпосередньо. Цей варіант " "зазвичай використовується як обхідний спосіб, коли звичайний метод видалення" " не працює." #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Продовжуйте" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Скасувати" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "Атрибут" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "Обраний" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "Посилання" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "Завантажити результати ..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "Вікно результатів" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "Додати папку ..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "Файл" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "Вид" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "Допомога" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "Завантажити нещодавні результати" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "Режим застосування:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "Музика" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "зображення" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "Стандартний" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "Тип пошуку:" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "Більше варіантів" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "Оберіть папки для пошуку і натисніть \"Шукати\"." #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "Завантажити результати" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "Шукати" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "Незбережені результати" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "Ви маєте незбережені результати, ви дійсно хочете вийти?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "Оберіть папку для додання в список пошуку" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "Виберіть файл результатів для завантаження" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "Всі файли (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "Результати dupeGuru (*.dupeguru) " #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "Почати новий пошук" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "Ви маєте незбережені результати, ви дійсно хочете продовжити?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "Ім'я" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "Стан" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "Виключений" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "Нормальний" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "Видалити обрані" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "Очистити" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "Закрити" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "Деталі" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "Теги для пошуку:" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "Трек" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "Артист" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "Альбом" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "Назва" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "Жанр" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "Рік" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "Порівнювати за словами" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "Вважати схожі слова однаковими" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "Можна змішувати типи файлів" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "Використовувати регулярних виразів при фільтрації" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "Видалити порожні папки під час видалення чи переміщення" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "Ігнорувати дублікати, що є жорсткими посиланнями на той самий файл" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "Режим налагодження (потрібен перезапуск)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "Порівнювати малюнки різних розмірів" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "Жорсткість фільтру:" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "Більше результатів" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "Менше результатів" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "Розмір шрифта:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "Мова:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "Копіювання і переміщення:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "Прямо у цільову папку" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "Відтворити відносний шлях" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "Відтворити абсолютний шлях" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "Власна команда (аргументи: %d для дублікату, %r для посиланя):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "dupeGuru необхідно перезапустити для застосування зміни мови." #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "Змінити пріоритети дублікатів" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "Додайте критерії в праве поле і натисніть кнопку ОК, щоб відправити " "дублікати, які найкраще відповідають цим критеріям, до вихідної позиції " "відповідних груп. Прочитайте файл довідки для отримання додаткової " "інформації." #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "Проблеми!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "Виникли проблеми під час обробки деяких (або всіх) файлів. Причини цих " "проблем описані в таблиці нижче. Ці файли не були видалені з результатів " "пошуку." #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "Показати вибрані" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "Дії" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "Показати тільки дуплікати" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "Показати різницю" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "Надіслати позначене до кошику..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "Перемістити позначене до ..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "Скопіювати позначене до ..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "Видалити позначене з результатів" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "Змінити пріоритети результатів ..." #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "Видалити обране з результатів" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "Додати обране в чорний список" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "Зробити обраний елемент в довідковий пункт." #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "Відкрити обране програмою за умовчанням" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "Відкрити папку, що містить обране" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "Перейменувати обране" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "Позначити всі" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "Скинути позначення" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "Інвертувати позначення" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "Позначити обране" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "Експорт в HTML" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "Експорт у CSV" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "Зберегти результати ..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "Викликати власну команду" #: qt/result_window.py:102 msgid "Mark" msgstr "Позначити" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "Колонки" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "Відновити налаштування за замовчуванням" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} Результати" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "Тільки дублікати" #: qt/result_window.py:194 msgid "Delta Values" msgstr "Різниця" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "Оберіть файл у який слід зберегти ваші результати" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "Ігнорувати файли менші ніж" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "КБ" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ Результати" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "Дія" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "Додати нову папку ..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "Розширені" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "Автоматично перевіряти наявність оновлень" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "Основні" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "Всі на передній план" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "Перевірити оновлення ..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "Закрити вікно" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "Копіювати" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "Власна комада (аргументи: %d для дублікату, %r для посилання):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "Вирізати" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Різниця" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "Інформація про обраний файл" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "Панель інформації" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "Папки" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "Налаштування dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "Результати dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "Веб-сайт dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "Редагувати" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "Експортувати результати до CSV" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "Експорт результатів в XHTML" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "Менше результатів" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "Фільтр" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "Жорсткість фільтру:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Фільтрувати результати ..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "Вікно вибору папок" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "Розмір шрифту:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "Приховати dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "Сховати інші" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "Ігнорувати файли менші ніж:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "Завантажити з файлу ..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "Мінімізувати" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "Режим" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "Більше результатів" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "Ok" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "Вставити" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "Налаштування ..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "Швидкий перегляд" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "Вийти з dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "Відновити налаштування за замовчуванням" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "Скинути до значень за замовчуванням" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "показувати" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Показати вибране у Finder" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "Виберіть Усі" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Надіслати позначене до кошику..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "Послуги" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "Показати всі" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "Почати пошук дублікатів" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "Ім’я '%@' вже існує." #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "Вікно" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Збільшити" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "Фільтри виключення" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "Результати сканування" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "Завантажити каталоги..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "Зберегти каталоги..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "Виберіть файл каталогів для завантаження." #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "dupeGuru довідники (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "Виберіть файл, до якого зберігатимуться ваші каталоги" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "dupeGuru довідники (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "Додати" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "Відновити значення за замовчуванням" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "Тестовий рядок" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "Введіть сюди регулярний вираз python..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "Введіть тут шлях до файлової системи або ім’я файлу..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Ці регулярні вирази python (чутливі до регістру) фільтруватимуть файли під час сканування.
    Також для каталогів буде встановлено статус за замовчуванням \"Виключено\" на вкладці \"Каталоги\", якщо їх ім'я збігається з одним із вибраних регулярних виразів.
    Для кожного зібраного файлу проводяться два тести, щоб визначити, чи повністю його ігнорувати:
  • 1. Регулярні вирази, у яких немає роздільника шляху, будуть порівнюватися лише з назвою файлу.
  • \n" "
  • 2. Регулярні вирази, що містять принаймні один роздільник шляхів, будуть порівняні з повним шляхом до файлу.

  • \n" "Приклад: якщо ви хочете відфільтрувати файли PNG лише з каталогу \"Мої фотографії\":
    .*My\\sPictures\\\\.*\\.png

    Ви можете перевірити регулярний вираз за допомогою кнопки \"тестовий рядок\" після вставки підробленого шляху в тестове поле:
    C:\\\\User\\My Pictures\\test.png

    \n" "Відповідні регулярні вирази будуть виділені.
    Якщо є хоча б одне виділення, тестований шлях або ім’я файлу буде проігноровано під час сканування.

    Каталоги та файли, що починаються з крапки '.' за замовчуванням відфільтровуються.

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "Помилка компіляції:" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "Збільште масштабування" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "Зменште масштабування" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "Звичайний розмір" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "Найкраще підходить" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "Режим кешування зображення:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "Замінити значки тем на панелі інструментів засобу перегляду" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" "Використовуйте наші власні внутрішні піктограми замість тих, що надаються " "механізмом теми" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "Показувати смуги прокрутки в засобах перегляду зображень" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" "Коли зображення, що відображається, не відповідає області перегляду, " "покажіть смуги прокрутки, щоб перемістити подання" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "" "Використовуйте положення за замовчуванням для панелі вкладок (потрібно " "перезапуск)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "Розташуйте панель вкладок під головним меню, а не поруч з нею.\n" "На MacOS, панель вкладок буде заповнити ширину вікна замість цього." #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "Використовуйте жирний шрифт для посилальних елементів." #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "Довідковий колір переднього плану:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "Довідковий колір тла:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "Колір переднього плану Delta:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "Показати рядок заголовка і може бути пристикований" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" "Поки рядок заголовка прихований, за допомогою клавіші модифікатора " "перетягніть плаваюче вікно навколо" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "Рядок заголовка можна вимкнути лише тоді, коли вікно закріплено" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "Вертикальний рядок заголовка" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "" "Змініть рядок заголовка з горизонтального зверху на вертикальний зліва" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "Показати панель вкладок" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Ці регулярні вирази python (чутливі до регістру) фільтруватимуть файли під час сканування.
    Також для каталогів буде встановлено статус за замовчуванням \"Виключено\" на вкладці \"Каталоги\", якщо їх ім'я збігається з одним із вибраних регулярних виразів.
    Для кожного зібраного файлу проводяться два тести, щоб визначити, чи повністю його ігнорувати:
  • 1. Регулярні вирази, у яких немає роздільника шляху, будуть порівнюватися лише з назвою файлу.
  • \n" "
  • 2. Регулярні вирази, що містять принаймні один роздільник шляхів, будуть порівняні з повним шляхом до файлу.

  • \n" "Приклад: якщо ви хочете відфільтрувати файли PNG лише з каталогу \"Мої фотографії\":
    .*My\\sPictures\\\\.*\\.png

    Ви можете перевірити регулярний вираз за допомогою кнопки \"тестовий рядок\" після вставки підробленого шляху в тестове поле:
    C:\\\\User\\My Pictures\\test.png

    \n" "Відповідні регулярні вирази будуть виділені.
    Якщо є хоча б одне виділення, тестований шлях або ім’я файлу буде проігноровано під час сканування.

    Каталоги та файли, що починаються з крапки '.' за замовчуванням відфільтровуються.

    " #: qt\app.py:256 msgid "Results" msgstr "Результати" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "Загальний інтерфейс" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "Таблиця результатів" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "Вікно деталей" #: qt\preferences_dialog.py:285 msgid "General" msgstr "Загальні" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "Дисплей" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" #: qt\app.py:299 msgid "Cache cleared." msgstr "" #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "" #: qt\about_box.py:31 msgid "About {}" msgstr "Про {}" #: qt\about_box.py:47 msgid "Version {}" msgstr "Версія {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "" #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "Ліцензовано згідно з GPLv3" #: qt\about_box.py:68 msgid "No update available." msgstr "" #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "Повідомлення про помилки" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "Щось пішло не так. Як щодо повідомлення про помилку? " #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "Звіти про помилки слід повідомляти як проблеми Github. Ви можете скопіювати помилку відстеження помилки вище та вставити її в нове видання.\n" "\n" "Будь ласка, не забудьте заздалегідь здійснити пошук уже існуючих проблем. Також не забудьте протестувати найновішу версію, доступну зі сховища, оскільки виправлена помилка, можливо, вже виправлена.\n" "\n" "Зазвичай справді допомагає, якщо ви додаєте опис того, як ви отримали помилку. Дякую!\n" "\n" "Незважаючи на те, що програма повинна продовжувати працювати після цієї помилки, вона може бути в нестабільному стані, тому рекомендується перезапустити програму." #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Перейдіть до Github" #: qt\preferences.py:24 msgid "Czech" msgstr "Чеська" #: qt\preferences.py:25 msgid "German" msgstr "Німецька" #: qt\preferences.py:26 msgid "Greek" msgstr "Німецька" #: qt\preferences.py:27 msgid "English" msgstr "Англійська" #: qt\preferences.py:28 msgid "Spanish" msgstr "Іспанська" #: qt\preferences.py:29 msgid "French" msgstr "Французька" #: qt\preferences.py:30 msgid "Armenian" msgstr "Вірменська" #: qt\preferences.py:31 msgid "Italian" msgstr "Італійська" #: qt\preferences.py:32 msgid "Japanese" msgstr "Японський" #: qt\preferences.py:33 msgid "Korean" msgstr "Корейська" #: qt\preferences.py:34 msgid "Malay" msgstr "Малайська" #: qt\preferences.py:35 msgid "Dutch" msgstr "Голландська" #: qt\preferences.py:36 msgid "Polish" msgstr "Польська" #: qt\preferences.py:37 msgid "Brazilian" msgstr "Бразильська" #: qt\preferences.py:38 msgid "Russian" msgstr "Російська" #: qt\preferences.py:39 msgid "Turkish" msgstr "Турецька" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "Українська" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "В'єтнамська" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "Китайська (спрощена)" #: qt\recent.py:54 msgid "Clear List" msgstr "Очистити список" #: qt\search_edit.py:78 msgid "Search..." msgstr "Шукати..." dupeguru-4.3.1/locale/vi/000077500000000000000000000000001426171743600152475ustar00rootroot00000000000000dupeguru-4.3.1/locale/vi/LC_MESSAGES/000077500000000000000000000000001426171743600170345ustar00rootroot00000000000000dupeguru-4.3.1/locale/vi/LC_MESSAGES/columns.po000066400000000000000000000053721426171743600210630ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # msgid "" msgstr "" "Last-Translator: Andrew Senetar , 2021\n" "Language-Team: Vietnamese (https://www.transifex.com/voltaicideas/teams/116153/vi/)\n" "Language: vi\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "Đường dẫn tập tin" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "Thông báo lỗi" #: core\me\prioritize.py:23 msgid "Duration" msgstr "Độ dài" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "Bitrate" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "Samplerate" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "Tên tập tin" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "Thư mục" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "Kích thước (MB)" #: core\me\result_table.py:22 msgid "Time" msgstr "Thời gian" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "Sample Rate" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "Loại" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "Chỉnh sửa" #: core\me\result_table.py:27 msgid "Title" msgstr "Tiêu đề" #: core\me\result_table.py:28 msgid "Artist" msgstr "Nghệ sĩ" #: core\me\result_table.py:29 msgid "Album" msgstr "Album" #: core\me\result_table.py:30 msgid "Genre" msgstr "Loại nhạc" #: core\me\result_table.py:31 msgid "Year" msgstr "Năm" #: core\me\result_table.py:32 msgid "Track Number" msgstr "Số track" #: core\me\result_table.py:33 msgid "Comment" msgstr "Bình luận" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "Tỉ lệ khớp %" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "Từ được dùng" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "Số lần bị lừa" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "Chiều" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "Kích thước (KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "EXIF Timestamp" #: core\prioritize.py:156 msgid "Size" msgstr "Kích thước" dupeguru-4.3.1/locale/vi/LC_MESSAGES/core.po000066400000000000000000000170351426171743600203320ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Fuan , 2021 # msgid "" msgstr "" "Last-Translator: Fuan , 2021\n" "Language-Team: Vietnamese (https://www.transifex.com/voltaicideas/teams/116153/vi/)\n" "Language: vi\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "" "Không có phần đánh dấu nào trùng nhau. Vẫn chưa thực hiện thao tác nào." #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "" "Không có phần đánh dấu nào trùng nhau. Vẫn chưa thực hiện thao tác nào." #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" "Bạn chuẩn bị mở nhiều tập tin cùng lúc. Dựa trên chương trình các tập tin " "được mở, thao tác này có thể gây ra trạng thái lộn xộn. Vẫn muốn tiếp tục?" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "Quét các phần trùng nhau" #: core\app.py:72 msgid "Loading" msgstr "Đang tải" #: core\app.py:73 msgid "Moving" msgstr "Đang di chuyển" #: core\app.py:74 msgid "Copying" msgstr "Đang sao chép" #: core\app.py:75 msgid "Sending to Trash" msgstr "Đang gửi vào thùng rác" #: core\app.py:289 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" "Hiện đã có một tiến trình đang được tiến hành. Bạn không thể bắt đầu một " "phần khác. Hãy đợi trong vài giây, và sau đó thử lại lần nữa." #: core\app.py:300 msgid "No duplicates found." msgstr "Không tìm thấy thành phần trùng nhau." #: core\app.py:315 msgid "All marked files were copied successfully." msgstr "Tất cả tập tin được đánh dấu đã được sao chép thành công." #: core\app.py:317 msgid "All marked files were moved successfully." msgstr "Tất cả các tập tin được đánh dấu đã được di chuyển thành công." #: core\app.py:319 msgid "All marked files were deleted successfully." msgstr "" #: core\app.py:321 msgid "All marked files were successfully sent to Trash." msgstr "" "Tất cả các tập tin được đánh dấu đã được gửi đến Thùng Rác thành công." #: core\app.py:326 msgid "Could not load file: {}" msgstr "Không thể tải tệp: {}" #: core\app.py:382 msgid "'{}' already is in the list." msgstr "'{}' đã tồn tại trong danh sách." #: core\app.py:384 msgid "'{}' does not exist." msgstr "'{}' không tồn tại." #: core\app.py:392 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "" "Các phần được chọn %d khớp với nhau sẽ được bỏ qua trong các lần quét sau. " "Tiếp tục?" #: core\app.py:469 msgid "Select a directory to copy marked files to" msgstr "" "Vui lòng chọn một thư mục mà bạn muốn sao chép các tệp đã đánh dấu vào" #: core\app.py:471 msgid "Select a directory to move marked files to" msgstr "" "Vui lòng chọn một thư mục mà bạn muốn di chuyển các tệp đã đánh dấu đến" #: core\app.py:510 msgid "Select a destination for your exported CSV" msgstr "Chọn một điểm xuất dữ liệu dạng CSV" #: core\app.py:516 core\app.py:771 core\app.py:781 msgid "Couldn't write to file: {}" msgstr "Không thể ghi vào tệp: {}" #: core\app.py:539 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" "Bạn vẫn chưa chỉnh sửa phần thiết lập dòng lệnh. Hãy sử dụng tính năng này " "trong phần tùy biến của bạn." #: core\app.py:695 core\app.py:707 msgid "You are about to remove %d files from results. Continue?" msgstr "Bạn chuẩn bị loại bỏ %d tập tin từ phần kết quả. Tiếp tục?" #: core\app.py:743 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} các nhóm trùng nhau đã được thay đổi bởi thứ tự-tái ưu tiên." #: core\app.py:790 msgid "The selected directories contain no scannable file." msgstr "Các thứ mục được chọn chứa các tập tin không thể quét được." #: core\app.py:803 msgid "Collecting files to scan" msgstr "Đang thu thập các tập tin để quét" #: core\app.py:850 msgid "%s (%d discarded)" msgstr "%s (%d bị bỏ qua)" #: core\directories.py:191 msgid "Collected {} files to scan" msgstr "" #: core\directories.py:207 msgid "Collected {} folders to scan" msgstr "" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "Bạn sắp sửa gửi {} (các)tập tin đến Thùng Rác." #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "Biểu thức chính quy" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "Bạn có thực sự muốn loại bỏ tất cả %d đối tượng từ danh sách bỏ qua?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "Tên tệp" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "Tên tệp - Trường" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "Tên tệp - Trường (Không có thứ tự)" #: core\me\scanner.py:23 msgid "Tags" msgstr "Tags" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "Nội dung" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "Đã phân tích %d/%d hình ảnh" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "Đã thể thiện %d/%d các phần khớp nhau" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "Đang chuẩn bị phần khớp nhau" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "Đã xác nhận %d/%d phần khớp nhau" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "Đọc thông tin EXIF của %d/%d hình ảnh" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "Dấu thời gian EXIF" #: core\prioritize.py:70 msgid "None" msgstr "Không " #: core\prioritize.py:100 msgid "Ends with number" msgstr "Tận cùng là số" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "Tận cùng không chứa số" #: core\prioritize.py:102 msgid "Longest" msgstr "Dài nhất" #: core\prioritize.py:103 msgid "Shortest" msgstr "Ngắn nhất" #: core\prioritize.py:140 msgid "Highest" msgstr "Cao nhất" #: core\prioritize.py:140 msgid "Lowest" msgstr "Thấp nhất" #: core\prioritize.py:169 msgid "Newest" msgstr "Mới nhất" #: core\prioritize.py:169 msgid "Oldest" msgstr "Cũ nhất" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) phần trùng nhau đã được đánh dấu." #: core\results.py:141 msgid " filter: %s" msgstr " bộ lọc: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "Đọc kích thước của các tập tin %d/%d" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "Đọc thông tin chi tiết của %d/%d tập tin" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "Sắp xong! Loay hoay với kết quả..." #: core\se\scanner.py:18 msgid "Folders" msgstr "Thư mục" dupeguru-4.3.1/locale/vi/LC_MESSAGES/ui.po000066400000000000000000001117461426171743600200230ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2022 # Fuan , 2022 # msgid "" msgstr "" "Last-Translator: Fuan , 2022\n" "Language-Team: Vietnamese (https://www.transifex.com/voltaicideas/teams/116153/vi/)\n" "Language: vi\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: qt/app.py:81 msgid "Quit" msgstr "Thoát" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "Tùy chọn" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Danh sách bỏ qua" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Dọn dẹp bộ nhớ đệm của hình ảnh" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Trợ giúp" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "Dịch bởi Phan Anh" #: qt/app.py:87 msgid "Open Debug Log" msgstr "Mở nhật trình gỡ rối" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "" "Bạn có muốn loại bỏ toàn bộ các phân tích trong bộ nhớ đệm về hình ảnh hay " "không?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "Đã dọn dẹp bộ nhớ đệm xử lý hình ảnh." #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} tập tin (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Xóa tùy chọn" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Liên kết đến các tập tin đã bị xóa" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" "Sau khi đã xóa một đối tượng bị trùng, đặt một liên kết chỉ thẳng nhằm tham " "chiếu đến tập tin để thay thế tập tin đã được xóa." #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "Liên kết cứng" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "Liên kết biểu tượng" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr " (chưa được hỗ trợ)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Trực tiếp xóa các tập tin" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" "Thay vì gửi các tập tin vào Thùng Rác thì bạn có thể xóa trực tiếp. Tùy chọn" " này thường được dùng trong các môi trường làm việc nơi mà các tác dụng xóa " "những tập tin theo cách thông thường không được áp dụng." #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Tiếp tục" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Hủy bỏ" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "Thuộc tính" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "Được chọn" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "Tham chiếu" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "Tải kết quả..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "Cửa sổ hiển thị kết quả" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "Thêm thư mục..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "Tập tin" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "Xem" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "Trợ giúp" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "Tải các kết quả gần đây" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "Chế độ ứng dụng:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "Âm nhạc" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "Hình ảnh" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "Tiêu chuẩn" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "Loại thao tác quét:" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "Các tùy chọn khác" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "Chọn thư mục để quét và nhấn \"Quét\"." #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "Tải kết quả" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "Quét" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "Các kết quả chưa lưu" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "Bạn vẫn chưa lưu các kết quả, bạn có thực sự muốn thoát?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "Chọn một thư mục để thêm vào danh sách quét" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "Chọn một tập tin kết quả để tải" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "Tất cả tập tin (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "tập tin kết quả của dupeGuru (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "Bắt đầu quá trình quét mới" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "" "Bạn vẫn chưa lưu các kết quả vừa quét, bạn có muốn tiếp tục hay không?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "Tên" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "Trạng thái" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "Bao gồm" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "Bình thường" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "Loại bỏ phần đã chọn" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "Dọn dẹp" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "Đóng lại" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "Chi tiết" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "Thẻ đánh dấu để quét:" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "Track" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "Nghệ sĩ" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "Album" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "Tiêu đề" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "Loại nhạc" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "Năm" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "Độ rộng của từ" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "Các từ tương tự và khớp lẫn nhau" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "Có thể pha trộn loại tập tin" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "Sử dụng các biểu thức thông thường khi lọc dữ liệu" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "Loại bỏ các thư mục rỗng khi xóa hoặc di chuyển" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "Bỏ qua các liên kết cứng đến phần trùng nhau trong cùng một tập tin" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "Chế độ gỡ rối (yêu cầu khởi động lại)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "Chỉ các hình ảnh khớp nhau với các chiều khác nhau" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "Lọc theo nguyên tắc:" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "Nhiều kết quả hơn" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "Ít kết quả hơn" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "Kích thước kiểu chữ:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "Ngôn ngữ:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "Sao chép và di chuyển:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "Ngay tại đích đến" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "Tạo lại các đường dẫn liên hệ lẫn nhau" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "Tạo lại đường dẫn xác thực" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "Lệnh tùy chọn (đối số: %d cho trùng nhau, %r cho tham số):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "" "dupeGure phải khởi động lại để việc thay đổi ngôn ngữ giao diện được áp " "dụng." #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "Tái-Ưu tiên phần trùng nhau" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "Xảy ra vấn đề!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "Biểu hiện các phần được chọn" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "Thao tác" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "Chỉ hiển thị trùng" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "Hiển thị giá trị Delta" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "Gửi các phần đánh dấu vào Thùng Rác..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "Di chuyển phần đánh dấu đến..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "Sao chép phần đánh dấu đến..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "Loại bỏ phần được đánh dấu khỏi kết quả" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "Tái-ưu tiên kết quả..." #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "Loại bỏ các phần được chọn khỏi kết quả" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "Thêm phần được chọn vào danh sách bỏ qua" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "Đem các phần được đánh dấu vào mục Tham Chiếu" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "Mở phần được chọn với ứng dụng mặc định" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "Mở thư mục chứa phần được lựa chọn" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "Lựa chọn được đổi tên" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "Đánh dấu tất cả" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "Không đánh dấu đối tượng nào" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "Chuyển đổi phần đánh dấu" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "Đánh dấu phần được chọn" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "Xuất sang định dạng HTML" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "Xuất sang định dạng CSV" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "Lưu kết quả..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "Dòng lệnh tùy chọn khẩn cấp" #: qt/result_window.py:102 msgid "Mark" msgstr "Đánh dấu" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "Cột" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "Thiết đặt lại về mặc định" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} Kết quả" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "Chỉ trùng nhau" #: qt/result_window.py:194 msgid "Delta Values" msgstr "Giá trị Delta" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "Chọn một tập tin để lưu kết quả vào" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "Bỏ qua các tập tin có kích thước nhỏ hơn" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ Kết quả" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "Thao tác" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "Thêm thư mục mới..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "nâng cao" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "Tự động kiểm tra cập nhật" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "Cơ bản" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "Đem tất cả lên phía trước" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "Kiểm tra cập nhật..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "Đóng lại cửa sổ" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "Sao chép" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "Lệnh tùy chọn (đối số: %d cho phần trùng, %r cho phần đối chiếu):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "Cắt" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Delta" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "Chi tiết tập tin được chọn" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "Khung cửa sổ chi tiết" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "Thư mục" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "Tùy biến" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "Kết quả" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "Website" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "Chỉnh sửa" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "Xuất kết quả sang dạng CSV" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "Xuất kết quả sang dạng XHTML" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "Ít kết quả hơn" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "Lọc dữ liệu" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "Lọc dữ liệu theo quy luật:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "Kết quả dữ liệu được lọc..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "Cửa sổ chọn thư mục" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "Kích thước kiểu chữ:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "Ẩn chương trình" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "Ẩn các phần khác" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "Bỏ qua các tập tin nhỏ hơn:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "Tải từ tập tin..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "Thu nhỏ" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "Chế độ" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "Thêm các kết quả" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "Đồng ý" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "Dán" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "Tùy biến..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "Xem sơ lược" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "Thoát chương trình" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "Thiết đặt lại về chế độ mặc định" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "Thiết đặt lại về chế độ mặc định" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "Biểu hiện" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "Biểu hiện các phần được chọn trong phần Tìm Kiếm" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "Chọn tất cả" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Gửi phần được đánh dấu vào Thùng Rác..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "Dịch vụ" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "Hiển thị tất cả" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "Bắt đầu quét phần trùng nhau" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "Tên '%@' đã tồn tại." #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "Cửa sổ" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Phóng to" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "Bộ lọc Loại trừ" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "Quét kết quả" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "Nạp Thư mục..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "Lưu các thư mục..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "Chọn một tập tin thư mục để tải" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "dupeGuru Thư mục (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "Chọn một tệp để lưu các thư mục của bạn vào" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "dupeGuru Thư mục (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "Thêm vào" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "Khôi phục mặc định" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "Thử nghiệm chuỗi ký tự" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "Nhập một biểu thức chính quy python tại đây..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "Nhập đường dẫn hệ thống tệp hoặc tên tệp tại đây..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Các biểu thức chính quy python (phân biệt chữ hoa chữ thường) này sẽ lọc ra các tệp trong quá trình quét.
    Các thư mục cũng sẽ có trạng thái mặc định được đặt thành \"Bị loại trừ\" trong tab \"Thư mục\" nếu tên của chúng khớp với một trong các biểu thức chính quy đã chọn.
    Đối với mỗi tệp được thu thập, hai bài kiểm tra được thực hiện để xác định xem có bỏ qua hoàn toàn tệp đó hay không:
  • 1. Biểu thức chính quy không có dấu phân tách đường dẫn sẽ chỉ được so sánh với tên tệp.
  • \n" "
  • 2. Biểu thức chính quy có ít nhất một dấu phân cách đường dẫn sẽ được so sánh với đường dẫn đầy đủ đến tệp.

  • \n" "Ví dụ: nếu bạn chỉ muốn lọc các tệp PNG từ thư mục \"Ảnh của tôi\":
    .*My\\sPictures\\\\.*\\.png

    Bạn có thể kiểm tra biểu thức chính quy bằng nút \"chuỗi kiểm tra\" sau khi dán đường dẫn giả vào trường kiểm tra:
    C:\\\\User\\My Pictures\\test.png

    \n" "Các cụm từ thông dụng phù hợp sẽ được đánh dấu.
    Nếu có ít nhất một điểm đánh dấu, đường dẫn hoặc tên tệp được kiểm tra sẽ bị bỏ qua trong quá trình quét.

    Các thư mục và tệp bắt đầu bằng dấu chấm '.' được lọc ra theo mặc định.

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "Lỗi biên dịch" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "Tăng thu phóng" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "Giảm thu phóng" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "Kích thước bình thường" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "Phù hợp nhất" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "Chế độ bộ nhớ cache hình ảnh:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "Ghi đè các biểu tượng chủ đề trong thanh công cụ của trình xem" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "" "Sử dụng các biểu tượng nội bộ của riêng chúng tôi thay vì những biểu tượng " "được cung cấp bởi công cụ chủ đề" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "Hiển thị thanh cuộn trong trình xem hình ảnh" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "" "Khi hình ảnh được hiển thị không vừa với chế độ xem, hãy hiển thị các thanh " "cuộn để di chuyển chế độ xem xung quanh" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "Sử dụng vị trí mặc định cho thanh tab (yêu cầu khởi động lại)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "Đặt thanh tab bên dưới menu chính thay vì bên cạnh nó.\n" "Trên MacOS, thay vào đó, thanh tab sẽ lấp đầy chiều rộng của cửa sổ." #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "Sử dụng phông chữ đậm cho tài liệu tham khảo" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "Màu nền trước tham khảo:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "Màu nền tham chiếu" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "Màu nền trước Delta:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "Hiển thị thanh tiêu đề và có thể được cập cảng" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "" "Trong khi thanh tiêu đề bị ẩn, hãy sử dụng phím bổ trợ để kéo cửa sổ nổi " "xung quanh" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "Thanh tiêu đề chỉ có thể bị vô hiệu hóa khi cửa sổ được gắn vào đế" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "Thanh tiêu đề dọc" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "Thay đổi thanh tiêu đề từ ngang ở trên, sang dọc ở bên trái" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "Hiển thị thanh tab" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "Các biểu thức chính quy python (phân biệt chữ hoa chữ thường) này sẽ lọc ra các tệp trong quá trình quét.
    Các thư mục cũng sẽ có trạng thái mặc định được đặt thành \"Bị loại trừ\" trong tab \"Thư mục\" nếu tên của chúng khớp với một trong các biểu thức chính quy đã chọn.
    Đối với mỗi tệp được thu thập, hai bài kiểm tra được thực hiện để xác định xem có bỏ qua hoàn toàn tệp đó hay không:
  • 1. Biểu thức chính quy không có dấu phân tách đường dẫn sẽ chỉ được so sánh với tên tệp.
  • \n" "
  • 2. Biểu thức chính quy có ít nhất một dấu phân cách đường dẫn sẽ được so sánh với đường dẫn đầy đủ đến tệp.

  • \n" "Ví dụ: nếu bạn chỉ muốn lọc các tệp PNG từ thư mục \"Ảnh của tôi\":
    .*My\\sPictures\\\\.*\\.png

    Bạn có thể kiểm tra biểu thức chính quy bằng nút \"chuỗi kiểm tra\" sau khi dán đường dẫn giả vào trường kiểm tra:
    C:\\\\User\\My Pictures\\test.png

    \n" "Các cụm từ thông dụng phù hợp sẽ được đánh dấu.
    Nếu có ít nhất một điểm đánh dấu, đường dẫn hoặc tên tệp được kiểm tra sẽ bị bỏ qua trong quá trình quét.

    Các thư mục và tệp bắt đầu bằng dấu chấm '.' được lọc ra theo mặc định.

    " #: qt\app.py:256 msgid "Results" msgstr "Kết quả" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "Giao diện chung" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "Kết quả Bảng" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "Cửa sổ chi tiết" #: qt\preferences_dialog.py:285 msgid "General" msgstr "Chung" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "Trưng bày" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "" #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" #: qt\app.py:299 msgid "Cache cleared." msgstr "" #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "" #: qt\about_box.py:31 msgid "About {}" msgstr "Về {}" #: qt\about_box.py:47 msgid "Version {}" msgstr "Phiên bản {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "" #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "Được cấp phép theo GPLv3" #: qt\about_box.py:68 msgid "No update available." msgstr "" #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "Báo cáo lỗi" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "Đã xảy ra lỗi. Làm thế nào về việc báo cáo lỗi?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "Các báo cáo lỗi phải được báo cáo dưới dạng sự cố Github. Bạn có thể sao chép các traceback lỗi trên và dán nó vào một vấn đề mới.\n" "\n" "Vui lòng đảm bảo chạy tìm kiếm bất kỳ vấn đề nào đã tồn tại trước đó. Ngoài ra, hãy đảm bảo kiểm tra phiên bản mới nhất có sẵn từ kho lưu trữ, vì lỗi bạn đang gặp phải có thể đã được vá.\n" "\n" "Điều thường thực sự hữu ích là nếu bạn thêm mô tả về cách bạn gặp lỗi. Cảm ơn!\n" "\n" "Mặc dù ứng dụng sẽ tiếp tục chạy sau lỗi này, nhưng nó có thể ở trạng thái không ổn định, vì vậy bạn nên khởi động lại ứng dụng." #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "Truy cập Github" #: qt\preferences.py:24 msgid "Czech" msgstr "Tiếng Séc" #: qt\preferences.py:25 msgid "German" msgstr "Tiếng Đức" #: qt\preferences.py:26 msgid "Greek" msgstr "Ngôn ngữ Hy lạp" #: qt\preferences.py:27 msgid "English" msgstr "Ngôn ngữ tiếng anh" #: qt\preferences.py:28 msgid "Spanish" msgstr "Tiếng Tây Ban Nha" #: qt\preferences.py:29 msgid "French" msgstr "ngôn ngữ Pháp" #: qt\preferences.py:30 msgid "Armenian" msgstr "ngôn ngữ Armenia" #: qt\preferences.py:31 msgid "Italian" msgstr "Ngôn ngữ Ý" #: qt\preferences.py:32 msgid "Japanese" msgstr "tiếng Nhật" #: qt\preferences.py:33 msgid "Korean" msgstr "Ngôn ngữ Hàn Quốc" #: qt\preferences.py:34 msgid "Malay" msgstr "Tiếng Mã lai" #: qt\preferences.py:35 msgid "Dutch" msgstr "Tiếng Hà Lan" #: qt\preferences.py:36 msgid "Polish" msgstr "Ngôn ngữ Ba Lan" #: qt\preferences.py:37 msgid "Brazilian" msgstr "Ngôn ngữ Brazil" #: qt\preferences.py:38 msgid "Russian" msgstr "Ngôn ngữ Nga" #: qt\preferences.py:39 msgid "Turkish" msgstr "Tiếng Thổ Nhĩ Kỳ" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "Tiếng Ukraina" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "Ngôn ngữ tiếng Việt" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "Ngôn ngữ Trung Quốc (giản thể)" #: qt\recent.py:54 msgid "Clear List" msgstr "Xóa danh sách" #: qt\search_edit.py:78 msgid "Search..." msgstr "Tìm kiếm..." dupeguru-4.3.1/locale/zh_CN/000077500000000000000000000000001426171743600156325ustar00rootroot00000000000000dupeguru-4.3.1/locale/zh_CN/LC_MESSAGES/000077500000000000000000000000001426171743600174175ustar00rootroot00000000000000dupeguru-4.3.1/locale/zh_CN/LC_MESSAGES/columns.po000066400000000000000000000052631426171743600214450ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Chris Ocelot, 2021 # msgid "" msgstr "" "Last-Translator: Chris Ocelot, 2021\n" "Language-Team: Chinese (China) (https://www.transifex.com/voltaicideas/teams/116153/zh_CN/)\n" "Language: zh_CN\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "文件路径" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "错误信息" #: core\me\prioritize.py:23 msgid "Duration" msgstr "持续时间" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "比特率" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "采样率" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\se\result_table.py:19 msgid "Filename" msgstr "文件名称" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "文件夹" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "大小 (MB)" #: core\me\result_table.py:22 msgid "Time" msgstr "时间" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "采样率" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "类型" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:163 core\se\result_table.py:23 msgid "Modification" msgstr "编辑日期" #: core\me\result_table.py:27 msgid "Title" msgstr "歌曲名" #: core\me\result_table.py:28 msgid "Artist" msgstr "作者" #: core\me\result_table.py:29 msgid "Album" msgstr "专辑" #: core\me\result_table.py:30 msgid "Genre" msgstr "音乐类型" #: core\me\result_table.py:31 msgid "Year" msgstr "年" #: core\me\result_table.py:32 msgid "Track Number" msgstr "音轨号" #: core\me\result_table.py:33 msgid "Comment" msgstr "注释" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "匹配度 %" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "使用过的词语" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "重复文件数" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "规格" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "大小 (KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "EXIF 时间戳" #: core\prioritize.py:156 msgid "Size" msgstr "大小" dupeguru-4.3.1/locale/zh_CN/LC_MESSAGES/core.po000066400000000000000000000152311426171743600207110ustar00rootroot00000000000000# Translators: # Andrew Senetar , 2021 # Chris Ocelot, 2021 # Fuan , 2021 # YaNing Lu, 2021 # msgid "" msgstr "" "Last-Translator: YaNing Lu, 2021\n" "Language-Team: Chinese (China) (https://www.transifex.com/voltaicideas/teams/116153/zh_CN/)\n" "Language: zh_CN\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: core\app.py:42 msgid "There are no marked duplicates. Nothing has been done." msgstr "没有已标记的重复项。无需任何操作。" #: core\app.py:43 msgid "There are no selected duplicates. Nothing has been done." msgstr "没有已选定的重复项。无需任何操作。" #: core\app.py:44 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "您即将一次性打开多个文件。取决于这些文件的默认打开方式,此项操作可能导致非常混乱的状况。是否继续?" #: core\app.py:71 msgid "Scanning for duplicates" msgstr "正在扫描重复内容" #: core\app.py:72 msgid "Loading" msgstr "载入中" #: core\app.py:73 msgid "Moving" msgstr "移动中" #: core\app.py:74 msgid "Copying" msgstr "复制中" #: core\app.py:75 msgid "Sending to Trash" msgstr "正在移至回收站" #: core\app.py:289 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "前一项操作还在执行,无法启动新操作。请等待几秒钟后再重试一次。" #: core\app.py:300 msgid "No duplicates found." msgstr "没有找到重复文件。" #: core\app.py:315 msgid "All marked files were copied successfully." msgstr "所有已标记的文件已复制成功。" #: core\app.py:317 msgid "All marked files were moved successfully." msgstr "所有已标记的文件已移动成功。" #: core\app.py:319 msgid "All marked files were deleted successfully." msgstr "已复制所有标记文件" #: core\app.py:321 msgid "All marked files were successfully sent to Trash." msgstr "所有已标记的文件已成功移至回收站。" #: core\app.py:326 msgid "Could not load file: {}" msgstr "无法加载文件: {}" #: core\app.py:382 msgid "'{}' already is in the list." msgstr "'{}' 已在列表中。" #: core\app.py:384 msgid "'{}' does not exist." msgstr "'{}' 不存在。" #: core\app.py:392 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "目前已选的 %d 个匹配项将在后续的扫描中被忽略。是否继续?" #: core\app.py:469 msgid "Select a directory to copy marked files to" msgstr "请选择要将标记文件复制到的目录" #: core\app.py:471 msgid "Select a directory to move marked files to" msgstr "请选择要将标记文件移动到的目录" #: core\app.py:510 msgid "Select a destination for your exported CSV" msgstr "选择您导出 CSV 的目标文件夹" #: core\app.py:516 core\app.py:771 core\app.py:781 msgid "Couldn't write to file: {}" msgstr "不能写入文件: {}" #: core\app.py:539 msgid "You have no custom command set up. Set it up in your preferences." msgstr "您没有设定自定义命令。请在设置中进行设定。" #: core\app.py:695 core\app.py:707 msgid "You are about to remove %d files from results. Continue?" msgstr "您将从结果中移除 %d 个文件。是否继续?" #: core\app.py:743 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{}个重复的组已被重新排列。" #: core\app.py:790 msgid "The selected directories contain no scannable file." msgstr "所选目录中不包含可供扫描的文件。" #: core\app.py:803 msgid "Collecting files to scan" msgstr "收集文件以供扫描" #: core\app.py:850 msgid "%s (%d discarded)" msgstr "%s (%d 项已丢弃)" #: core\directories.py:191 msgid "Collected {} files to scan" msgstr "收集要扫描的{}文件" #: core\directories.py:207 msgid "Collected {} folders to scan" msgstr "收集要扫描的{}文件夹" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "从1%d组中找到1%d个匹配" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "您正在移动 {} 个文件至回收站。" #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "正则表达式" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "确定要从忽略列表中移除所有 %d 项吗?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "文件名" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "分组比较文件名(如作者-歌曲名)" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "分组比较文件名(不固定顺序(如作者-歌曲名或者歌曲名-作者))" #: core\me\scanner.py:23 msgid "Tags" msgstr "标签" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "内容" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "已分析 %d/%d 图像" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "已执行 %d/%d 个区块匹配" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "准备进行匹配" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "已验证 %d/%d 匹配项" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "已读取 %d/%d 张图片的 EXIF" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "EXIF 时间戳" #: core\prioritize.py:70 msgid "None" msgstr "无" #: core\prioritize.py:100 msgid "Ends with number" msgstr "以数字结尾" #: core\prioritize.py:101 msgid "Doesn't end with number" msgstr "不以数字结尾" #: core\prioritize.py:102 msgid "Longest" msgstr "最长" #: core\prioritize.py:103 msgid "Shortest" msgstr "最短" #: core\prioritize.py:140 msgid "Highest" msgstr "最高" #: core\prioritize.py:140 msgid "Lowest" msgstr "最低" #: core\prioritize.py:169 msgid "Newest" msgstr "最新" #: core\prioritize.py:169 msgid "Oldest" msgstr "最旧" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "已标记 %d / %d (%s / %s) 个重复项。" #: core\results.py:141 msgid " filter: %s" msgstr " 过滤: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "已读取 %d/%d 文件大小" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "已读取 %d/%d 文件元数据" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "即将完成!整理结果中..." #: core\se\scanner.py:18 msgid "Folders" msgstr "文件夹" dupeguru-4.3.1/locale/zh_CN/LC_MESSAGES/ui.po000066400000000000000000001017461426171743600204050ustar00rootroot00000000000000# Translators: # Fuan , 2022 # 太子 VC , 2022 # Andrew Senetar , 2022 # Chris Ocelot, 2022 # msgid "" msgstr "" "Last-Translator: Chris Ocelot, 2022\n" "Language-Team: Chinese (China) (https://www.transifex.com/voltaicideas/teams/116153/zh_CN/)\n" "Language: zh_CN\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: qt/app.py:81 msgid "Quit" msgstr "退出" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "选项" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "忽略列表" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "清空图片缓存" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "dupeGuru 帮助" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "关于 dupeGuru" #: qt/app.py:87 msgid "Open Debug Log" msgstr "打开调试记录" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "确定要移除所有缓存图片?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "图片缓存已清空。" #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} 文件 (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "删除选项" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "链接已删除的文件" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "在删除重复文件后,以源文件的链接来替代已删除的文件。" #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "硬链接" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "符号链接" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr " (不支持)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "直接删除文件" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "直接将文件删除,而不是将其移至回收站。此选项通常作为普通删除方法不能正常使用时替代方案。" #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "继续" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "取消" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "属性" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "已选择" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "源文件" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "载入结果..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "结果窗口" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "添加文件夹..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "文件" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "视图" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "帮助" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "载入最近的结果" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "程序模式:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "音乐" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "图片" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "标准" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "扫描类型:" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "更多选项" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "请选择要扫描的文件夹,然后点击 \"扫描\"。" #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "载入结果" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "扫描" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "未保存的结果" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "您还没有保存扫描结果,确定要退出吗?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "请选择一个文件夹以加入到扫描列表中" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "选择一个结果文件以载入" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "所有文件 (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "dupeGuru 结果 (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "开始新的扫描" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "您还没有保存扫描结果,确定要继续吗?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "名称" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "状态" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "不包含" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "正常" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "移除已选择的文件" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "清除" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "关闭" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "详细信息" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "扫描标签:" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "音轨" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "作者" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "专辑" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "歌曲名" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "音乐类型" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "年" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "根据词语长度不同赋予不同权重" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "匹配近似词语" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "允许混合文件类型" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "过滤时使用正则表达式" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "删除或移动时一并移除空文件夹" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "忽略硬链接到相同文件的重复文件" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "调试模式 (需要重新启动)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "匹配不同尺寸的图像" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "过滤强度:" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "较多结果" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "较少结果" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "字体大小:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "语言:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "复制并移动:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "直接至目标文件夹" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "重建相对路径" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "重建绝对路径" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "自定义命令 (参数: %d 指重复文件, %r 指源文件):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "dupeGuru需要重新启动以使语言修改生效。" #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "重新排列重复文件" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "在右侧的框内添加规则然后点击确定,以将最符合规则的文件置于组内源文件的位置。阅读帮助文件获取更多信息。" #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "有问题!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "在处理部分(或全部)文件时出现问题。产生问题的原因如下。这些文件没有从结果中移除。" #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "显示已选择的文件" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "操作" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "仅显示重复文件" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "显示 Delta 值" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "将已标记的文件移至回收站..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "将已标记的文件移动到..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "将已标记的文件复制到..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "从结果中移除已标记的文件" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "重新排列结果..." #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "从结果中移除所选文件" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "将所选文件添加到忽略列表中" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "将所选文件作为源文件" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "使用默认程序打开所选文件" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "打开所选文件所在的文件夹" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "重命名所选文件" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "全部标记" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "全部取消标记" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "反转文件标记" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "标记所选文件" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "导出为 HTML" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "导出为 CSV" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "保存结果..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "调用自定义命令" #: qt/result_window.py:102 msgid "Mark" msgstr "标记" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "显示列" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "重置为默认值" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} 个结果" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "仅 Dupes" #: qt/result_window.py:194 msgid "Delta Values" msgstr "Delta 值" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "选择一个文件来保存您的结果" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "忽略文件当其小于" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ 结果" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "操作" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "添加新文件夹..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "高级" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "自动检查更新" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "基本" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "全部前置" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "检查更新..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "关闭窗口" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "复制" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "自定义命令 (参数: %d 指重复文件, %r 指源文件):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "剪切" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Delta" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "所选文件的详细信息" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "详细信息面板" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "目录" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "dupeGuru 设置" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "dupeGuru 结果" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "dupeGuru 网站" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "编辑" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "导出结果到 CSV" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "导出结果到 XHTML" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "较少结果" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "过滤器" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "过滤强度:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "过滤结果..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "文件夹选择窗口" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "字体大小:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "隐藏 dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "隐藏其他" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "忽略文件当其小于:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "从文件载入..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "最小化" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "模式" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "更多结果" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "确定" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "粘贴" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "设置..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "快速查找" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "退出 dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "重置为默认值" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "重置为默认值" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "显示" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "在 Finder 中显示所选项" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "全选" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "将已标记的文件移至回收站..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "服务" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "全部显示" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "开始重复内容扫描" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "名称 '%@' 已存在。" #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "窗口" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "缩放" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "排除过滤器" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "扫描结果" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "载入目录..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "保存目录..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "选择一个目录文件以载入" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "dupeGuru 结果 (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "选择一个文件来保存您的目录" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "dupeGuru 目录 (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "添加" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "还原至默认值" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "测试字符串" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "在此输入一个python正则表达式..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "在此输入一个系统路径或者文件名..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "这些(大小写敏感)的python正则表达式会在扫描过程中筛选文件。
    如果目录的名称和某一个正则表达式匹配的话,它们的默认状态将为被设为排除状态。
    每一个被采集的文件都会被进行两种不同的测试来决定它是否会被排除掉:
  • 1. 没有路径分隔符的正则表达式只会和文件名作比较。
  • \n" "
  • 2. 有路径分隔符的正则表达式,会和文件的完整路径作比较。

  • \n" "如:假如您想要仅从“我的图片”目录排除掉 .PNG 文件的话:
    .*My\\sPictures\\\\.*\\.png

    您可以使用测试字符串功能来测试正则表达式,只需要在其中粘贴一个假的路径:
    C:\\\\User\\My Pictures\\test.png

    \n" "匹配的正则表达式会被高亮。
    假如至少有一个高亮的话,在扫描中这个路径将会被忽略。

    以“.”开头的目录和文件默认就会被忽略。

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "编译错误:" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "放大" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "缩小" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "正常尺寸" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "最佳结果" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "图片缓存模式:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "在图片浏览器的工具栏里,覆盖默认图标设置" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "使用程序自带的图标来替代系统默认图标。" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "在图片浏览器里显示滚动条" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "当图片尺寸大于显示窗口时,显示滚动条来移动图片。" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "在默认位置显示Tab Bar(需要重启)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "把Tab Bar放在主菜单下面而不是旁边\n" "在MacOS上,Tab Bar会填充满整个窗口的宽度。" #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "源文件使用粗体" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "源文件前景色:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "源文件背景色:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "Delta 前景色:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "显示标题栏,并使其可被停靠" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "标题栏隐藏时,使用修饰键来移动浮动窗口。" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "仅当窗口被停靠时,标题栏可被隐藏" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "竖直标题栏" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "把标题栏从顶部横向改为左侧竖直" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "显示 Tab Bar" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "这些(大小写敏感)的python正则表达式会在扫描过程中对结果进行过滤。
    除非目录名称和正则表达式匹配,它们的默认状态会被设成从目录标签排除。
    每一个文件都会经过两种测试,以确定它是否会被完全忽略:
  • 1. 没有路径分隔符的正则表达式,仅用于和文件名进行比较。
  • \n" "
  • 2. 至少有一个路径分隔符的正则表达式,会被用于和文件的完整路径进行比较。

  • \n" "例如:如果你想要仅在“图片”目录中排除所有.PNG文件:
    .*My\\sPictures\\\\.*\\.png

    你可以使用“测试字符串”按钮来测试你的正则表达式,只需要将虚拟的路径输入测试框即可:
    C:\\\\User\\My Pictures\\test.png

    \n" "匹配的正则表达式会被高亮。
    假如有至少一个高亮,测试文件的文件名或者路径就会在扫描中被忽略。

    以“.”开头的目录或文件默认会被忽略。

    " #: qt\app.py:256 msgid "Results" msgstr "结果" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "通用介面" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "结果表" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "详细信息窗口" #: qt\preferences_dialog.py:285 msgid "General" msgstr "一般" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "展示" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "只哈希部分如果文件大于" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "使用操作系统原生对话窗口" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "使用操作系统原生对话窗口选择文件、文件夹。部分系统的原生对话窗口功能可能有限制。" #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" #: qt\app.py:299 msgid "Cache cleared." msgstr "" #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "" #: qt\about_box.py:31 msgid "About {}" msgstr "关于 {}" #: qt\about_box.py:47 msgid "Version {}" msgstr "版本 {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "" #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "本项目基于GPLv3开源协议发布" #: qt\about_box.py:68 msgid "No update available." msgstr "" #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "错误报告" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "发生错误,是否要报告错误?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "错误报告应该以Github issue的形式进行提交。您可以把错误信息复制粘贴到新的issue中\n" "\n" "在提交新issue前,请搜索已经存在的issue,以确保没有其他人已经报告了相同的错误。同时请确保使用仓库中的最新版进行测试,因为您所遇到的bug可能已经被最新版修复。\n" "\n" "如果您能详细描述一下错误发生时的具体情况,将会更好的帮助我们解决问题,谢谢!\n" "\n" "虽然本程序在此错误后依然会继续运行,但是可能处于不稳定的状态,因此推荐重启本程序。" #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "前往Github" #: qt\preferences.py:24 msgid "Czech" msgstr "捷克语" #: qt\preferences.py:25 msgid "German" msgstr "德语" #: qt\preferences.py:26 msgid "Greek" msgstr "希腊语" #: qt\preferences.py:27 msgid "English" msgstr "英语" #: qt\preferences.py:28 msgid "Spanish" msgstr "西班牙语" #: qt\preferences.py:29 msgid "French" msgstr "法语" #: qt\preferences.py:30 msgid "Armenian" msgstr "亚美尼亚语" #: qt\preferences.py:31 msgid "Italian" msgstr "意大利语" #: qt\preferences.py:32 msgid "Japanese" msgstr "日语" #: qt\preferences.py:33 msgid "Korean" msgstr "韩语" #: qt\preferences.py:34 msgid "Malay" msgstr "马来语" #: qt\preferences.py:35 msgid "Dutch" msgstr "荷兰语" #: qt\preferences.py:36 msgid "Polish" msgstr "波兰语" #: qt\preferences.py:37 msgid "Brazilian" msgstr "巴西葡萄牙语" #: qt\preferences.py:38 msgid "Russian" msgstr "俄语" #: qt\preferences.py:39 msgid "Turkish" msgstr "土耳其" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "乌克兰语" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "越南语" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "中文(简体)" #: qt\recent.py:54 msgid "Clear List" msgstr "清空列表" #: qt\search_edit.py:78 msgid "Search..." msgstr "搜索..." dupeguru-4.3.1/locale/zh_TW/000077500000000000000000000000001426171743600156645ustar00rootroot00000000000000dupeguru-4.3.1/locale/zh_TW/LC_MESSAGES/000077500000000000000000000000001426171743600174515ustar00rootroot00000000000000dupeguru-4.3.1/locale/zh_TW/LC_MESSAGES/columns.po000066400000000000000000000053111426171743600214710ustar00rootroot00000000000000# Translators: # Chris Ocelot, 2022 # Andrew Senetar , 2022 # msgid "" msgstr "" "Last-Translator: Andrew Senetar , 2022\n" "Language-Team: Chinese (Taiwan) (https://www.transifex.com/voltaicideas/teams/116153/zh_TW/)\n" "Language: zh_TW\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\problem_table.py:18 msgid "File Path" msgstr "文件路径" #: core\gui\problem_table.py:19 msgid "Error Message" msgstr "错误信息" #: core\me\prioritize.py:23 msgid "Duration" msgstr "持续时间" #: core\me\prioritize.py:30 core\me\result_table.py:23 msgid "Bitrate" msgstr "比特率" #: core\me\prioritize.py:37 msgid "Samplerate" msgstr "采样率" #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:94 #: core\se\result_table.py:19 msgid "Filename" msgstr "文件名" #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\se\result_table.py:20 msgid "Folder" msgstr "文件夹" #: core\me\result_table.py:21 msgid "Size (MB)" msgstr "大小 (MB)" #: core\me\result_table.py:22 msgid "Time" msgstr "时间" #: core\me\result_table.py:24 msgid "Sample Rate" msgstr "采样率" #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\se\result_table.py:22 msgid "Kind" msgstr "类型" #: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\prioritize.py:165 core\se\result_table.py:23 msgid "Modification" msgstr "编辑日期" #: core\me\result_table.py:27 msgid "Title" msgstr "歌曲名" #: core\me\result_table.py:28 msgid "Artist" msgstr "作者" #: core\me\result_table.py:29 msgid "Album" msgstr "专辑" #: core\me\result_table.py:30 msgid "Genre" msgstr "音乐类型" #: core\me\result_table.py:31 msgid "Year" msgstr "年" #: core\me\result_table.py:32 msgid "Track Number" msgstr "音轨号" #: core\me\result_table.py:33 msgid "Comment" msgstr "注释" #: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\se\result_table.py:24 msgid "Match %" msgstr "匹配度 %" #: core\me\result_table.py:35 core\se\result_table.py:25 msgid "Words Used" msgstr "使用过的词语" #: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\se\result_table.py:26 msgid "Dupe Count" msgstr "重复文件数" #: core\pe\prioritize.py:23 core\pe\result_table.py:23 msgid "Dimensions" msgstr "规格" #: core\pe\result_table.py:21 core\se\result_table.py:21 msgid "Size (KB)" msgstr "大小 (KB)" #: core\pe\result_table.py:24 msgid "EXIF Timestamp" msgstr "EXIF 时间戳" #: core\prioritize.py:158 msgid "Size" msgstr "大小" dupeguru-4.3.1/locale/zh_TW/LC_MESSAGES/core.po000066400000000000000000000152351426171743600207470ustar00rootroot00000000000000# Translators: # Fuan , 2022 # YaNing Lu, 2022 # Andrew Senetar , 2022 # Chris Ocelot, 2022 # msgid "" msgstr "" "Last-Translator: Chris Ocelot, 2022\n" "Language-Team: Chinese (Taiwan) (https://www.transifex.com/voltaicideas/teams/116153/zh_TW/)\n" "Language: zh_TW\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: core\app.py:44 msgid "There are no marked duplicates. Nothing has been done." msgstr "没有已标记的重复项。无需任何操作。" #: core\app.py:45 msgid "There are no selected duplicates. Nothing has been done." msgstr "没有已选定的重复项。无需任何操作。" #: core\app.py:46 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "您即将一次性打开多个文件。取决于这些文件的默认打开方式,此项操作可能导致非常混乱的状况。是否继续?" #: core\app.py:73 msgid "Scanning for duplicates" msgstr "正在扫描重复内容" #: core\app.py:74 msgid "Loading" msgstr "载入中" #: core\app.py:75 msgid "Moving" msgstr "移动中" #: core\app.py:76 msgid "Copying" msgstr "复制中" #: core\app.py:77 msgid "Sending to Trash" msgstr "正在移至回收站" #: core\app.py:291 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "前一项操作还在执行,无法启动新操作。请等待几秒钟后再重试一次。" #: core\app.py:302 msgid "No duplicates found." msgstr "没有找到重复文件。" #: core\app.py:317 msgid "All marked files were copied successfully." msgstr "所有已标记的文件已复制成功。" #: core\app.py:319 msgid "All marked files were moved successfully." msgstr "所有已标记的文件已移动成功。" #: core\app.py:321 msgid "All marked files were deleted successfully." msgstr "已复制所有标记文件" #: core\app.py:323 msgid "All marked files were successfully sent to Trash." msgstr "所有已标记的文件已成功移至回收站。" #: core\app.py:328 msgid "Could not load file: {}" msgstr "无法加载文件: {}" #: core\app.py:384 msgid "'{}' already is in the list." msgstr "'{}' 已在列表中。" #: core\app.py:386 msgid "'{}' does not exist." msgstr "'{}' 不存在。" #: core\app.py:394 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "目前已选的 %d 个匹配项将在后续的扫描中被忽略。是否继续?" #: core\app.py:471 msgid "Select a directory to copy marked files to" msgstr "请选择要将标记文件复制到的目录" #: core\app.py:473 msgid "Select a directory to move marked files to" msgstr "请选择要将标记文件移动到的目录" #: core\app.py:512 msgid "Select a destination for your exported CSV" msgstr "选择您导出 CSV 的目标文件夹" #: core\app.py:518 core\app.py:773 core\app.py:783 msgid "Couldn't write to file: {}" msgstr "不能写入文件: {}" #: core\app.py:541 msgid "You have no custom command set up. Set it up in your preferences." msgstr "您没有设定自定义命令。请在设置中进行设定。" #: core\app.py:697 core\app.py:709 msgid "You are about to remove %d files from results. Continue?" msgstr "您将从结果中移除 %d 个文件。是否继续?" #: core\app.py:745 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{}个重复的组已被重新排列。" #: core\app.py:792 msgid "The selected directories contain no scannable file." msgstr "所选目录中不包含可供扫描的文件。" #: core\app.py:808 msgid "Collecting files to scan" msgstr "收集文件以供扫描" #: core\app.py:858 msgid "%s (%d discarded)" msgstr "%s (%d 项已丢弃)" #: core\directories.py:190 msgid "Collected {} files to scan" msgstr "收集要扫描的{}文件" #: core\directories.py:206 msgid "Collected {} folders to scan" msgstr "收集要扫描的{}文件夹" #: core\engine.py:27 msgid "%d matches found from %d groups" msgstr "从1%d组中找到1%d个匹配" #: core\gui\deletion_options.py:71 msgid "You are sending {} file(s) to the Trash." msgstr "您正在移动 {} 个文件至回收站。" #: core\gui\exclude_list_table.py:14 msgid "Regular Expressions" msgstr "正则表达式" #: core\gui\ignore_list_dialog.py:25 msgid "Do you really want to remove all %d items from the ignore list?" msgstr "确定要从忽略列表中移除所有 %d 项吗?" #: core\me\scanner.py:20 core\se\scanner.py:16 msgid "Filename" msgstr "文件名" #: core\me\scanner.py:21 msgid "Filename - Fields" msgstr "分组比较文件名(如作者-歌曲名)" #: core\me\scanner.py:22 msgid "Filename - Fields (No Order)" msgstr "分组比较文件名(不固定顺序(如作者-歌曲名或者歌曲名-作者))" #: core\me\scanner.py:23 msgid "Tags" msgstr "标签" #: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17 msgid "Contents" msgstr "内容" #: core\pe\matchblock.py:72 msgid "Analyzed %d/%d pictures" msgstr "已分析 %d/%d 图像" #: core\pe\matchblock.py:177 msgid "Performed %d/%d chunk matches" msgstr "已执行 %d/%d 个区块匹配" #: core\pe\matchblock.py:185 msgid "Preparing for matching" msgstr "准备进行匹配" #: core\pe\matchblock.py:234 msgid "Verified %d/%d matches" msgstr "已验证 %d/%d 匹配项" #: core\pe\matchexif.py:19 msgid "Read EXIF of %d/%d pictures" msgstr "已读取 %d/%d 张图片的 EXIF" #: core\pe\scanner.py:22 msgid "EXIF Timestamp" msgstr "EXIF 时间戳" #: core\prioritize.py:70 msgid "None" msgstr "无" #: core\prioritize.py:102 msgid "Ends with number" msgstr "以数字结尾" #: core\prioritize.py:103 msgid "Doesn't end with number" msgstr "不以数字结尾" #: core\prioritize.py:104 msgid "Longest" msgstr "最长" #: core\prioritize.py:105 msgid "Shortest" msgstr "最短" #: core\prioritize.py:142 msgid "Highest" msgstr "最高" #: core\prioritize.py:142 msgid "Lowest" msgstr "最低" #: core\prioritize.py:171 msgid "Newest" msgstr "最新" #: core\prioritize.py:171 msgid "Oldest" msgstr "最旧" #: core\results.py:134 msgid "%d / %d (%s / %s) duplicates marked." msgstr "已标记 %d / %d (%s / %s) 个重复项。" #: core\results.py:141 msgid " filter: %s" msgstr " 过滤: %s" #: core\scanner.py:90 msgid "Read size of %d/%d files" msgstr "已读取 %d/%d 文件大小" #: core\scanner.py:116 msgid "Read metadata of %d/%d files" msgstr "已读取 %d/%d 文件元数据" #: core\scanner.py:154 msgid "Almost done! Fiddling with results..." msgstr "即将完成!整理结果中..." #: core\se\scanner.py:18 msgid "Folders" msgstr "文件夹" dupeguru-4.3.1/locale/zh_TW/LC_MESSAGES/ui.po000066400000000000000000001017551426171743600204370ustar00rootroot00000000000000# Translators: # Fuan , 2022 # 太子 VC , 2022 # Andrew Senetar , 2022 # Chris Ocelot, 2022 # msgid "" msgstr "" "Last-Translator: Chris Ocelot, 2022\n" "Language-Team: Chinese (Taiwan) (https://www.transifex.com/voltaicideas/teams/116153/zh_TW/)\n" "Language: zh_TW\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" "Plural-Forms: nplurals=1; plural=0;\n" #: qt/app.py:81 msgid "Quit" msgstr "退出" #: qt/app.py:82 qt/preferences_dialog.py:116 #: cocoa/en.lproj/Localizable.strings:0 msgid "Options" msgstr "选项" #: qt/app.py:83 qt/ignore_list_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "忽略列表" #: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "清空图片缓存" #: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "dupeGuru 帮助" #: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "关于 dupeGuru" #: qt/app.py:87 msgid "Open Debug Log" msgstr "打开调试记录" #: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "确定要移除所有缓存分析图片?" #: qt/app.py:184 msgid "Picture cache cleared." msgstr "图片缓存已清空。" #: qt/app.py:251 msgid "{} file (*.{})" msgstr "{} 文件 (*.{})" #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "删除选项" #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "链接已删除的文件" #: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "在删除重复文件后,以源文件的链接来替代已删除的文件。" #: qt/deletion_options.py:44 msgid "Hardlink" msgstr "硬链接" #: qt/deletion_options.py:44 msgid "Symlink" msgstr "符号链接" #: qt/deletion_options.py:48 msgid " (unsupported)" msgstr " (不支持)" #: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "直接删除文件" #: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "直接将文件删除,而不是将其移至回收站。此选项通常作为普通删除方法不能正常使用时替代方案。" #: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "继续" #: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "取消" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Attribute" msgstr "属性" #: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0 msgid "Selected" msgstr "已选择" #: qt/details_table.py:16 qt/directories_model.py:24 #: cocoa/en.lproj/Localizable.strings:0 msgid "Reference" msgstr "源文件" #: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results..." msgstr "载入结果..." #: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0 msgid "Results Window" msgstr "结果窗口" #: qt/directories_dialog.py:66 msgid "Add Folder..." msgstr "添加文件夹..." #: qt/directories_dialog.py:74 qt/result_window.py:100 #: cocoa/en.lproj/Localizable.strings:0 msgid "File" msgstr "文件" #: qt/directories_dialog.py:76 qt/result_window.py:108 msgid "View" msgstr "视图" #: qt/directories_dialog.py:78 qt/result_window.py:110 #: cocoa/en.lproj/Localizable.strings:0 msgid "Help" msgstr "帮助" #: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0 msgid "Load Recent Results" msgstr "载入最近的结果" #: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0 msgid "Application Mode:" msgstr "程序模式:" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Music" msgstr "音乐" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Picture" msgstr "图片" #: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0 msgid "Standard" msgstr "标准" #: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0 msgid "Scan Type:" msgstr "扫描类型:" #: qt/directories_dialog.py:135 msgid "More Options" msgstr "更多选项" #: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." msgstr "请选择要扫描的文件夹,然后点击 \"扫描\"。" #: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0 msgid "Load Results" msgstr "载入结果" #: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0 msgid "Scan" msgstr "扫描" #: qt/directories_dialog.py:230 msgid "Unsaved results" msgstr "未保存的结果" #: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" msgstr "您还没有保存扫描结果,确定要退出吗?" #: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" msgstr "请选择一个文件夹以加入到扫描列表中" #: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0 msgid "Select a results file to load" msgstr "选择一个结果文件以载入" #: qt/directories_dialog.py:267 msgid "All Files (*.*)" msgstr "所有文件 (*.*)" #: qt/directories_dialog.py:267 qt/result_window.py:311 msgid "dupeGuru Results (*.dupeguru)" msgstr "dupeGuru 结果 (*.dupeguru)" #: qt/directories_dialog.py:278 msgid "Start a new scan" msgstr "开始新的扫描" #: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to continue?" msgstr "您还没有保存扫描结果,确定要继续吗?" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "Name" msgstr "名称" #: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0 msgid "State" msgstr "状态" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Excluded" msgstr "不包含" #: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0 msgid "Normal" msgstr "正常" #: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected" msgstr "移除已选择的文件" #: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Clear" msgstr "清除" #: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61 #: cocoa/en.lproj/Localizable.strings:0 msgid "Close" msgstr "关闭" #: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24 #: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18 #: cocoa/en.lproj/Localizable.strings:0 msgid "Details" msgstr "详细信息" #: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0 msgid "Tags to scan:" msgstr "扫描标签:" #: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Track" msgstr "音轨" #: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Artist" msgstr "作者" #: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0 msgid "Album" msgstr "专辑" #: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0 msgid "Title" msgstr "歌曲名" #: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0 msgid "Genre" msgstr "音乐类型" #: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0 msgid "Year" msgstr "年" #: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30 #: cocoa/en.lproj/Localizable.strings:0 msgid "Word weighting" msgstr "根据词语长度不同赋予不同权重" #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: cocoa/en.lproj/Localizable.strings:0 msgid "Match similar words" msgstr "匹配近似词语" #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 msgid "Can mix file kind" msgstr "允许混合文件类型" #: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23 #: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0 msgid "Use regular expressions when filtering" msgstr "过滤时使用正则表达式" #: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25 #: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0 msgid "Remove empty folders on delete or move" msgstr "删除或移动时一并移除空文件夹" #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Ignore duplicates hardlinking to the same file" msgstr "忽略硬链接到相同文件的重复文件" #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Debug mode (restart required)" msgstr "调试模式 (需要重新启动)" #: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0 msgid "Match pictures of different dimensions" msgstr "匹配不同尺寸的图像" #: qt/preferences_dialog.py:43 msgid "Filter Hardness:" msgstr "过滤强度:" #: qt/preferences_dialog.py:69 msgid "More Results" msgstr "较多结果" #: qt/preferences_dialog.py:74 msgid "Fewer Results" msgstr "较少结果" #: qt/preferences_dialog.py:81 msgid "Font size:" msgstr "字体大小:" #: qt/preferences_dialog.py:85 msgid "Language:" msgstr "语言:" #: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0 msgid "Copy and Move:" msgstr "复制并移动:" #: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0 msgid "Right in destination" msgstr "直接至目标文件夹" #: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate relative path" msgstr "重建相对路径" #: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0 msgid "Recreate absolute path" msgstr "重建绝对路径" #: qt/preferences_dialog.py:99 msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgstr "自定义命令 (参数: %d 指重复文件, %r 指源文件):" #: qt/preferences_dialog.py:174 msgid "dupeGuru has to restart for language changes to take effect." msgstr "dupeGuru需要重新启动以使语言修改生效。" #: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" msgstr "重新排列重复文件" #: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0 msgid "" "Add criteria to the right box and click OK to send the dupes that correspond" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "在右侧的框内添加规则然后点击确定,以将最符合规则的文件置于组内源文件的位置。阅读帮助文件获取更多信息。" #: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0 msgid "Problems!" msgstr "有问题!" #: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0 msgid "" "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." msgstr "在处理部分(或全部)文件时出现问题。产生问题的原因如下。这些文件没有从结果中移除。" #: qt/problem_dialog.py:56 msgid "Reveal Selected" msgstr "显示已选择的文件" #: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167 #: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0 msgid "Actions" msgstr "操作" #: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0 msgid "Show Dupes Only" msgstr "仅显示重复文件" #: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0 msgid "Show Delta Values" msgstr "显示 Delta 值" #: qt/result_window.py:60 msgid "Send Marked to Recycle Bin..." msgstr "将已标记的文件移至回收站..." #: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0 msgid "Move Marked to..." msgstr "将已标记的文件移动到..." #: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0 msgid "Copy Marked to..." msgstr "将已标记的文件复制到..." #: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Marked from Results" msgstr "从结果中移除已标记的文件" #: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." msgstr "重新排列结果..." #: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" msgstr "从结果中移除所选文件" #: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0 msgid "Add Selected to Ignore List" msgstr "将所选文件添加到忽略列表中" #: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0 msgid "Make Selected into Reference" msgstr "将所选文件作为源文件" #: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0 msgid "Open Selected with Default Application" msgstr "使用默认程序打开所选文件" #: qt/result_window.py:80 msgid "Open Containing Folder of Selected" msgstr "打开所选文件所在的文件夹" #: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0 msgid "Rename Selected" msgstr "重命名所选文件" #: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0 msgid "Mark All" msgstr "全部标记" #: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0 msgid "Mark None" msgstr "全部取消标记" #: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0 msgid "Invert Marking" msgstr "反转文件标记" #: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0 msgid "Mark Selected" msgstr "标记所选文件" #: qt/result_window.py:87 msgid "Export To HTML" msgstr "导出为 HTML" #: qt/result_window.py:88 msgid "Export To CSV" msgstr "导出为 CSV" #: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0 msgid "Save Results..." msgstr "保存结果..." #: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0 msgid "Invoke Custom Command" msgstr "调用自定义命令" #: qt/result_window.py:102 msgid "Mark" msgstr "标记" #: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0 msgid "Columns" msgstr "显示列" #: qt/result_window.py:163 msgid "Reset to Defaults" msgstr "重置为默认值" #: qt/result_window.py:185 msgid "{} Results" msgstr "{} 个结果" #: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0 msgid "Dupes Only" msgstr "仅 Dupes" #: qt/result_window.py:194 msgid "Delta Values" msgstr "Delta 值" #: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0 msgid "Select a file to save your results to" msgstr "选择一个文件来保存您的结果" #: qt/se/preferences_dialog.py:41 msgid "Ignore files smaller than" msgstr "忽略文件当其小于" #: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0 msgid "KB" msgstr "KB" #: cocoa/en.lproj/Localizable.strings:0 msgid "%@ Results" msgstr "%@ 结果" #: cocoa/en.lproj/Localizable.strings:0 msgid "Action" msgstr "操作" #: cocoa/en.lproj/Localizable.strings:0 msgid "Add New Folder..." msgstr "添加新文件夹..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Advanced" msgstr "高级" #: cocoa/en.lproj/Localizable.strings:0 msgid "Automatically check for updates" msgstr "自动检查更新" #: cocoa/en.lproj/Localizable.strings:0 msgid "Basic" msgstr "基本" #: cocoa/en.lproj/Localizable.strings:0 msgid "Bring All to Front" msgstr "全部前置" #: cocoa/en.lproj/Localizable.strings:0 msgid "Check for update..." msgstr "检查更新..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Close Window" msgstr "关闭窗口" #: cocoa/en.lproj/Localizable.strings:0 msgid "Copy" msgstr "复制" #: cocoa/en.lproj/Localizable.strings:0 msgid "Custom command (arguments: %d for dupe, %r for ref):" msgstr "自定义命令 (参数: %d 指重复文件, %r 指源文件):" #: cocoa/en.lproj/Localizable.strings:0 msgid "Cut" msgstr "剪切" #: cocoa/en.lproj/Localizable.strings:0 msgid "Delta" msgstr "Delta" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details of Selected File" msgstr "所选文件的详细信息" #: cocoa/en.lproj/Localizable.strings:0 msgid "Details Panel" msgstr "详细信息面板" #: cocoa/en.lproj/Localizable.strings:0 msgid "Directories" msgstr "目录" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru" msgstr "dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Preferences" msgstr "dupeGuru 设置" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Results" msgstr "dupeGuru 结果" #: cocoa/en.lproj/Localizable.strings:0 msgid "dupeGuru Website" msgstr "dupeGuru 网站" #: cocoa/en.lproj/Localizable.strings:0 msgid "Edit" msgstr "编辑" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to CSV" msgstr "导出结果到 CSV" #: cocoa/en.lproj/Localizable.strings:0 msgid "Export Results to XHTML" msgstr "导出结果到 XHTML" #: cocoa/en.lproj/Localizable.strings:0 msgid "Fewer results" msgstr "较少结果" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter" msgstr "过滤器" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter hardness:" msgstr "过滤强度:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Filter Results..." msgstr "过滤结果..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Folder Selection Window" msgstr "文件夹选择窗口" #: cocoa/en.lproj/Localizable.strings:0 msgid "Font Size:" msgstr "字体大小:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide dupeGuru" msgstr "隐藏 dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Hide Others" msgstr "隐藏其他" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ignore files smaller than:" msgstr "忽略文件当其小于:" #: cocoa/en.lproj/Localizable.strings:0 msgid "Load from file..." msgstr "从文件载入..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Minimize" msgstr "最小化" #: cocoa/en.lproj/Localizable.strings:0 msgid "Mode" msgstr "模式" #: cocoa/en.lproj/Localizable.strings:0 msgid "More results" msgstr "更多结果" #: cocoa/en.lproj/Localizable.strings:0 msgid "Ok" msgstr "确定" #: cocoa/en.lproj/Localizable.strings:0 msgid "Paste" msgstr "粘贴" #: cocoa/en.lproj/Localizable.strings:0 msgid "Preferences..." msgstr "设置..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Quick Look" msgstr "快速查找" #: cocoa/en.lproj/Localizable.strings:0 msgid "Quit dupeGuru" msgstr "退出 dupeGuru" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset to Default" msgstr "重置为默认值" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reset To Defaults" msgstr "重置为默认值" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal" msgstr "显示" #: cocoa/en.lproj/Localizable.strings:0 msgid "Reveal Selected in Finder" msgstr "在 Finder 中显示所选项" #: cocoa/en.lproj/Localizable.strings:0 msgid "Select All" msgstr "全选" #: cocoa/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "将已标记的文件移至回收站..." #: cocoa/en.lproj/Localizable.strings:0 msgid "Services" msgstr "服务" #: cocoa/en.lproj/Localizable.strings:0 msgid "Show All" msgstr "全部显示" #: cocoa/en.lproj/Localizable.strings:0 msgid "Start Duplicate Scan" msgstr "开始重复内容扫描" #: cocoa/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." msgstr "名称 '%@' 已存在。" #: cocoa/en.lproj/Localizable.strings:0 msgid "Window" msgstr "窗口" #: cocoa/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "缩放" #: qt\app.py:158 msgid "Exclusion Filters" msgstr "排除过滤器" #: qt\directories_dialog.py:91 msgid "Scan Results" msgstr "扫描结果" #: qt\directories_dialog.py:95 msgid "Load Directories..." msgstr "载入目录..." #: qt\directories_dialog.py:96 msgid "Save Directories..." msgstr "保存目录..." #: qt\directories_dialog.py:337 msgid "Select a directories file to load" msgstr "选择一个目录文件以载入" #: qt\directories_dialog.py:338 msgid "dupeGuru Results (*.dupegurudirs)" msgstr "dupeGuru 结果 (*.dupegurudirs)" #: qt\directories_dialog.py:347 msgid "Select a file to save your directories to" msgstr "选择一个文件来保存您的目录" #: qt\directories_dialog.py:348 msgid "dupeGuru Directories (*.dupegurudirs)" msgstr "dupeGuru 目录 (*.dupegurudirs)" #: qt\exclude_list_dialog.py:44 msgid "Add" msgstr "添加" #: qt\exclude_list_dialog.py:46 msgid "Restore defaults" msgstr "还原至默认值" #: qt\exclude_list_dialog.py:47 msgid "Test string" msgstr "测试字符串" #: qt\exclude_list_dialog.py:83 msgid "Type a python regular expression here..." msgstr "在此输入一个python正则表达式..." #: qt\exclude_list_dialog.py:85 msgid "Type a file system path or filename here..." msgstr "在此输入一个系统路径或者文件名..." #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happen to match one of the regular expressions.
    For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with no path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the test string feature by pasting a fake path in it:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "这些(大小写敏感)的python正则表达式会在扫描过程中筛选文件。
    如果目录的名称和某一个正则表达式匹配的话,它们的默认状态将为被设为排除状态。
    每一个被采集的文件都会被进行两种不同的测试来决定它是否会被排除掉:
  • 1. 没有路径分隔符的正则表达式只会和文件名作比较。
  • \n" "
  • 2. 有路径分隔符的正则表达式,会和文件的完整路径作比较。

  • \n" "如:假如您想要仅从“我的图片”目录排除掉 .PNG 文件的话:
    .*My\\sPictures\\\\.*\\.png

    您可以使用测试字符串功能来测试正则表达式,只需要在其中粘贴一个假的路径:
    C:\\\\User\\My Pictures\\test.png

    \n" "匹配的正则表达式会被高亮。
    假如至少有一个高亮的话,在扫描中这个路径将会被忽略。

    以“.”开头的目录和文件默认就会被忽略。

    " #: qt\exclude_list_table.py:36 msgid "Compilation error: " msgstr "编译错误:" #: qt\pe\image_viewer.py:56 msgid "Increase zoom" msgstr "放大" #: qt\pe\image_viewer.py:66 msgid "Decrease zoom" msgstr "缩小" #: qt\pe\image_viewer.py:71 msgid "Ctrl+/" msgstr "Ctrl+/" #: qt\pe\image_viewer.py:76 msgid "Normal size" msgstr "正常尺寸" #: qt\pe\image_viewer.py:81 msgid "Ctrl+*" msgstr "Ctrl+*" #: qt\pe\image_viewer.py:86 msgid "Best fit" msgstr "最佳结果" #: qt\pe\preferences_dialog.py:49 msgid "Picture cache mode:" msgstr "图片缓存模式:" #: qt\pe\preferences_dialog.py:56 msgid "Override theme icons in viewer toolbar" msgstr "在图片浏览器的工具栏里,覆盖默认图标设置" #: qt\pe\preferences_dialog.py:58 msgid "" "Use our own internal icons instead of those provided by the theme engine" msgstr "使用程序自带的图标来替代系统默认图标。" #: qt\pe\preferences_dialog.py:66 msgid "Show scrollbars in image viewers" msgstr "在图片浏览器里显示滚动条" #: qt\pe\preferences_dialog.py:68 msgid "" "When the image displayed doesn't fit the viewport, show scrollbars to span " "the view around" msgstr "当图片尺寸大于显示窗口时,显示滚动条来移动图片。" #: qt\preferences_dialog.py:156 msgid "Use default position for tab bar (requires restart)" msgstr "在默认位置显示Tab Bar(需要重启)" #: qt\preferences_dialog.py:158 msgid "" "Place the tab bar below the main menu instead of next to it\n" "On MacOS, the tab bar will fill up the window's width instead." msgstr "" "把Tab Bar放在主菜单下面而不是旁边\n" "在MacOS上,Tab Bar会填充满整个窗口的宽度。" #: qt\preferences_dialog.py:172 msgid "Use bold font for references" msgstr "源文件使用粗体" #: qt\preferences_dialog.py:176 msgid "Reference foreground color:" msgstr "源文件前景色:" #: qt\preferences_dialog.py:179 msgid "Reference background color:" msgstr "源文件背景色:" #: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216 msgid "Delta foreground color:" msgstr "Delta 前景色:" #: qt\preferences_dialog.py:195 msgid "Show the title bar and can be docked" msgstr "显示标题栏,并使其可被停靠" #: qt\preferences_dialog.py:197 msgid "" "While the title bar is hidden, use the modifier key to drag the floating " "window around" msgstr "标题栏隐藏时,使用修饰键来移动浮动窗口。" #: qt\preferences_dialog.py:199 msgid "The title bar can only be disabled while the window is docked" msgstr "仅当窗口被停靠时,标题栏可被隐藏" #: qt\preferences_dialog.py:202 msgid "Vertical title bar" msgstr "竖直标题栏" #: qt\preferences_dialog.py:204 msgid "" "Change the title bar from horizontal on top, to vertical on the left side" msgstr "把标题栏从顶部横向改为左侧竖直" #: qt\tabbed_window.py:44 msgid "Show tab bar" msgstr "显示 Tab Bar" #: qt\exclude_list_dialog.py:152 msgid "" "These (case sensitive) python regular expressions will filter out files during scans.
    Directores will also have their default state set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.
    For each file collected, two tests are performed to determine whether or not to completely ignore it:
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • \n" "
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • \n" "Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:
    .*My\\sPictures\\\\.*\\.png

    You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:
    C:\\\\User\\My Pictures\\test.png

    \n" "Matching regular expressions will be highlighted.
    If there is at least one highlight, the path or filename tested will be ignored during scans.

    Directories and files starting with a period '.' are filtered out by default.

    " msgstr "" "这些(大小写敏感)的python正则表达式会在扫描过程中对结果进行过滤。
    除非目录名称和正则表达式匹配,它们的默认状态会被设成从目录标签排除。
    每一个文件都会经过两种测试,以确定它是否会被完全忽略:
  • 1. 没有路径分隔符的正则表达式,仅用于和文件名进行比较。
  • \n" "
  • 2. 至少有一个路径分隔符的正则表达式,会被用于和文件的完整路径进行比较。

  • \n" "例如:如果你想要仅在“图片”目录中排除所有.PNG文件:
    .*My\\sPictures\\\\.*\\.png

    你可以使用“测试字符串”按钮来测试你的正则表达式,只需要将虚拟的路径输入测试框即可:
    C:\\\\User\\My Pictures\\test.png

    \n" "匹配的正则表达式会被高亮。
    假如有至少一个高亮,测试文件的文件名或者路径就会在扫描中被忽略。

    以“.”开头的目录或文件默认会被忽略。

    " #: qt\app.py:256 msgid "Results" msgstr "结果" #: qt\preferences_dialog.py:150 msgid "General Interface" msgstr "通用介面" #: qt\preferences_dialog.py:176 msgid "Result Table" msgstr "结果表" #: qt\preferences_dialog.py:205 msgid "Details Window" msgstr "详细信息窗口" #: qt\preferences_dialog.py:285 msgid "General" msgstr "一般" #: qt\preferences_dialog.py:286 msgid "Display" msgstr "展示" #: qt\se\preferences_dialog.py:70 msgid "Partially hash files bigger than" msgstr "只哈希部分如果文件大于" #: qt\se\preferences_dialog.py:80 msgid "MB" msgstr "" #: qt\preferences_dialog.py:163 msgid "Use native OS dialogs" msgstr "使用操作系统原生对话窗口" #: qt\preferences_dialog.py:166 msgid "" "For actions such as file/folder selection use the OS native dialogs.\n" "Some native dialogs have limited functionality." msgstr "使用操作系统原生对话窗口选择文件、文件夹。部分系统的原生对话窗口功能可能有限制。" #: qt\se\preferences_dialog.py:68 msgid "Ignore files larger than" msgstr "" #: qt\app.py:135 qt\app.py:293 msgid "Clear Cache" msgstr "" #: qt\app.py:294 msgid "" "Do you really want to clear the cache? This will remove all cached file " "hashes and picture analysis." msgstr "" #: qt\app.py:299 msgid "Cache cleared." msgstr "" #: qt\preferences_dialog.py:173 msgid "Use dark style" msgstr "" #: qt\preferences_dialog.py:241 msgid "Profile scan operation" msgstr "" #: qt\preferences_dialog.py:242 msgid "Profile the scan operation and save logs for optimization." msgstr "" #: qt\preferences_dialog.py:246 msgid "Logs located in: {}" msgstr "" #: qt\preferences_dialog.py:291 msgid "Debug" msgstr "" #: qt\about_box.py:31 msgid "About {}" msgstr "关于 {}" #: qt\about_box.py:47 msgid "Version {}" msgstr "版本 {}" #: qt\about_box.py:49 qt\about_box.py:75 msgid "Checking for updates..." msgstr "" #: qt\about_box.py:54 msgid "Licensed under GPLv3" msgstr "本项目基于GPLv3开源协议发布" #: qt\about_box.py:68 msgid "No update available." msgstr "" #: qt\about_box.py:71 msgid "New version {} available, download here." msgstr "" #: qt\error_report_dialog.py:50 msgid "Error Report" msgstr "错误报告" #: qt\error_report_dialog.py:54 msgid "Something went wrong. How about reporting the error?" msgstr "发生错误,是否要报告错误?" #: qt\error_report_dialog.py:60 msgid "" "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n" "\n" "Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n" "\n" "What usually really helps is if you add a description of how you got the error. Thanks!\n" "\n" "Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." msgstr "" "错误报告应该以Github issue的形式进行提交。您可以把错误信息复制粘贴到新的issue中\n" "\n" "在提交新issue前,请搜索已经存在的issue,以确保没有其他人已经报告了相同的错误。同时请确保使用仓库中的最新版进行测试,因为您所遇到的bug可能已经被最新版修复。\n" "\n" "如果您能详细描述一下错误发生时的具体情况,将会更好的帮助我们解决问题,谢谢!\n" "\n" "虽然本程序在此错误后依然会继续运行,但是可能处于不稳定的状态,因此推荐重启本程序。" #: qt\error_report_dialog.py:80 msgid "Go to Github" msgstr "前往Github" #: qt\preferences.py:24 msgid "Czech" msgstr "捷克语" #: qt\preferences.py:25 msgid "German" msgstr "德语" #: qt\preferences.py:26 msgid "Greek" msgstr "希腊语" #: qt\preferences.py:27 msgid "English" msgstr "英语" #: qt\preferences.py:28 msgid "Spanish" msgstr "西班牙语" #: qt\preferences.py:29 msgid "French" msgstr "法语" #: qt\preferences.py:30 msgid "Armenian" msgstr "亚美尼亚语" #: qt\preferences.py:31 msgid "Italian" msgstr "意大利语" #: qt\preferences.py:32 msgid "Japanese" msgstr "日语" #: qt\preferences.py:33 msgid "Korean" msgstr "韩语" #: qt\preferences.py:34 msgid "Malay" msgstr "马来语" #: qt\preferences.py:35 msgid "Dutch" msgstr "荷兰语" #: qt\preferences.py:36 msgid "Polish" msgstr "波兰语" #: qt\preferences.py:37 msgid "Brazilian" msgstr "巴西葡萄牙语" #: qt\preferences.py:38 msgid "Russian" msgstr "俄语" #: qt\preferences.py:39 msgid "Turkish" msgstr "土耳其" #: qt\preferences.py:40 msgid "Ukrainian" msgstr "乌克兰语" #: qt\preferences.py:41 msgid "Vietnamese" msgstr "越南语" #: qt\preferences.py:42 msgid "Chinese (Simplified)" msgstr "中文(简体)" #: qt\recent.py:54 msgid "Clear List" msgstr "清空列表" #: qt\search_edit.py:78 msgid "Search..." msgstr "搜索..." dupeguru-4.3.1/macos.md000066400000000000000000000043161426171743600150220ustar00rootroot00000000000000## How to build dupeGuru for macos These instructions are for the Qt version of the UI on macOS. *Note: The Cocoa UI of dupeGuru is hosted in a separate repo: https://github.com/arsenetar/dupeguru-cocoa and is no longer "supported".* ### Prerequisites - [Python 3.7+][python] - [Xcode 12.3][xcode] or just Xcode command line tools (older versions can be used if not interested in arm macs) - [Homebrew][homebrew] - [qt5](https://www.qt.io/) #### Prerequisite setup 1. Install Xcode if desired 2. Install [Homebrew][homebrew], if not on the path after install (arm based Macs) create `~/.zshrc` with `export PATH="/opt/homebrew/bin:$PATH"`. Will need to reload terminal or source the file to take effect. 3. Install qt5 with `brew`. If you are using a version of macos without system python 3.7+ then you will also need to install that via brew or with pyenv. $ brew install qt5 NOTE: Using `brew` to install qt5 is to allow pyqt5 to build without a native wheel available. If you are using an intel based mac you can probably skip this step. 4. May need to launch a new terminal to have everything working. ### With build.py OSX comes with a version of python 3 by default in newer versions of OSX. To produce universal builds either the 3.8 version shipped in macos or 3.9.1 or newer needs to be used. If needing to build pyqt5 from source then the first line below is needed, else it may be omitted. (Path shown is for an arm mac.) $ export PATH="/opt/homebrew/opt/qt/bin:$PATH" $ cd $ python3 -m venv ./env $ source ./env/bin/activate $ pip install -r requirements.txt $ python build.py $ python run.py ### Generate OSX Packages The extra requirements need to be installed to run packaging: `pip install -r requirements-extra.txt`. Run the following in the respective virtual environment. $ python package.py This will produce a dupeGuru.app in the dist folder. ### Running tests The complete test suite can be run with tox just like on linux. NOTE: The extra requirements need to be installed to run unit tests: `pip install -r requirements-extra.txt`. [python]: http://www.python.org/ [homebrew]: https://brew.sh/ [xcode]: https://developer.apple.com/xcode/ dupeguru-4.3.1/package.py000066400000000000000000000204631426171743600153440ustar00rootroot00000000000000# Copyright 2017 Virgil Dupras # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import sys import os import os.path as op import compileall import shutil import json from argparse import ArgumentParser import platform import distro import re from hscommon.build import ( print_and_do, copy_packages, build_debian_changelog, get_module_version, filereplace, copy, setup_package_argparser, copy_all, ) ENTRY_SCRIPT = "run.py" LOCALE_DIR = "build/locale" HELP_DIR = "build/help" def parse_args(): parser = ArgumentParser() setup_package_argparser(parser) return parser.parse_args() def check_loc_doc(): if not op.exists(LOCALE_DIR): print('Locale files are missing. Have you run "build.py --loc"?') # include help files if they are built otherwise exit as they should be included? if not op.exists(HELP_DIR): print('Help files are missing. Have you run "build.py --doc"?') return op.exists(LOCALE_DIR) and op.exists(HELP_DIR) def copy_files_to_package(destpath, packages, with_so): # when with_so is true, we keep .so files in the package, and otherwise, we don't. We need this # flag because when building debian src pkg, we *don't* want .so files (they're compiled later) # and when we're packaging under Arch, we're packaging a binary package, so we want them. if op.exists(destpath): shutil.rmtree(destpath) os.makedirs(destpath) shutil.copy(ENTRY_SCRIPT, op.join(destpath, ENTRY_SCRIPT)) extra_ignores = ["*.so"] if not with_so else None copy_packages(packages, destpath, extra_ignores=extra_ignores) # include locale files if they are built otherwise exit as it will break # the localization if not check_loc_doc(): print("Exiting...") return shutil.copytree(op.join("build", "help"), op.join(destpath, "help")) shutil.copytree(op.join("build", "locale"), op.join(destpath, "locale")) compileall.compile_dir(destpath) def package_debian_distribution(distribution): app_version = get_module_version("core") version = "{}~{}".format(app_version, distribution) destpath = op.join("build", "dupeguru-{}".format(version)) srcpath = op.join(destpath, "src") packages = ["hscommon", "core", "qt", "send2trash"] copy_files_to_package(srcpath, packages, with_so=False) os.mkdir(op.join(destpath, "modules")) copy_all(op.join("core", "pe", "modules", "*.*"), op.join(destpath, "modules")) copy( op.join("qt", "pe", "modules", "block.c"), op.join(destpath, "modules", "block_qt.c"), ) copy( op.join("pkg", "debian", "build_pe_modules.py"), op.join(destpath, "build_pe_modules.py"), ) debdest = op.join(destpath, "debian") debskel = op.join("pkg", "debian") os.makedirs(debdest) debopts = json.load(open(op.join(debskel, "dupeguru.json"))) for fn in ["compat", "copyright", "dirs", "rules", "source"]: copy(op.join(debskel, fn), op.join(debdest, fn)) filereplace(op.join(debskel, "control"), op.join(debdest, "control"), **debopts) filereplace(op.join(debskel, "Makefile"), op.join(destpath, "Makefile"), **debopts) filereplace(op.join(debskel, "dupeguru.desktop"), op.join(debdest, "dupeguru.desktop"), **debopts) changelogpath = op.join("help", "changelog") changelog_dest = op.join(debdest, "changelog") project_name = debopts["pkgname"] from_version = "2.9.2" build_debian_changelog( changelogpath, changelog_dest, project_name, from_version=from_version, distribution=distribution, ) shutil.copy(op.join("images", "dgse_logo_128.png"), srcpath) os.chdir(destpath) cmd = "dpkg-buildpackage -F -us -uc" os.system(cmd) os.chdir("../..") def package_debian(): print("Packaging for Debian/Ubuntu") for distribution in ["unstable"]: package_debian_distribution(distribution) def package_arch(): # For now, package_arch() will only copy the source files into build/. It copies less packages # than package_debian because there are more python packages available in Arch (so we don't # need to include them). print("Packaging for Arch") srcpath = op.join("build", "dupeguru-arch") packages = ["hscommon", "core", "qt", "send2trash"] copy_files_to_package(srcpath, packages, with_so=True) shutil.copy(op.join("images", "dgse_logo_128.png"), srcpath) debopts = json.load(open(op.join("pkg", "arch", "dupeguru.json"))) filereplace(op.join("pkg", "arch", "dupeguru.desktop"), op.join(srcpath, "dupeguru.desktop"), **debopts) def package_source_txz(): print("Creating git archive") app_version = get_module_version("core") name = "dupeguru-src-{}.tar".format(app_version) base_path = os.getcwd() build_path = op.join(base_path, "build") dest = op.join(build_path, name) print_and_do("git archive -o {} HEAD".format(dest)) print_and_do("xz {}".format(dest)) def package_windows(): app_version = get_module_version("core") arch = platform.architecture()[0] # Information to pass to pyinstaller and NSIS match = re.search("[0-9]+.[0-9]+.[0-9]+", app_version) version_array = match.group(0).split(".") match = re.search("[0-9]+", arch) bits = match.group(0) if bits == "64": arch = "x64" else: arch = "x86" # include locale files if they are built otherwise exit as it will break # the localization if not check_loc_doc(): print("Exiting...") return # create version information file from template try: version_template = open("win_version_info.temp", "r") version_info = version_template.read() version_template.close() version_info_file = open("win_version_info.txt", "w") version_info_file.write(version_info.format(version_array[0], version_array[1], version_array[2], bits)) version_info_file.close() except Exception: print("Error creating version info file, exiting...") return # run pyinstaller from here: import PyInstaller.__main__ # UCRT dlls are included if the system has the windows kit installed PyInstaller.__main__.run( [ "--name=dupeguru-win{0}".format(bits), "--windowed", "--noconfirm", "--icon=images/dgse_logo.ico", "--add-data={0};locale".format(LOCALE_DIR), "--add-data={0};help".format(HELP_DIR), "--version-file=win_version_info.txt", "--paths=C:\\Program Files (x86)\\Windows Kits\\10\\Redist\\ucrt\\DLLs\\{0}".format(arch), ENTRY_SCRIPT, ] ) # remove version info file os.remove("win_version_info.txt") # Call NSIS (TODO update to not use hardcoded path) cmd = ( '"C:\\Program Files (x86)\\NSIS\\Bin\\makensis.exe" ' "/DVERSIONMAJOR={0} /DVERSIONMINOR={1} /DVERSIONPATCH={2} /DBITS={3} setup.nsi" ) print_and_do(cmd.format(version_array[0], version_array[1], version_array[2], bits)) def package_macos(): # include locale files if they are built otherwise exit as it will break # the localization if not check_loc_doc(): print("Exiting") return # run pyinstaller from here: import PyInstaller.__main__ PyInstaller.__main__.run( [ "--name=dupeguru", "--windowed", "--noconfirm", "--icon=images/dupeguru.icns", "--osx-bundle-identifier=com.hardcoded-software.dupeguru", "--add-data={0}:locale".format(LOCALE_DIR), "--add-data={0}:help".format(HELP_DIR), "{0}".format(ENTRY_SCRIPT), ] ) def main(): args = parse_args() if args.src_pkg: print("Creating source package for dupeGuru") package_source_txz() return print("Packaging dupeGuru with UI qt") if sys.platform == "win32": package_windows() elif sys.platform == "darwin": package_macos() else: if not args.arch_pkg: distname = distro.id() else: distname = "arch" if distname == "arch": package_arch() else: package_debian() if __name__ == "__main__": main() dupeguru-4.3.1/pkg/000077500000000000000000000000001426171743600141535ustar00rootroot00000000000000dupeguru-4.3.1/pkg/arch/000077500000000000000000000000001426171743600150705ustar00rootroot00000000000000dupeguru-4.3.1/pkg/arch/dupeguru.desktop000066400000000000000000000002221426171743600203170ustar00rootroot00000000000000[Desktop Entry] Name={longname} Comment=Find duplicate files. Exec={execname} Icon={iconpath} Terminal=false Type=Application Categories=Utility; dupeguru-4.3.1/pkg/arch/dupeguru.json000066400000000000000000000001661426171743600176260ustar00rootroot00000000000000{ "pkgname": "dupeguru", "longname": "dupeGuru", "execname": "dupeguru", "arch": "any", "iconpath": "dupeguru" } dupeguru-4.3.1/pkg/debian/000077500000000000000000000000001426171743600153755ustar00rootroot00000000000000dupeguru-4.3.1/pkg/debian/Makefile000066400000000000000000000010311426171743600170300ustar00rootroot00000000000000#!/usr/bin/make -f all: dh_prep dh_installdirs touch build_pe_modules.py python3 build_pe_modules.py chmod +x src/run.py cp -R src/ "$(CURDIR)/debian/{pkgname}/usr/share/{execname}" cp "$(CURDIR)/debian/{execname}.desktop" "$(CURDIR)/debian/{pkgname}/usr/share/applications" mkdir -p "$(CURDIR)/debian/{pkgname}/usr/share/pixmaps" ln -s "/usr/share/{execname}/dgse_logo_128.png" "$(CURDIR)/debian/{pkgname}/usr/share/pixmaps/{execname}.png" ln -s "/usr/share/{execname}/run.py" "$(CURDIR)/debian/{pkgname}/usr/bin/{execname}" dupeguru-4.3.1/pkg/debian/build_pe_modules.py000066400000000000000000000012321426171743600212600ustar00rootroot00000000000000import sys import os import os.path as op import shutil import importlib from setuptools import setup, Extension sys.path.insert(1, op.abspath("src")) from hscommon.build import move_all exts = [ Extension("_block", [op.join("modules", "block.c"), op.join("modules", "common.c")]), Extension("_cache", [op.join("modules", "cache.c"), op.join("modules", "common.c")]), Extension("_block_qt", [op.join("modules", "block_qt.c")]), ] setup( script_args=["build_ext", "--inplace"], ext_modules=exts, ) move_all("_block_qt*", op.join("src", "qt", "pe")) move_all("_cache*", op.join("src", "core/pe")) move_all("_block*", op.join("src", "core/pe")) dupeguru-4.3.1/pkg/debian/changelog000066400000000000000000000342061426171743600172540ustar00rootroot00000000000000dupeguru (4.0.4-1) unstable; urgency=low * Update qt/platform.py to support other Unix style OSes (#444) * Fix font size scaling issue in properties dialog [qt] (#504) * Updates to support Python 3.7 * Fix issue with result window appearing partially off-screen [qt] (#521) * Fix translation error for Simplified Chinese * Updates to language files for German (#479) * Fix error with multiple close calls to the progress window [qt] (#460, #449) * Add Travis CI Builds * Un-recurse methods get_files() and get_state() to improve stability (#421) * Updates to language files for Italian (#445, #446, #447, #448) * Fix issue with cache_shelve (#402, #439) * Updated Windows packaging and builds (#438, #456, #461, #491, #474, #490, #565) * Handle OS termination signals (#425) * Make documentation installation optional * Move cocoa UI to dupeguru-cocoa [cocoa] -- Virgil Dupras Mon, 13 May 2019 00:00:00 +0000 dupeguru (4.0.3-1) unstable; urgency=low * Add new picture cache backend: shelve * Make shelve picture cache backend the active one on MacOS to fix #394 more elegantly. [cocoa] * Remove Sparkle (auto-updates) due to technical limitations. [cocoa] -- Virgil Dupras Thu, 24 Nov 2016 00:00:00 +0000 dupeguru (4.0.2-1) unstable; urgency=low * Fix systematic crash in Picture Mode under MacOS Sierra. (#394) * No change for Linux. Just keeping version in sync. -- Virgil Dupras Sun, 09 Oct 2016 00:00:00 +0000 dupeguru (4.0.1-1) unstable; urgency=low * Add Greek localization, by Gabriel Koutilellis. (#382) * Fix localization base path. [qt] (#378) * Fix broken load results dialog. [qt] * Fix crash on load results. [cocoa] (#380) * Save preferences more predictably. [qt] (#379) * Fix picture mode's fuzzy block scanner threshold. (#387) -- Virgil Dupras Wed, 24 Aug 2016 00:00:00 +0000 dupeguru (4.0.0-1) unstable; urgency=low * Merge Standard, Music and Picture editions in the same application! * Improve documentation. (#294) * Add Polish, Korean, Spanish and Dutch localizations. * qt: Fix wrong use_regexp option propagation to core. (#295) * qt: Fix progress window mistakenly showing up on startup. (#357) * Bump Python requirement to v3.4. * Bump OS X requirement to 10.8 * Drop Windows support, maybe temporarily. `Details `_ * cocoa: Drop iPhoto, Aperture and iTunes support. Was unmaintained and obsolete. * Drop "Audio Contents" scan type. Was confusing and seldom useful. * Change license to GPLv3 -- Virgil Dupras Fri, 01 Jul 2016 00:00:00 +0000 dupeguru (3.9.1-1) unstable; urgency=low * Fixed ``AttributeError: 'ComboboxModel' object has no attribute 'reset'``. [Linux, Windows] (#254) * Fixed ``PermissionError`` on saving results. (#266) * Fixed a build problem introduced by Sphinx 1.2.3. * Updated German localisation, by Frank Weber. -- Virgil Dupras Fri, 17 Oct 2014 00:00:00 +0000 dupeguru (3.9.0-1) unstable; urgency=low * This is mostly a dependencies upgrade. * Upgraded to Python 3.3. * Upgraded to Qt 5. * Minimum Windows version is now Windows 7 64bit. * Minimum Ubuntu version is now 14.04. * Minimum OS X version is now 10.7 (Lion). * ... But with a couple of little improvements. * Improved documentation. * Overwrite subfolders' state when setting states in folder dialog (#248) * The error report dialog now brings the user to Github issues. -- Virgil Dupras Sat, 19 Apr 2014 00:00:00 +0000 dupeguru (3.8.0-1) unstable; urgency=low * Disable symlink/hardlink deletion option when not relevant. (#247) * Make Cmd+A select all folders in the Folder Selection dialog. [Mac] (#228) * Make non-numeric delta comparison case insensitive. (#239) * Fix surrogate-related UnicodeEncodeError on CSV export. (#210) * Fixed crash on Dupe Count sorting with Delta + Dupes Only. (#238) * Improved documentation. * Important internal refactorings. * Dropped Ubuntu 12.04 and 12.10 support. * Removed the fairware dialog ([More Info](http://www.hardcoded.net/articles/phasing-out-fairware)). -- Virgil Dupras Sat, 07 Dec 2013 00:00:00 +0000 dupeguru (3.7.1-1) unstable; urgency=low * Fixed folder scan type, which was broken in v3.7.0. -- Virgil Dupras Mon, 19 Aug 2013 00:00:00 +0000 dupeguru (3.7.0-1) unstable; urgency=low * Improved delta values to support non-numerical values. (#213) * Improved the Re-Prioritize dialog's UI. (#224) * Added hardlink/symlink support on Windows Vista+. (#220) * Dropped 32bit support on Mac OS X. * Added Vietnamese localization by Phan Anh. -- Virgil Dupras Sat, 17 Aug 2013 00:00:00 +0000 dupeguru (3.6.1-1) unstable; urgency=low * Improved "Make Selection Reference" to make it clearer. (#222) * Improved "Open Selected" to allow opening more than one file at once. (#142) * Fixed a few typos here and there. (#216 #225) * Tweaked the fairware dialog ([More Info](http://www.hardcoded.net/articles/phasing-out-fairware)). * Added Arch Linux packaging * Added a 64-bit build for Windows. * Improved Russian localization by Kyrill Detinov. * Improved Brazilian localization by Victor Figueiredo. -- Virgil Dupras Sun, 28 Apr 2013 00:00:00 +0000 dupeguru (3.6.0-1) unstable; urgency=low * Added "Export to CSV". (#189) * Added "Replace with symlinks" to complement "Replace with hardlinks". [Mac, Linux] (#194) * dupeGuru now tells how many duplicates were affected after each re-prioritization operation. (#204) * Added Longest/Shortest filename criteria in the re-prioritize dialog. (#198) * Fixed result table cells which mistakenly became writable in v3.5.0. [Mac] (#203) * Fixed "Rename Selected" which was broken since v3.5.0. [Mac] (#202) * Fixed a bug where "Reset to Defaults" in the Columns menu wouldn't refresh menu items' marked state. * Added Brazilian localization by Victor Figueiredo. -- Virgil Dupras Wed, 08 Aug 2012 00:00:00 +0000 dupeguru (3.5.0-1) unstable; urgency=low * Added a Deletion Options panel. * Greatly improved memory usage for big scans. * Added a keybinding for the filter field. (#182) [Mac] * Upgraded minimum requirements for Ubuntu to 12.04. -- Virgil Dupras Fri, 01 Jun 2012 00:00:00 +0000 dupeguru (3.4.1-1) unstable; urgency=low * Fixed the "Folders" scan type. [Mac] * Fixed localization issues. [Windows, Linux] -- Virgil Dupras Sat, 14 Apr 2012 00:00:00 +0000 dupeguru (3.4.0-1) unstable; urgency=low * Improved results window UI. [Windows, Linux] * Added a dialog to edit the Ignore List. * Added the ability to sort results by "marked" status. * Fixed "Open with default application". (#190) * Fixed a bug where there would be a false reporting of discarded matches. (#195) * Fixed various localization glitches. * Fixed hard crashes on crash reporting. (#196) * Fixed bug where the details panel would show up at inconvenient places in the screen. [Windows, Linux] -- Virgil Dupras Thu, 29 Mar 2012 00:00:00 +0000 dupeguru (3.3.3-1) unstable; urgency=low * Fixed crash on adding some folders. [Mac OS X] * Added Ukrainian localization by Yuri Petrashko. -- Virgil Dupras Wed, 01 Feb 2012 00:00:00 +0000 dupeguru (3.3.2-1) unstable; urgency=low * Fixed random hard crashes (yeah, again). [Mac OS X] * Fixed crash on Export to HTML. [Windows, Linux] * Added Armenian localization by Hrant Ohanyan. * Added Russian localization by Igor Pavlov. -- Virgil Dupras Mon, 16 Jan 2012 00:00:00 +0000 dupeguru (3.3.1-1) unstable; urgency=low * Fixed a couple of nasty crashes. -- Virgil Dupras Fri, 02 Dec 2011 00:00:00 +0000 dupeguru (3.3.0-1) unstable; urgency=low * Added multiple-selection in folder selection dialog for a more efficient folder removal. (#179) * Fixed a crash in the prioritize dialog. (#178) * Fixed a bug where mass marking with a filter would mark more than filtered duplicates. (#181) * Fixed random hard crashes. [Mac OS X] (#183 #184) * Added Czech localization by Ale Nehyba. * Added Italian localization by Paolo Rossi. -- Virgil Dupras Wed, 30 Nov 2011 00:00:00 +0000 dupeguru (3.2.1-1) unstable; urgency=low * Fixed a couple of broken action bindings from v3.2.0. -- Virgil Dupras Sun, 02 Oct 2011 00:00:00 +0000 dupeguru (3.2.0-1) unstable; urgency=low * Added duplicate re-prioritization dialog. (#138) * Added font size preference for duplicate table. (#82) * Added Quicklook support. [Mac OS X] (#21) * Improved behavior of Mark Selected. (#139) * Improved filename sorting. (#169) * Added Chinese (Simplified) localization by Eric Dee. * Tweaked the fairware system. * Upgraded minimum requirements to OS X 10.6 and Ubuntu 11.04. -- Virgil Dupras Tue, 27 Sep 2011 00:00:00 +0000 dupeguru (3.1.2-1) unstable; urgency=low * Fixed a bug preventing the Folders scan from working. (#172) -- Virgil Dupras Thu, 25 Aug 2011 00:00:00 +0000 dupeguru (3.1.1-1) unstable; urgency=low * Added German localization by Gregor Ttzner. * Improved OS X Lion compatibility. [Mac OS X] * Made the file collection phase cancellable. (#168) * Fixed glitch in folder window upon selecting a folder state. [Windows, Linux] (#165) * Fixed a text coloring glitch in the results. (#156) * Fixed glitch in the sorting feature of the Folder column. (#161) * Make sure that saved results have the ".dupeguru" extension. [Linux] (#157) -- Virgil Dupras Wed, 24 Aug 2011 00:00:00 +0000 dupeguru (3.1.0-1) unstable; urgency=low * Added the "Folders" scan type. (#89) * Fixed a couple of crashes. (#140 #149) -- Virgil Dupras Sat, 16 Apr 2011 00:00:00 +0000 dupeguru (3.0.2-1) unstable; urgency=low * Fixed crash after removing marked dupes. (#140) * Fixed crash on error handling. [Windows] (#144) * Fixed crash on copy/move. [Windows] (#148) * Fixed crash when launching dupeGuru from a very long folder name. [Mac OS X] (#119) * Fixed a refresh bug in directory panel. (#153) * Improved reliability of the "Send to Trash" operation. [Linux] * Tweaked Fairware reminders. -- Virgil Dupras Wed, 16 Mar 2011 00:00:00 +0000 dupeguru (3.0.1-1) unstable; urgency=low * Restored the context menu which had been broken in 3.0.0. [Mac OS X] (#133) * Fixed a bug where an "unsaved results" warning would be issued on quit even with empty results. (#134) * Removed focus from the cancel button in the progress dialog to avoid accidental cancellations. [Mac OS X] (#135) * Folders added through drag and drop are added to the recent folders list. (#136) * Added a debugging mode. (#132) * Fixed french localization glitches. -- Virgil Dupras Thu, 27 Jan 2011 00:00:00 +0000 dupeguru (3.0.0-1) unstable; urgency=low * Re-designed the UI. (#129) * Internationalized dupeGuru and localized it to french. (#32) * Changed the format of the help file. (#130) -- Virgil Dupras Mon, 24 Jan 2011 00:00:00 +0000 dupeguru (2.12.3-1) unstable; urgency=low * Fixed bug causing results to be corrupted after a scan cancellation. (#120) * Fixed crash when fetching Fairware unpaid hours. (#121) * Fixed crash when replacing files with hardlinks. (#122) -- Virgil Dupras Sat, 01 Jan 2011 00:00:00 +0000 dupeguru (2.12.2-1) unstable; urgency=low * Fixed delta column colors which were broken since 2.12.0. * Fixed column sorting crash. (#108) * Fixed occasional crash during scan. (#106) -- Virgil Dupras Tue, 05 Oct 2010 00:00:00 +0000 dupeguru (2.12.1-1) unstable; urgency=low * Re-licensed dupeGuru to BSD and made it [Fairware](http://open.hardcoded.net/about/). -- Virgil Dupras Thu, 30 Sep 2010 00:00:00 +0000 dupeguru (2.12.0-1) unstable; urgency=low * Improved UI with a little revamp. * Added the possibility to place hardlinks to references after having deleted duplicates. [Mac OS X, Linux] (#91) * Added an option to ignore duplicates hardlinking to the same file. [Mac OS X, Linux] (#92) * Added multiple selection in the "Add Directory" dialog. [Mac OS X] (#105) * Fixed a bug preventing drag & drop from working in the Directories panel. [Windows, Linux] -- Virgil Dupras Sun, 26 Sep 2010 00:00:00 +0000 dupeguru (2.11.1-1) unstable; urgency=low * Fixed HTML exporting which was broken in 2.11.0. -- Virgil Dupras Thu, 26 Aug 2010 00:00:00 +0000 dupeguru (2.11.0-1) unstable; urgency=low * Added the ability to save results (and reload them) at arbitrary locations. * Improved the way reference files in dupe groups are chosen. (#15) * Remember size/position of all windows between launches. (#102) * Fixed a bug sometimes preventing dupeGuru from reloading previous results. * Fixed a bug sometimes causing the progress dialog to be stuck there. [Mac OS X] (#103) * Removed the Creation Date column, which wasn't displaying the correct value anyway. (#101) -- Virgil Dupras Wed, 18 Aug 2010 00:00:00 +0000 dupeguru (2.10.1-1) unstable; urgency=low * Fixed a couple of crashes. (#95, #97, #100) -- Virgil Dupras Thu, 15 Jul 2010 00:00:00 +0000 dupeguru (2.10.0-1) unstable; urgency=low * Improved error messages when files can't be sent to trash, moved or copied. * Added a custom command invocation action. (#12) * Filters are now applied on whole paths. (#4) -- Virgil Dupras Tue, 13 Apr 2010 00:00:00 +0000 dupeguru (2.9.2-1) unstable; urgency=low * dupeGuru is now 64-bit on Mac OS X! * Fixed a crash upon quitting when support folder is not present. (#83) * Fixed a crash during sorting. (#85) * Fixed selection glitches, especially while renaming. (#93) -- Virgil Dupras Wed, 10 Feb 2010 00:00:00 +0000 dupeguru-4.3.1/pkg/debian/compat000066400000000000000000000000021426171743600165730ustar00rootroot000000000000009 dupeguru-4.3.1/pkg/debian/control000066400000000000000000000020001426171743600167700ustar00rootroot00000000000000Source: {pkgname} Section: devel Priority: extra Maintainer: Virgil Dupras , Eugene San (eugenesan) Build-Depends: debhelper (>= 7), python3-dev, python3-setuptools Standards-Version: 3.9.2 Homepage: https://dupeguru.voltaicideas.net Vcs-Browser: https://github.com/arsenetar/dupeguru Vcs-Git: https://github.com/arsenetar/dupeguru.git Package: {pkgname} Architecture: {arch} Depends: ${shlibs:Depends}, python3 (>=3.7), python3-pyqt5, python3-mutagen, python3-semantic-version Provides: dupeguru-se, dupeguru-me, dupeguru-pe Replaces: dupeguru-se, dupeguru-me, dupeguru-pe Conflicts: dupeguru-se, dupeguru-me, dupeguru-pe Description: {longname} dupeGuru is a cross-platform (Linux and OS X) GUI tool to find duplicate files in a system. It's written mostly in Python 3 and has the peculiarity of using multiple GUI toolkits, all using the same core Python code. On OS X, the UI layer is written in Objective-C and uses Cocoa. On Linux, it's written in Python and uses Qt5. dupeguru-4.3.1/pkg/debian/copyright000066400000000000000000000033351426171743600173340ustar00rootroot00000000000000Copyright 2014 Hardcoded Software Inc. (http://www.hardcoded.net) All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. dupeguru-4.3.1/pkg/debian/dirs000066400000000000000000000000511426171743600162550ustar00rootroot00000000000000usr/bin usr/share usr/share/applications dupeguru-4.3.1/pkg/debian/dupeguru.desktop000066400000000000000000000002221426171743600206240ustar00rootroot00000000000000[Desktop Entry] Name={longname} Comment=Find duplicate files. Exec={execname} Icon={iconpath} Terminal=false Type=Application Categories=Utility; dupeguru-4.3.1/pkg/debian/dupeguru.json000066400000000000000000000001661426171743600201330ustar00rootroot00000000000000{ "pkgname": "dupeguru", "longname": "dupeGuru", "execname": "dupeguru", "arch": "any", "iconpath": "dupeguru" } dupeguru-4.3.1/pkg/debian/rules000077500000000000000000000000351426171743600164530ustar00rootroot00000000000000#!/usr/bin/make -f %: dh $@ dupeguru-4.3.1/pkg/debian/source/000077500000000000000000000000001426171743600166755ustar00rootroot00000000000000dupeguru-4.3.1/pkg/debian/source/format000066400000000000000000000000141426171743600201030ustar00rootroot000000000000003.0 (native)dupeguru-4.3.1/pkg/debian/source/options000066400000000000000000000000231426171743600203060ustar00rootroot00000000000000compression = "xz" dupeguru-4.3.1/pkg/dupeguru.desktop000066400000000000000000000002141426171743600174030ustar00rootroot00000000000000[Desktop Entry] Name=dupeGuru Comment=Find duplicate files. Exec=dupeguru Icon=dupeguru Terminal=false Type=Application Categories=Utility; dupeguru-4.3.1/pyproject.toml000066400000000000000000000003061426171743600163050ustar00rootroot00000000000000[build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.black] line-length = 120 [tool.isort] # make it compatible with black profile = "black" skip_gitignore = true dupeguru-4.3.1/qt/000077500000000000000000000000001426171743600140165ustar00rootroot00000000000000dupeguru-4.3.1/qt/__init__.py000066400000000000000000000000001426171743600161150ustar00rootroot00000000000000dupeguru-4.3.1/qt/about_box.py000066400000000000000000000065531426171743600163630ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2009-05-09 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import Qt, QCoreApplication, QTimer from PyQt5.QtGui import QPixmap, QFont from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QSizePolicy, QHBoxLayout, QVBoxLayout, QLabel from core.util import check_for_update from qt.util import move_to_screen_center from hscommon.trans import trget tr = trget("ui") class AboutBox(QDialog): def __init__(self, parent, app, **kwargs): flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.MSWindowsFixedSizeDialogHint super().__init__(parent, flags, **kwargs) self.app = app self._setupUi() self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) def _setupUi(self): self.setWindowTitle(tr("About {}").format(QCoreApplication.instance().applicationName())) size_policy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.setSizePolicy(size_policy) main_layout = QHBoxLayout(self) logo_label = QLabel() logo_label.setPixmap(QPixmap(":/%s_big" % self.app.LOGO_NAME)) main_layout.addWidget(logo_label) detail_layout = QVBoxLayout() name_label = QLabel() font = QFont() font.setWeight(75) font.setBold(True) name_label.setFont(font) name_label.setText(QCoreApplication.instance().applicationName()) detail_layout.addWidget(name_label) version_label = QLabel() version_label.setText(tr("Version {}").format(QCoreApplication.instance().applicationVersion())) detail_layout.addWidget(version_label) self.update_label = QLabel(tr("Checking for updates...")) self.update_label.setTextInteractionFlags(Qt.TextBrowserInteraction) self.update_label.setOpenExternalLinks(True) detail_layout.addWidget(self.update_label) license_label = QLabel() license_label.setText(tr("Licensed under GPLv3")) detail_layout.addWidget(license_label) spacer_label = QLabel() spacer_label.setFont(font) detail_layout.addWidget(spacer_label) self.button_box = QDialogButtonBox() self.button_box.setOrientation(Qt.Horizontal) self.button_box.setStandardButtons(QDialogButtonBox.Ok) detail_layout.addWidget(self.button_box) main_layout.addLayout(detail_layout) def _check_for_update(self): update = check_for_update(QCoreApplication.instance().applicationVersion(), include_prerelease=False) if update is None: self.update_label.setText(tr("No update available.")) else: self.update_label.setText( tr('New version {} available, download here.').format(update["version"], update["url"]) ) def showEvent(self, event): self.update_label.setText(tr("Checking for updates...")) # have to do this here as the frameGeometry is not correct until shown move_to_screen_center(self) super().showEvent(event) QTimer.singleShot(0, self._check_for_update) dupeguru-4.3.1/qt/app.py000066400000000000000000000457641426171743600151700ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import sys import os.path as op from PyQt5.QtCore import QTimer, QObject, QUrl, pyqtSignal, Qt from PyQt5.QtGui import QColor, QDesktopServices, QPalette from PyQt5.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox, QStyleFactory, QToolTip from hscommon.trans import trget from hscommon import desktop, plat from qt.about_box import AboutBox from qt.recent import Recent from qt.util import create_actions from qt.progress_window import ProgressWindow from core.app import AppMode, DupeGuru as DupeGuruModel import core.pe.photo from qt import platform from qt.preferences import Preferences from qt.result_window import ResultWindow from qt.directories_dialog import DirectoriesDialog from qt.problem_dialog import ProblemDialog from qt.ignore_list_dialog import IgnoreListDialog from qt.exclude_list_dialog import ExcludeListDialog from qt.deletion_options import DeletionOptions from qt.se.details_dialog import DetailsDialog as DetailsDialogStandard from qt.me.details_dialog import DetailsDialog as DetailsDialogMusic from qt.pe.details_dialog import DetailsDialog as DetailsDialogPicture from qt.se.preferences_dialog import PreferencesDialog as PreferencesDialogStandard from qt.me.preferences_dialog import PreferencesDialog as PreferencesDialogMusic from qt.pe.preferences_dialog import PreferencesDialog as PreferencesDialogPicture from qt.pe.photo import File as PlatSpecificPhoto from qt.tabbed_window import TabBarWindow, TabWindow tr = trget("ui") class DupeGuru(QObject): LOGO_NAME = "logo_se" NAME = "dupeGuru" def __init__(self, **kwargs): super().__init__(**kwargs) self.prefs = Preferences() self.prefs.load() # Enable tabs instead of separate floating windows for each dialog # Could be passed as an argument to this class if we wanted self.use_tabs = True self.model = DupeGuruModel(view=self, portable=self.prefs.portable) self._setup() # --- Private def _setup(self): core.pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = PlatSpecificPhoto self._setupActions() self.details_dialog = None self._update_options() self.recentResults = Recent(self, "recentResults") self.recentResults.mustOpenItem.connect(self.model.load_from) self.resultWindow = None if self.use_tabs: self.main_window = TabBarWindow(self) if not self.prefs.tabs_default_pos else TabWindow(self) parent_window = self.main_window self.directories_dialog = self.main_window.createPage("DirectoriesDialog", app=self) self.main_window.addTab(self.directories_dialog, tr("Directories"), switch=False) self.actionDirectoriesWindow.setEnabled(False) else: # floating windows only self.main_window = None self.directories_dialog = DirectoriesDialog(self) parent_window = self.directories_dialog self.progress_window = ProgressWindow(parent_window, self.model.progress_window) self.problemDialog = ProblemDialog(parent=parent_window, model=self.model.problem_dialog) if self.use_tabs: self.ignoreListDialog = self.main_window.createPage( "IgnoreListDialog", parent=self.main_window, model=self.model.ignore_list_dialog, ) self.excludeListDialog = self.main_window.createPage( "ExcludeListDialog", app=self, parent=self.main_window, model=self.model.exclude_list_dialog, ) else: self.ignoreListDialog = IgnoreListDialog(parent=parent_window, model=self.model.ignore_list_dialog) self.excludeDialog = ExcludeListDialog(app=self, parent=parent_window, model=self.model.exclude_list_dialog) self.deletionOptions = DeletionOptions(parent=parent_window, model=self.model.deletion_options) self.about_box = AboutBox(parent_window, self) parent_window.show() self.model.load() self.SIGTERM.connect(self.handleSIGTERM) # The timer scheme is because if the nag is not shown before the application is # completely initialized, the nag will be shown before the app shows up in the task bar # In some circumstances, the nag is hidden by other window, which may make the user think # that the application haven't launched. QTimer.singleShot(0, self.finishedLaunching) def _setupActions(self): # Setup actions that are common to both the directory dialog and the results window. # (name, shortcut, icon, desc, func) ACTIONS = [ ("actionQuit", "Ctrl+Q", "", tr("Quit"), self.quitTriggered), ( "actionPreferences", "Ctrl+P", "", tr("Options"), self.preferencesTriggered, ), ("actionIgnoreList", "", "", tr("Ignore List"), self.ignoreListTriggered), ( "actionDirectoriesWindow", "", "", tr("Directories"), self.showDirectoriesWindow, ), ( "actionClearCache", "Ctrl+Shift+P", "", tr("Clear Cache"), self.clearCacheTriggered, ), ( "actionExcludeList", "", "", tr("Exclusion Filters"), self.excludeListTriggered, ), ("actionShowHelp", "F1", "", tr("dupeGuru Help"), self.showHelpTriggered), ("actionAbout", "", "", tr("About dupeGuru"), self.showAboutBoxTriggered), ( "actionOpenDebugLog", "", "", tr("Open Debug Log"), self.openDebugLogTriggered, ), ] create_actions(ACTIONS, self) def _update_options(self): self.model.options["mix_file_kind"] = self.prefs.mix_file_kind self.model.options["escape_filter_regexp"] = not self.prefs.use_regexp self.model.options["clean_empty_dirs"] = self.prefs.remove_empty_folders self.model.options["ignore_hardlink_matches"] = self.prefs.ignore_hardlink_matches self.model.options["copymove_dest_type"] = self.prefs.destination_type self.model.options["scan_type"] = self.prefs.get_scan_type(self.model.app_mode) self.model.options["min_match_percentage"] = self.prefs.filter_hardness self.model.options["word_weighting"] = self.prefs.word_weighting self.model.options["match_similar_words"] = self.prefs.match_similar threshold = self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0 self.model.options["size_threshold"] = threshold * 1024 # threshold is in KB. The scanner wants bytes large_threshold = self.prefs.large_file_threshold if self.prefs.ignore_large_files else 0 self.model.options["large_size_threshold"] = ( large_threshold * 1024 * 1024 ) # threshold is in MB. The Scanner wants bytes big_file_size_threshold = self.prefs.big_file_size_threshold if self.prefs.big_file_partial_hashes else 0 self.model.options["big_file_size_threshold"] = ( big_file_size_threshold * 1024 * 1024 # threshold is in MiB. The scanner wants bytes ) scanned_tags = set() if self.prefs.scan_tag_track: scanned_tags.add("track") if self.prefs.scan_tag_artist: scanned_tags.add("artist") if self.prefs.scan_tag_album: scanned_tags.add("album") if self.prefs.scan_tag_title: scanned_tags.add("title") if self.prefs.scan_tag_genre: scanned_tags.add("genre") if self.prefs.scan_tag_year: scanned_tags.add("year") self.model.options["scanned_tags"] = scanned_tags self.model.options["match_scaled"] = self.prefs.match_scaled self.model.options["picture_cache_type"] = self.prefs.picture_cache_type if self.details_dialog: self.details_dialog.update_options() self._set_style("dark" if self.prefs.use_dark_style else "light") # --- Private def _get_details_dialog_class(self): if self.model.app_mode == AppMode.PICTURE: return DetailsDialogPicture elif self.model.app_mode == AppMode.MUSIC: return DetailsDialogMusic else: return DetailsDialogStandard def _get_preferences_dialog_class(self): if self.model.app_mode == AppMode.PICTURE: return PreferencesDialogPicture elif self.model.app_mode == AppMode.MUSIC: return PreferencesDialogMusic else: return PreferencesDialogStandard def _set_style(self, style="light"): # Only support this feature on windows for now if not plat.ISWINDOWS: return if style == "dark": QApplication.setStyle(QStyleFactory.create("Fusion")) palette = QApplication.style().standardPalette() palette.setColor(QPalette.ColorRole.Window, QColor(53, 53, 53)) palette.setColor(QPalette.ColorRole.WindowText, Qt.white) palette.setColor(QPalette.ColorRole.Base, QColor(25, 25, 25)) palette.setColor(QPalette.ColorRole.AlternateBase, QColor(53, 53, 53)) palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(53, 53, 53)) palette.setColor(QPalette.ColorRole.ToolTipText, Qt.white) palette.setColor(QPalette.ColorRole.Text, Qt.white) palette.setColor(QPalette.ColorRole.Button, QColor(53, 53, 53)) palette.setColor(QPalette.ColorRole.ButtonText, Qt.white) palette.setColor(QPalette.ColorRole.BrightText, Qt.red) palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218)) palette.setColor(QPalette.ColorRole.Highlight, QColor(42, 130, 218)) palette.setColor(QPalette.ColorRole.HighlightedText, Qt.black) palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, QColor(164, 166, 168)) palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, QColor(164, 166, 168)) palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, QColor(164, 166, 168)) palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.HighlightedText, QColor(164, 166, 168)) palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Base, QColor(68, 68, 68)) palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Window, QColor(68, 68, 68)) palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Highlight, QColor(68, 68, 68)) else: QApplication.setStyle(QStyleFactory.create("windowsvista" if plat.ISWINDOWS else "Fusion")) palette = QApplication.style().standardPalette() QToolTip.setPalette(palette) QApplication.setPalette(palette) # --- Public def add_selected_to_ignore_list(self): self.model.add_selected_to_ignore_list() def remove_selected(self): self.model.remove_selected(self) def confirm(self, title, msg, default_button=QMessageBox.Yes): active = QApplication.activeWindow() buttons = QMessageBox.Yes | QMessageBox.No answer = QMessageBox.question(active, title, msg, buttons, default_button) return answer == QMessageBox.Yes def invokeCustomCommand(self): self.model.invoke_custom_command() def show_details(self): if self.details_dialog is not None: if not self.details_dialog.isVisible(): self.details_dialog.show() else: self.details_dialog.hide() def showResultsWindow(self): if self.resultWindow is not None: if self.use_tabs: if self.main_window.indexOfWidget(self.resultWindow) < 0: self.main_window.addTab(self.resultWindow, tr("Results"), switch=True) return self.main_window.showTab(self.resultWindow) else: self.resultWindow.show() def showDirectoriesWindow(self): if self.directories_dialog is not None: if self.use_tabs: self.main_window.showTab(self.directories_dialog) else: self.directories_dialog.show() def shutdown(self): self.willSavePrefs.emit() self.prefs.save() self.model.save() self.model.close() # Workaround for #857, hide() or close(). if self.details_dialog is not None: self.details_dialog.close() QApplication.quit() # --- Signals willSavePrefs = pyqtSignal() SIGTERM = pyqtSignal() # --- Events def finishedLaunching(self): if sys.getfilesystemencoding() == "ascii": # No need to localize this, it's a debugging message. msg = ( "Something is wrong with the way your system locale is set. If the files you're " "scanning have accented letters, you'll probably get a crash. It is advised that " "you set your system locale properly." ) QMessageBox.warning( self.main_window if self.main_window else self.directories_dialog, "Wrong Locale", msg, ) # Load results on open if passed a .dupeguru file if len(sys.argv) > 1: results = sys.argv[1] if results.endswith(".dupeguru"): self.model.load_from(results) self.recentResults.insertItem(results) def clearCacheTriggered(self): title = tr("Clear Cache") msg = tr("Do you really want to clear the cache? This will remove all cached file hashes and picture analysis.") if self.confirm(title, msg, QMessageBox.No): self.model.clear_picture_cache() self.model.clear_hash_cache() active = QApplication.activeWindow() QMessageBox.information(active, title, tr("Cache cleared.")) def ignoreListTriggered(self): if self.use_tabs: self.showTriggeredTabbedDialog(self.ignoreListDialog, tr("Ignore List")) else: # floating windows self.model.ignore_list_dialog.show() def excludeListTriggered(self): if self.use_tabs: self.showTriggeredTabbedDialog(self.excludeListDialog, tr("Exclusion Filters")) else: # floating windows self.model.exclude_list_dialog.show() def showTriggeredTabbedDialog(self, dialog, desc_string): """Add tab for dialog, name the tab with desc_string, then show it.""" index = self.main_window.indexOfWidget(dialog) # Create the tab if it doesn't exist already if index < 0: # or (not dialog.isVisible() and not self.main_window.isTabVisible(index)): index = self.main_window.addTab(dialog, desc_string, switch=True) # Show the tab for that widget self.main_window.setCurrentIndex(index) def openDebugLogTriggered(self): debug_log_path = op.join(self.model.appdata, "debug.log") desktop.open_path(debug_log_path) def preferencesTriggered(self): preferences_dialog = self._get_preferences_dialog_class()( self.main_window if self.main_window else self.directories_dialog, self ) preferences_dialog.load() result = preferences_dialog.exec() if result == QDialog.Accepted: preferences_dialog.save() self.prefs.save() self._update_options() preferences_dialog.setParent(None) def quitTriggered(self): if self.details_dialog is not None: self.details_dialog.close() if self.main_window: self.main_window.close() else: self.directories_dialog.close() def showAboutBoxTriggered(self): self.about_box.show() def showHelpTriggered(self): base_path = platform.HELP_PATH help_path = op.abspath(op.join(base_path, "index.html")) if op.exists(help_path): url = QUrl.fromLocalFile(help_path) else: url = QUrl("https://dupeguru.voltaicideas.net/help/en/") QDesktopServices.openUrl(url) def handleSIGTERM(self): self.shutdown() # --- model --> view def get_default(self, key): return self.prefs.get_value(key) def set_default(self, key, value): self.prefs.set_value(key, value) def show_message(self, msg): window = QApplication.activeWindow() QMessageBox.information(window, "", msg) def ask_yes_no(self, prompt): return self.confirm("", prompt) def create_results_window(self): """Creates resultWindow and details_dialog depending on the selected ``app_mode``.""" if self.details_dialog is not None: # The object is not deleted entirely, avoid saving its geometry in the future # self.willSavePrefs.disconnect(self.details_dialog.appWillSavePrefs) # or simply delete it on close which is probably cleaner: self.details_dialog.setAttribute(Qt.WA_DeleteOnClose) self.details_dialog.close() # if we don't do the following, Qt will crash when we recreate the Results dialog self.details_dialog.setParent(None) if self.resultWindow is not None: self.resultWindow.close() # This is better for tabs, as it takes care of duplicate items in menu bar self.resultWindow.deleteLater() if self.use_tabs else self.resultWindow.setParent(None) if self.use_tabs: self.resultWindow = self.main_window.createPage("ResultWindow", parent=self.main_window, app=self) else: # We don't use a tab widget, regular floating QMainWindow self.resultWindow = ResultWindow(self.directories_dialog, self) self.directories_dialog._updateActionsState() self.details_dialog = self._get_details_dialog_class()(self.resultWindow, self) def show_results_window(self): self.showResultsWindow() def show_problem_dialog(self): self.problemDialog.show() def select_dest_folder(self, prompt): flags = QFileDialog.ShowDirsOnly return QFileDialog.getExistingDirectory(self.resultWindow, prompt, "", flags) def select_dest_file(self, prompt, extension): files = tr("{} file (*.{})").format(extension.upper(), extension) destination, chosen_filter = QFileDialog.getSaveFileName(self.resultWindow, prompt, "", files) if not destination.endswith(f".{extension}"): destination = f"{destination}.{extension}" return destination dupeguru-4.3.1/qt/column.py000066400000000000000000000106511426171743600156700ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2009-11-25 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QHeaderView class Column: def __init__( self, attrname, default_width, editor=None, alignment=Qt.AlignLeft, cant_truncate=False, painter=None, resize_to_fit=False, ): self.attrname = attrname self.default_width = default_width self.editor = editor # See moneyguru #15. Painter attribute was added to allow custom painting of amount value and # currency information. Can be used as a pattern for custom painting of any column. self.painter = painter self.alignment = alignment # This is to indicate, during printing, that a column can't have its data truncated. self.cant_truncate = cant_truncate self.resize_to_fit = resize_to_fit class Columns: def __init__(self, model, columns, header_view): self.model = model self._header_view = header_view self._header_view.setDefaultAlignment(Qt.AlignLeft) def setspecs(col, modelcol): modelcol.default_width = col.default_width modelcol.editor = col.editor modelcol.painter = col.painter modelcol.resize_to_fit = col.resize_to_fit modelcol.alignment = col.alignment modelcol.cant_truncate = col.cant_truncate if columns: for col in columns: modelcol = self.model.column_by_name(col.attrname) setspecs(col, modelcol) else: col = Column("", 100) for modelcol in self.model.column_list: setspecs(col, modelcol) self.model.view = self self._header_view.sectionMoved.connect(self.header_section_moved) self._header_view.sectionResized.connect(self.header_section_resized) # See moneyguru #14 and #15. This was added in order to allow automatic resizing of columns. for column in self.model.column_list: if column.resize_to_fit: self._header_view.setSectionResizeMode(column.logical_index, QHeaderView.ResizeToContents) # --- Public def set_columns_width(self, widths): # `widths` can be None. If it is, then default widths are set. columns = self.model.column_list if not widths: widths = [column.default_width for column in columns] for column, width in zip(columns, widths): if width == 0: # column was hidden before. width = column.default_width self._header_view.resizeSection(column.logical_index, width) def set_columns_order(self, column_indexes): if not column_indexes: return for dest_index, column_index in enumerate(column_indexes): # moveSection takes 2 visual index arguments, so we have to get our visual index first visual_index = self._header_view.visualIndex(column_index) self._header_view.moveSection(visual_index, dest_index) # --- Events def header_section_moved(self, logical_index, old_visual_index, new_visual_index): attrname = self.model.column_by_index(logical_index).name self.model.move_column(attrname, new_visual_index) def header_section_resized(self, logical_index, old_size, new_size): attrname = self.model.column_by_index(logical_index).name self.model.resize_column(attrname, new_size) # --- model --> view def restore_columns(self): columns = self.model.ordered_columns indexes = [col.logical_index for col in columns] self.set_columns_order(indexes) widths = [col.width for col in self.model.column_list] if not any(widths): widths = None self.set_columns_width(widths) for column in self.model.column_list: visible = self.model.column_is_visible(column.name) self._header_view.setSectionHidden(column.logical_index, not visible) def set_column_visible(self, colname, visible): column = self.model.column_by_name(colname) self._header_view.setSectionHidden(column.logical_index, not visible) dupeguru-4.3.1/qt/deletion_options.py000066400000000000000000000067721426171743600177620ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2012-05-30 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QCheckBox, QDialogButtonBox from hscommon.trans import trget from qt.radio_box import RadioBox tr = trget("ui") class DeletionOptions(QDialog): def __init__(self, parent, model, **kwargs): flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint super().__init__(parent, flags, **kwargs) self.model = model self._setupUi() self.model.view = self self.linkCheckbox.stateChanged.connect(self.linkCheckboxChanged) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) def _setupUi(self): self.setWindowTitle(tr("Deletion Options")) self.resize(400, 270) self.verticalLayout = QVBoxLayout(self) self.msgLabel = QLabel() self.verticalLayout.addWidget(self.msgLabel) self.linkCheckbox = QCheckBox(tr("Link deleted files")) self.verticalLayout.addWidget(self.linkCheckbox) text = tr( "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." ) self.linkMessageLabel = QLabel(text) self.linkMessageLabel.setWordWrap(True) self.verticalLayout.addWidget(self.linkMessageLabel) self.linkTypeRadio = RadioBox(items=[tr("Symlink"), tr("Hardlink")], spread=False) self.verticalLayout.addWidget(self.linkTypeRadio) if not self.model.supports_links(): self.linkCheckbox.setEnabled(False) self.linkCheckbox.setText(self.linkCheckbox.text() + tr(" (unsupported)")) self.directCheckbox = QCheckBox(tr("Directly delete files")) self.verticalLayout.addWidget(self.directCheckbox) text = tr( "Instead of sending files to trash, delete them directly. This option is usually " "used as a workaround when the normal deletion method doesn't work." ) self.directMessageLabel = QLabel(text) self.directMessageLabel.setWordWrap(True) self.verticalLayout.addWidget(self.directMessageLabel) self.buttonBox = QDialogButtonBox() self.buttonBox.addButton(tr("Proceed"), QDialogButtonBox.AcceptRole) self.buttonBox.addButton(tr("Cancel"), QDialogButtonBox.RejectRole) self.verticalLayout.addWidget(self.buttonBox) # --- Signals def linkCheckboxChanged(self, changed: int): self.model.link_deleted = bool(changed) # --- model --> view def update_msg(self, msg: str): self.msgLabel.setText(msg) def show(self): self.linkCheckbox.setChecked(self.model.link_deleted) self.linkTypeRadio.selected_index = 1 if self.model.use_hardlinks else 0 self.directCheckbox.setChecked(self.model.direct) result = self.exec() self.model.link_deleted = self.linkCheckbox.isChecked() self.model.use_hardlinks = self.linkTypeRadio.selected_index == 1 self.model.direct = self.directCheckbox.isChecked() return result == QDialog.Accepted def set_hardlink_option_enabled(self, is_enabled: bool): self.linkTypeRadio.setEnabled(is_enabled) dupeguru-4.3.1/qt/details_dialog.py000066400000000000000000000066411426171743600173430ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2010-02-05 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QDockWidget, QWidget from qt.util import move_to_screen_center from qt.details_table import DetailsModel from hscommon.plat import ISLINUX class DetailsDialog(QDockWidget): def __init__(self, parent, app, **kwargs): super().__init__(parent, Qt.Tool, **kwargs) self.parent = parent self.app = app self.model = app.model.details_panel self.setAllowedAreas(Qt.AllDockWidgetAreas) self._setupUi() # To avoid saving uninitialized geometry on appWillSavePrefs, we track whether our dialog # has been shown. If it has, we know that our geometry should be saved. self._shown_once = False self._wasDocked, area = self.app.prefs.restoreGeometry("DetailsWindowRect", self) self.tableModel = DetailsModel(self.model, app) # tableView is defined in subclasses self.tableView.setModel(self.tableModel) self.model.view = self self.app.willSavePrefs.connect(self.appWillSavePrefs) # self.setAttribute(Qt.WA_DeleteOnClose) parent.addDockWidget(area if self._wasDocked else Qt.BottomDockWidgetArea, self) def _setupUi(self): # Virtual pass def show(self): if not self._shown_once and self._wasDocked: self.setFloating(False) self._shown_once = True super().show() self.update_options() def update_options(self): # This disables the title bar (if we had not set one before already) # essentially making it a simple floating window, not dockable anymore if not self.app.prefs.details_dialog_titlebar_enabled: if not self.titleBarWidget(): # default title bar self.setTitleBarWidget(QWidget()) # disables title bar # Windows (and MacOS?) users cannot move a floating window which # has no native decoration so we force it to dock for now if not ISLINUX: self.setFloating(False) elif self.titleBarWidget() is not None: # title bar is disabled self.setTitleBarWidget(None) # resets to the default title bar elif not self.titleBarWidget() and not self.app.prefs.details_dialog_titlebar_enabled: self.setTitleBarWidget(QWidget()) features = self.features() if self.app.prefs.details_dialog_vertical_titlebar: self.setFeatures(features | QDockWidget.DockWidgetVerticalTitleBar) elif features & QDockWidget.DockWidgetVerticalTitleBar: self.setFeatures(features ^ QDockWidget.DockWidgetVerticalTitleBar) # --- Events def appWillSavePrefs(self): if self._shown_once: self.app.prefs.saveGeometry("DetailsWindowRect", self) # --- model --> view def refresh(self): self.tableModel.beginResetModel() self.tableModel.endResetModel() def showEvent(self, event): if self._wasDocked is False: # have to do this here as the frameGeometry is not correct until shown move_to_screen_center(self) super().showEvent(event) dupeguru-4.3.1/qt/details_table.py000066400000000000000000000064511426171743600171720ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2009-05-17 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import Qt, QAbstractTableModel from PyQt5.QtWidgets import QHeaderView, QTableView from PyQt5.QtGui import QFont, QBrush from hscommon.trans import trget tr = trget("ui") HEADER = [tr("Selected"), tr("Reference")] class DetailsModel(QAbstractTableModel): def __init__(self, model, app, **kwargs): super().__init__(**kwargs) self.model = model self.prefs = app.prefs def columnCount(self, parent): return len(HEADER) def data(self, index, role): if not index.isValid(): return None # Skip first value "Attribute" column = index.column() + 1 row = index.row() ignored_fields = ["Dupe Count"] if ( self.model.row(row)[0] in ignored_fields or self.model.row(row)[1] == "---" or self.model.row(row)[2] == "---" ): if role != Qt.DisplayRole: return None return self.model.row(row)[column] if role == Qt.DisplayRole: return self.model.row(row)[column] if role == Qt.ForegroundRole and self.model.row(row)[1] != self.model.row(row)[2]: return QBrush(self.prefs.details_table_delta_foreground_color) if role == Qt.FontRole and self.model.row(row)[1] != self.model.row(row)[2]: font = QFont(self.model.view.font()) # or simply QFont() font.setBold(True) return font return None # QVariant() def headerData(self, section, orientation, role): if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(HEADER): return HEADER[section] elif orientation == Qt.Vertical and role == Qt.DisplayRole and section < self.model.row_count(): # Read "Attribute" cell for horizontal header return self.model.row(section)[0] return None def rowCount(self, parent): return self.model.row_count() class DetailsTable(QTableView): def __init__(self, *args): QTableView.__init__(self, *args) self.setAlternatingRowColors(True) self.setSelectionBehavior(QTableView.SelectRows) self.setSelectionMode(QTableView.NoSelection) self.setShowGrid(False) self.setWordWrap(False) self.setCornerButtonEnabled(False) def setModel(self, model): QTableView.setModel(self, model) # The model needs to be set to set header stuff hheader = self.horizontalHeader() hheader.setHighlightSections(False) hheader.setSectionResizeMode(0, QHeaderView.Stretch) hheader.setSectionResizeMode(1, QHeaderView.Stretch) vheader = self.verticalHeader() vheader.setVisible(True) vheader.setDefaultSectionSize(18) # hardcoded value above is not ideal, perhaps resize to contents first? # vheader.setSectionResizeMode(QHeaderView.ResizeToContents) vheader.setSectionResizeMode(QHeaderView.Fixed) vheader.setSectionsMovable(True) dupeguru-4.3.1/qt/dg.qrc000066400000000000000000000013671426171743600151260ustar00rootroot00000000000000 ../images/dgse_logo_32.png ../images/dgse_logo_128.png ../images/plus_8.png ../images/minus_8.png ../images/search_clear_13.png ../images/exchange_purple_upscaled.png ../images/old_zoom_in.png ../images/old_zoom_out.png ../images/old_zoom_original.png ../images/old_zoom_best_fit.png ../images/dialog-error.png dupeguru-4.3.1/qt/directories_dialog.py000066400000000000000000000366621426171743600202400ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import QRect, Qt from PyQt5.QtWidgets import ( QListView, QWidget, QFileDialog, QHeaderView, QVBoxLayout, QHBoxLayout, QTreeView, QAbstractItemView, QSpacerItem, QSizePolicy, QPushButton, QMainWindow, QMenuBar, QMenu, QLabel, QComboBox, ) from PyQt5.QtGui import QPixmap, QIcon from hscommon.trans import trget from core.app import AppMode from qt.radio_box import RadioBox from qt.recent import Recent from qt.util import move_to_screen_center, create_actions from qt import platform from qt.directories_model import DirectoriesModel, DirectoriesDelegate tr = trget("ui") class DirectoriesDialog(QMainWindow): def __init__(self, app, **kwargs): super().__init__(None, **kwargs) self.app = app self.specific_actions = set() self.lastAddedFolder = platform.INITIAL_FOLDER_IN_DIALOGS self.recentFolders = Recent(self.app, "recentFolders") self._setupUi() self._updateScanTypeList() self.directoriesModel = DirectoriesModel(self.app.model.directory_tree, view=self.treeView) self.directoriesDelegate = DirectoriesDelegate() self.treeView.setItemDelegate(self.directoriesDelegate) self._setupColumns() self.app.recentResults.addMenu(self.menuLoadRecent) self.app.recentResults.addMenu(self.menuRecentResults) self.recentFolders.addMenu(self.menuRecentFolders) self._updateAddButton() self._updateRemoveButton() self._updateLoadResultsButton() self._updateActionsState() self._setupBindings() def _setupBindings(self): self.appModeRadioBox.itemSelected.connect(self.appModeButtonSelected) self.showPreferencesButton.clicked.connect(self.app.actionPreferences.trigger) self.scanButton.clicked.connect(self.scanButtonClicked) self.loadResultsButton.clicked.connect(self.actionLoadResults.trigger) self.addFolderButton.clicked.connect(self.actionAddFolder.trigger) self.removeFolderButton.clicked.connect(self.removeFolderButtonClicked) self.treeView.selectionModel().selectionChanged.connect(self.selectionChanged) self.app.recentResults.itemsChanged.connect(self._updateLoadResultsButton) self.recentFolders.itemsChanged.connect(self._updateAddButton) self.recentFolders.mustOpenItem.connect(self.app.model.add_directory) self.directoriesModel.foldersAdded.connect(self.directoriesModelAddedFolders) self.app.willSavePrefs.connect(self.appWillSavePrefs) def _setupActions(self): # (name, shortcut, icon, desc, func) ACTIONS = [ ( "actionLoadResults", "Ctrl+L", "", tr("Load Results..."), self.loadResultsTriggered, ), ( "actionShowResultsWindow", "", "", tr("Scan Results"), self.app.showResultsWindow, ), ("actionAddFolder", "", "", tr("Add Folder..."), self.addFolderTriggered), ("actionLoadDirectories", "", "", tr("Load Directories..."), self.loadDirectoriesTriggered), ("actionSaveDirectories", "", "", tr("Save Directories..."), self.saveDirectoriesTriggered), ] create_actions(ACTIONS, self) if self.app.use_tabs: # Keep track of actions which should only be accessible from this window self.specific_actions.add(self.actionLoadDirectories) self.specific_actions.add(self.actionSaveDirectories) def _setupMenu(self): if not self.app.use_tabs: # we are our own QMainWindow, we need our own menu bar self.menubar = QMenuBar(self) self.menubar.setGeometry(QRect(0, 0, 42, 22)) self.menuFile = QMenu(self.menubar) self.menuFile.setTitle(tr("File")) self.menuView = QMenu(self.menubar) self.menuView.setTitle(tr("View")) self.menuHelp = QMenu(self.menubar) self.menuHelp.setTitle(tr("Help")) self.setMenuBar(self.menubar) menubar = self.menubar else: # we are part of a tab widget, we populate its window's menubar instead self.menuFile = self.app.main_window.menuFile self.menuView = self.app.main_window.menuView self.menuHelp = self.app.main_window.menuHelp menubar = self.app.main_window.menubar self.menuLoadRecent = QMenu(self.menuFile) self.menuLoadRecent.setTitle(tr("Load Recent Results")) self.menuFile.addAction(self.actionLoadResults) self.menuFile.addAction(self.menuLoadRecent.menuAction()) self.menuFile.addSeparator() self.menuFile.addAction(self.app.actionClearCache) self.menuFile.addSeparator() self.menuFile.addAction(self.actionLoadDirectories) self.menuFile.addAction(self.actionSaveDirectories) self.menuFile.addSeparator() self.menuFile.addAction(self.app.actionQuit) self.menuView.addAction(self.app.actionDirectoriesWindow) self.menuView.addAction(self.actionShowResultsWindow) self.menuView.addAction(self.app.actionIgnoreList) self.menuView.addAction(self.app.actionExcludeList) self.menuView.addSeparator() self.menuView.addAction(self.app.actionPreferences) self.menuHelp.addAction(self.app.actionShowHelp) self.menuHelp.addAction(self.app.actionOpenDebugLog) self.menuHelp.addAction(self.app.actionAbout) menubar.addAction(self.menuFile.menuAction()) menubar.addAction(self.menuView.menuAction()) menubar.addAction(self.menuHelp.menuAction()) # Recent folders menu self.menuRecentFolders = QMenu() self.menuRecentFolders.addAction(self.actionAddFolder) self.menuRecentFolders.addSeparator() # Recent results menu self.menuRecentResults = QMenu() self.menuRecentResults.addAction(self.actionLoadResults) self.menuRecentResults.addSeparator() def _setupUi(self): self.setWindowTitle(self.app.NAME) self.resize(420, 338) self.centralwidget = QWidget(self) self.verticalLayout = QVBoxLayout(self.centralwidget) self.verticalLayout.setContentsMargins(4, 0, 4, 0) self.verticalLayout.setSpacing(0) hl = QHBoxLayout() label = QLabel(tr("Application Mode:"), self) label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) hl.addWidget(label) self.appModeRadioBox = RadioBox(self, items=[tr("Standard"), tr("Music"), tr("Picture")], spread=False) hl.addWidget(self.appModeRadioBox) self.verticalLayout.addLayout(hl) hl = QHBoxLayout() hl.setAlignment(Qt.AlignLeft) label = QLabel(tr("Scan Type:"), self) label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) hl.addWidget(label) self.scanTypeComboBox = QComboBox(self) self.scanTypeComboBox.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) self.scanTypeComboBox.setMaximumWidth(400) hl.addWidget(self.scanTypeComboBox) self.showPreferencesButton = QPushButton(tr("More Options"), self.centralwidget) self.showPreferencesButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) hl.addWidget(self.showPreferencesButton) self.verticalLayout.addLayout(hl) self.promptLabel = QLabel(tr('Select folders to scan and press "Scan".'), self.centralwidget) self.verticalLayout.addWidget(self.promptLabel) self.treeView = QTreeView(self.centralwidget) self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection) self.treeView.setSelectionBehavior(QAbstractItemView.SelectRows) self.treeView.setAcceptDrops(True) triggers = ( QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed | QAbstractItemView.SelectedClicked ) self.treeView.setEditTriggers(triggers) self.treeView.setDragDropOverwriteMode(True) self.treeView.setDragDropMode(QAbstractItemView.DropOnly) self.treeView.setUniformRowHeights(True) self.verticalLayout.addWidget(self.treeView) self.horizontalLayout = QHBoxLayout() self.removeFolderButton = QPushButton(self.centralwidget) self.removeFolderButton.setIcon(QIcon(QPixmap(":/minus"))) self.removeFolderButton.setShortcut("Del") self.horizontalLayout.addWidget(self.removeFolderButton) self.addFolderButton = QPushButton(self.centralwidget) self.addFolderButton.setIcon(QIcon(QPixmap(":/plus"))) self.horizontalLayout.addWidget(self.addFolderButton) spacer_item = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacer_item) self.loadResultsButton = QPushButton(self.centralwidget) self.loadResultsButton.setText(tr("Load Results")) self.horizontalLayout.addWidget(self.loadResultsButton) self.scanButton = QPushButton(self.centralwidget) self.scanButton.setText(tr("Scan")) self.scanButton.setDefault(True) self.horizontalLayout.addWidget(self.scanButton) self.verticalLayout.addLayout(self.horizontalLayout) self.setCentralWidget(self.centralwidget) self._setupActions() self._setupMenu() if self.app.prefs.directoriesWindowRect is not None: self.setGeometry(self.app.prefs.directoriesWindowRect) else: move_to_screen_center(self) def _setupColumns(self): header = self.treeView.header() header.setStretchLastSection(False) header.setSectionResizeMode(0, QHeaderView.Stretch) header.setSectionResizeMode(1, QHeaderView.Fixed) header.resizeSection(1, 100) def _updateActionsState(self): self.actionShowResultsWindow.setEnabled(self.app.resultWindow is not None) def _updateAddButton(self): if self.recentFolders.isEmpty(): self.addFolderButton.setMenu(None) else: self.addFolderButton.setMenu(self.menuRecentFolders) def _updateRemoveButton(self): indexes = self.treeView.selectedIndexes() if not indexes: self.removeFolderButton.setEnabled(False) return self.removeFolderButton.setEnabled(True) def _updateLoadResultsButton(self): if self.app.recentResults.isEmpty(): self.loadResultsButton.setMenu(None) else: self.loadResultsButton.setMenu(self.menuRecentResults) def _updateScanTypeList(self): try: self.scanTypeComboBox.currentIndexChanged[int].disconnect(self.scanTypeChanged) except TypeError: # Not connected, ignore pass self.scanTypeComboBox.clear() scan_options = self.app.model.SCANNER_CLASS.get_scan_options() for scan_option in scan_options: self.scanTypeComboBox.addItem(scan_option.label) SCAN_TYPE_ORDER = [so.scan_type for so in scan_options] selected_scan_type = self.app.prefs.get_scan_type(self.app.model.app_mode) scan_type_index = SCAN_TYPE_ORDER.index(selected_scan_type) self.scanTypeComboBox.setCurrentIndex(scan_type_index) self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged) self.app._update_options() # --- QWidget overrides def closeEvent(self, event): event.accept() if self.app.model.results.is_modified: title = tr("Unsaved results") msg = tr("You have unsaved results, do you really want to quit?") if not self.app.confirm(title, msg): event.ignore() if event.isAccepted(): self.app.shutdown() # --- Events def addFolderTriggered(self): no_native = not self.app.prefs.use_native_dialogs title = tr("Select a folder to add to the scanning list") file_dialog = QFileDialog(self, title, self.lastAddedFolder) file_dialog.setFileMode(QFileDialog.DirectoryOnly) file_dialog.setOption(QFileDialog.DontUseNativeDialog, no_native) if no_native: file_view = file_dialog.findChild(QListView, "listView") if file_view: file_view.setSelectionMode(QAbstractItemView.MultiSelection) f_tree_view = file_dialog.findChild(QTreeView) if f_tree_view: f_tree_view.setSelectionMode(QAbstractItemView.MultiSelection) if not file_dialog.exec(): return paths = file_dialog.selectedFiles() self.lastAddedFolder = paths[-1] [self.app.model.add_directory(path) for path in paths] [self.recentFolders.insertItem(path) for path in paths] def appModeButtonSelected(self, index): if index == 2: mode = AppMode.PICTURE elif index == 1: mode = AppMode.MUSIC else: mode = AppMode.STANDARD self.app.model.app_mode = mode self._updateScanTypeList() def appWillSavePrefs(self): self.app.prefs.directoriesWindowRect = self.geometry() def directoriesModelAddedFolders(self, folders): for folder in folders: self.recentFolders.insertItem(folder) def loadResultsTriggered(self): title = tr("Select a results file to load") files = ";;".join([tr("dupeGuru Results (*.dupeguru)"), tr("All Files (*.*)")]) destination = QFileDialog.getOpenFileName(self, title, "", files)[0] if destination: self.app.model.load_from(destination) self.app.recentResults.insertItem(destination) def loadDirectoriesTriggered(self): title = tr("Select a directories file to load") files = ";;".join([tr("dupeGuru Directories (*.dupegurudirs)"), tr("All Files (*.*)")]) destination = QFileDialog.getOpenFileName(self, title, "", files)[0] if destination: self.app.model.load_directories(destination) def removeFolderButtonClicked(self): self.directoriesModel.model.remove_selected() def saveDirectoriesTriggered(self): title = tr("Select a file to save your directories to") files = tr("dupeGuru Directories (*.dupegurudirs)") destination, chosen_filter = QFileDialog.getSaveFileName(self, title, "", files) if destination: if not destination.endswith(".dupegurudirs"): destination = f"{destination}.dupegurudirs" self.app.model.save_directories_as(destination) def scanButtonClicked(self): if self.app.model.results.is_modified: title = tr("Start a new scan") msg = tr("You have unsaved results, do you really want to continue?") if not self.app.confirm(title, msg): return self.app.model.start_scanning(self.app.prefs.profile_scan) def scanTypeChanged(self, index): scan_options = self.app.model.SCANNER_CLASS.get_scan_options() self.app.prefs.set_scan_type(self.app.model.app_mode, scan_options[index].scan_type) self.app._update_options() def selectionChanged(self, selected, deselected): self._updateRemoveButton() dupeguru-4.3.1/qt/directories_model.py000066400000000000000000000127561426171743600200770ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2009-04-25 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import pyqtSignal, Qt, QRect, QUrl, QModelIndex, QItemSelection from PyQt5.QtWidgets import ( QComboBox, QStyledItemDelegate, QStyle, QStyleOptionComboBox, QStyleOptionViewItem, QApplication, ) from PyQt5.QtGui import QBrush from hscommon.trans import trget from qt.tree_model import RefNode, TreeModel tr = trget("ui") HEADERS = [tr("Name"), tr("State")] STATES = [tr("Normal"), tr("Reference"), tr("Excluded")] class DirectoriesDelegate(QStyledItemDelegate): def createEditor(self, parent, option, index): editor = QComboBox(parent) editor.addItems(STATES) return editor def paint(self, painter, option, index): self.initStyleOption(option, index) # No idea why, but this cast is required if we want to have access to the V4 valuess option = QStyleOptionViewItem(option) if (index.column() == 1) and (option.state & QStyle.State_Selected): cboption = QStyleOptionComboBox() cboption.rect = option.rect # On OS X (with Qt4.6.0), adding State_Enabled to the flags causes the whole drawing to # fail (draw nothing), but it's an OS X only glitch. On Windows, it works alright. cboption.state |= QStyle.State_Enabled QApplication.style().drawComplexControl(QStyle.CC_ComboBox, cboption, painter) painter.setBrush(option.palette.text()) rect = QRect(option.rect) rect.setLeft(rect.left() + 4) painter.drawText(rect, Qt.AlignLeft, option.text) else: super().paint(painter, option, index) def setEditorData(self, editor, index): value = index.model().data(index, Qt.EditRole) editor.setCurrentIndex(value) editor.showPopup() def setModelData(self, editor, model, index): value = editor.currentIndex() model.setData(index, value, Qt.EditRole) def updateEditorGeometry(self, editor, option, index): editor.setGeometry(option.rect) class DirectoriesModel(TreeModel): MIME_TYPE_FORMAT = "text/uri-list" def __init__(self, model, view, **kwargs): super().__init__(**kwargs) self.model = model self.model.view = self self.view = view self.view.setModel(self) self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged) def _create_node(self, ref, row): return RefNode(self, None, ref, row) def _get_children(self): return list(self.model) def columnCount(self, parent=QModelIndex()): return 2 def data(self, index, role): if not index.isValid(): return None node = index.internalPointer() ref = node.ref if role == Qt.DisplayRole: if index.column() == 0: return ref.name else: return STATES[ref.state] elif role == Qt.EditRole and index.column() == 1: return ref.state elif role == Qt.ForegroundRole: state = ref.state if state == 1: return QBrush(Qt.blue) elif state == 2: return QBrush(Qt.red) return None def dropMimeData(self, mime_data, action, row, column, parent_index): # the data in mimeData is urlencoded **in utf-8** if not mime_data.hasFormat(self.MIME_TYPE_FORMAT): return False data = bytes(mime_data.data(self.MIME_TYPE_FORMAT)).decode("ascii") urls = data.split("\r\n") paths = [QUrl(url).toLocalFile() for url in urls if url] for path in paths: self.model.add_directory(path) self.foldersAdded.emit(paths) self.reset() return True def flags(self, index): if not index.isValid(): return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled result = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDropEnabled if index.column() == 1: result |= Qt.ItemIsEditable return result def headerData(self, section, orientation, role): if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(HEADERS): return HEADERS[section] return None def mimeTypes(self): return [self.MIME_TYPE_FORMAT] def setData(self, index, value, role): if not index.isValid() or role != Qt.EditRole or index.column() != 1: return False node = index.internalPointer() ref = node.ref ref.state = value return True def supportedDropActions(self): # Normally, the correct action should be ActionLink, but the drop doesn't work. It doesn't # work with ActionMove either. So screw that, and accept anything. return Qt.ActionMask # --- Events def selectionChanged(self, selected, deselected): new_nodes = [modelIndex.internalPointer().ref for modelIndex in self.view.selectionModel().selectedRows()] self.model.selected_nodes = new_nodes # --- Signals foldersAdded = pyqtSignal(list) # --- model --> view def refresh(self): self.reset() def refresh_states(self): self.refreshData() dupeguru-4.3.1/qt/error_report_dialog.py000066400000000000000000000075421426171743600204430ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2009-05-23 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import traceback import sys import os import platform from PyQt5.QtCore import Qt, QCoreApplication, QSize from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPlainTextEdit, QPushButton, ) from hscommon.trans import trget from hscommon.desktop import open_url from qt.util import horizontal_spacer tr = trget("ui") class ErrorReportDialog(QDialog): def __init__(self, parent, github_url, error, **kwargs): flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint super().__init__(parent, flags, **kwargs) self._setupUi() name = QCoreApplication.applicationName() version = QCoreApplication.applicationVersion() error_text = "Application Name: {}\nVersion: {}\nPython: {}\nOperating System: {}\n\n{}".format( name, version, platform.python_version(), platform.platform(), error ) # Under windows, we end up with an error report without linesep if we don't mangle it error_text = error_text.replace("\n", os.linesep) self.errorTextEdit.setPlainText(error_text) self.github_url = github_url self.sendButton.clicked.connect(self.goToGithub) self.dontSendButton.clicked.connect(self.reject) def _setupUi(self): self.setWindowTitle(tr("Error Report")) self.resize(553, 349) self.verticalLayout = QVBoxLayout(self) self.label = QLabel(self) self.label.setText(tr("Something went wrong. How about reporting the error?")) self.label.setWordWrap(True) self.verticalLayout.addWidget(self.label) self.errorTextEdit = QPlainTextEdit(self) self.errorTextEdit.setReadOnly(True) self.verticalLayout.addWidget(self.errorTextEdit) msg = tr( "Error reports should be reported as Github issues. You can copy the error traceback " "above and paste it in a new issue.\n\nPlease make sure to run a search for any already " "existing issues beforehand. Also make sure to test the very latest version available from the repository, " "since the bug you are experiencing might have already been patched.\n\n" "What usually really helps is if you add a description of how you got the error. Thanks!" "\n\n" "Although the application should continue to run after this error, it may be in an " "unstable state, so it is recommended that you restart the application." ) self.label2 = QLabel(msg) self.label2.setWordWrap(True) self.verticalLayout.addWidget(self.label2) self.horizontalLayout = QHBoxLayout() self.horizontalLayout.addItem(horizontal_spacer()) self.dontSendButton = QPushButton(self) self.dontSendButton.setText(tr("Close")) self.dontSendButton.setMinimumSize(QSize(110, 0)) self.horizontalLayout.addWidget(self.dontSendButton) self.sendButton = QPushButton(self) self.sendButton.setText(tr("Go to Github")) self.sendButton.setMinimumSize(QSize(110, 0)) self.sendButton.setDefault(True) self.horizontalLayout.addWidget(self.sendButton) self.verticalLayout.addLayout(self.horizontalLayout) def goToGithub(self): open_url(self.github_url) def install_excepthook(github_url): def my_excepthook(exctype, value, tb): s = "".join(traceback.format_exception(exctype, value, tb)) dialog = ErrorReportDialog(None, github_url, s) dialog.exec_() sys.excepthook = my_excepthook dupeguru-4.3.1/qt/exclude_list_dialog.py000066400000000000000000000163011426171743600203740ustar00rootroot00000000000000# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import re from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtWidgets import ( QPushButton, QLineEdit, QVBoxLayout, QGridLayout, QDialog, QTableView, QAbstractItemView, QSpacerItem, QSizePolicy, QHeaderView, ) from qt.exclude_list_table import ExcludeListTable from core.exclude import AlreadyThereException from hscommon.trans import trget tr = trget("ui") class ExcludeListDialog(QDialog): def __init__(self, app, parent, model, **kwargs): flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint super().__init__(parent, flags, **kwargs) self.app = app self.specific_actions = frozenset() self._setupUI() self.model = model # ExcludeListDialogCore self.model.view = self self.table = ExcludeListTable(app, view=self.tableView) # Qt ExcludeListTable self._row_matched = False # test if at least one row matched our test string self._input_styled = False self.buttonAdd.clicked.connect(self.addStringFromLineEdit) self.buttonRemove.clicked.connect(self.removeSelected) self.buttonRestore.clicked.connect(self.restoreDefaults) self.buttonClose.clicked.connect(self.accept) self.buttonHelp.clicked.connect(self.display_help_message) self.buttonTestString.clicked.connect(self.onTestStringButtonClicked) self.inputLine.textEdited.connect(self.reset_input_style) self.testLine.textEdited.connect(self.reset_input_style) self.testLine.textEdited.connect(self.reset_table_style) def _setupUI(self): layout = QVBoxLayout(self) gridlayout = QGridLayout() self.buttonAdd = QPushButton(tr("Add")) self.buttonRemove = QPushButton(tr("Remove Selected")) self.buttonRestore = QPushButton(tr("Restore defaults")) self.buttonTestString = QPushButton(tr("Test string")) self.buttonClose = QPushButton(tr("Close")) self.buttonHelp = QPushButton(tr("Help")) self.inputLine = QLineEdit() self.testLine = QLineEdit() self.tableView = QTableView() triggers = ( QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed | QAbstractItemView.SelectedClicked ) self.tableView.setEditTriggers(triggers) self.tableView.setSelectionMode(QTableView.ExtendedSelection) self.tableView.setSelectionBehavior(QTableView.SelectRows) self.tableView.setShowGrid(False) vheader = self.tableView.verticalHeader() vheader.setSectionsMovable(True) vheader.setVisible(False) hheader = self.tableView.horizontalHeader() hheader.setSectionsMovable(False) hheader.setSectionResizeMode(QHeaderView.Fixed) hheader.setStretchLastSection(True) hheader.setHighlightSections(False) hheader.setVisible(True) gridlayout.addWidget(self.inputLine, 0, 0) gridlayout.addWidget(self.buttonAdd, 0, 1, Qt.AlignLeft) gridlayout.addWidget(self.buttonRemove, 1, 1, Qt.AlignLeft) gridlayout.addWidget(self.buttonRestore, 2, 1, Qt.AlignLeft) gridlayout.addWidget(self.buttonHelp, 3, 1, Qt.AlignLeft) gridlayout.addWidget(self.buttonClose, 4, 1) gridlayout.addWidget(self.tableView, 1, 0, 6, 1) gridlayout.addItem(QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding), 4, 1) gridlayout.addWidget(self.buttonTestString, 6, 1) gridlayout.addWidget(self.testLine, 6, 0) layout.addLayout(gridlayout) self.inputLine.setPlaceholderText(tr("Type a python regular expression here...")) self.inputLine.setFocus() self.testLine.setPlaceholderText(tr("Type a file system path or filename here...")) self.testLine.setClearButtonEnabled(True) # --- model --> view def show(self): super().show() self.inputLine.setFocus() @pyqtSlot() def addStringFromLineEdit(self): text = self.inputLine.text() if not text: return try: self.model.add(text) except AlreadyThereException: self.app.show_message("Expression already in the list.") return except Exception as e: self.app.show_message(f"Expression is invalid: {e}") return self.inputLine.clear() def removeSelected(self): self.model.remove_selected() def restoreDefaults(self): self.model.restore_defaults() def onTestStringButtonClicked(self): input_text = self.testLine.text() if not input_text: self.reset_input_style() return # If at least one row matched, we know whether table is highlighted or not self._row_matched = self.model.test_string(input_text) self.table.refresh() # Test the string currently in the input text box as well input_regex = self.inputLine.text() if not input_regex: self.reset_input_style() return compiled = None try: compiled = re.compile(input_regex) except re.error: self.reset_input_style() return if self.model.is_match(input_text, compiled): self.inputLine.setStyleSheet("background-color: rgb(10, 200, 10);") self._input_styled = True else: self.reset_input_style() def reset_input_style(self): """Reset regex input line background""" if self._input_styled: self.inputLine.setStyleSheet(self.styleSheet()) self._input_styled = False def reset_table_style(self): if self._row_matched: self._row_matched = False self.model.reset_rows_highlight() self.table.refresh() def display_help_message(self): self.app.show_message( tr( """\ These (case sensitive) python regular expressions will filter out files during scans.
    \ Directores will also have their default state set to Excluded \ in the Directories tab if their name happens to match one of the selected regular expressions.
    \ For each file collected, two tests are performed to determine whether or not to completely ignore it:
    \
  • 1. Regular expressions with no path separator in them will be compared to the file name only.
  • 2. Regular expressions with at least one path separator in them will be compared to the full path to the file.

  • Example: if you want to filter out .PNG files from the "My Pictures" directory only:
    \ .*My\\sPictures\\\\.*\\.png

    \ You can test the regular expression with the "test string" button after pasting a fake path in the test field:
    \ C:\\\\User\\My Pictures\\test.png

    Matching regular expressions will be highlighted.
    \ If there is at least one highlight, the path or filename tested will be ignored during scans.

    \ Directories and files starting with a period '.' are filtered out by default.

    """ ) ) dupeguru-4.3.1/qt/exclude_list_table.py000066400000000000000000000050001426171743600202160ustar00rootroot00000000000000# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont, QFontMetrics, QIcon, QColor from qt.column import Column from qt.table import Table from hscommon.trans import trget tr = trget("ui") class ExcludeListTable(Table): """Model for exclude list""" COLUMNS = [Column("marked", default_width=15), Column("regex", default_width=230)] def __init__(self, app, view, **kwargs): model = app.model.exclude_list_dialog.exclude_list_table # pointer to GUITable super().__init__(model, view, **kwargs) font = view.font() font.setPointSize(app.prefs.tableFontSize) view.setFont(font) fm = QFontMetrics(font) view.verticalHeader().setDefaultSectionSize(fm.height() + 2) def _getData(self, row, column, role): if column.name == "marked": if role == Qt.CheckStateRole and row.markable: return Qt.Checked if row.marked else Qt.Unchecked if role == Qt.ToolTipRole and not row.markable: return tr("Compilation error: ") + row.get_cell_value("error") if role == Qt.DecorationRole and not row.markable: return QIcon.fromTheme("dialog-error", QIcon(":/error")) return None if role == Qt.DisplayRole: return row.data[column.name] elif role == Qt.FontRole: return QFont(self.view.font()) elif role == Qt.BackgroundRole and column.name == "regex": if row.highlight: return QColor(10, 200, 10) # green elif role == Qt.EditRole and column.name == "regex": return row.data[column.name] return None def _getFlags(self, row, column): flags = Qt.ItemIsEnabled if column.name == "marked": if row.markable: flags |= Qt.ItemIsUserCheckable elif column.name == "regex": flags |= Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled return flags def _setData(self, row, column, value, role): if role == Qt.CheckStateRole: if column.name == "marked": row.marked = bool(value) return True elif role == Qt.EditRole and column.name == "regex": return self.model.rename_selected(value) return False dupeguru-4.3.1/qt/ignore_list_dialog.py000066400000000000000000000047061426171743600202340ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2012-03-13 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QPushButton, QTableView, QAbstractItemView, ) from hscommon.trans import trget from qt.util import horizontal_wrap from qt.ignore_list_table import IgnoreListTable tr = trget("ui") class IgnoreListDialog(QDialog): def __init__(self, parent, model, **kwargs): flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint super().__init__(parent, flags, **kwargs) self.specific_actions = frozenset() self._setupUi() self.model = model self.model.view = self self.table = IgnoreListTable(self.model.ignore_list_table, view=self.tableView) self.removeSelectedButton.clicked.connect(self.model.remove_selected) self.clearButton.clicked.connect(self.model.clear) self.closeButton.clicked.connect(self.accept) def _setupUi(self): self.setWindowTitle(tr("Ignore List")) self.resize(540, 330) self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.tableView = QTableView() self.tableView.setEditTriggers(QAbstractItemView.NoEditTriggers) self.tableView.setSelectionMode(QAbstractItemView.ExtendedSelection) self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) self.tableView.setShowGrid(False) self.tableView.horizontalHeader().setStretchLastSection(True) self.tableView.verticalHeader().setDefaultSectionSize(18) self.tableView.verticalHeader().setHighlightSections(False) self.tableView.verticalHeader().setVisible(False) self.tableView.setWordWrap(False) self.verticalLayout.addWidget(self.tableView) self.removeSelectedButton = QPushButton(tr("Remove Selected")) self.clearButton = QPushButton(tr("Clear")) self.closeButton = QPushButton(tr("Close")) self.verticalLayout.addLayout( horizontal_wrap([self.removeSelectedButton, self.clearButton, None, self.closeButton]) ) # --- model --> view def show(self): super().show() dupeguru-4.3.1/qt/ignore_list_table.py000066400000000000000000000010211426171743600200470ustar00rootroot00000000000000# Created On: 2012-03-13 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from qt.column import Column from qt.table import Table class IgnoreListTable(Table): """Ignore list model""" COLUMNS = [ Column("path1", default_width=230), Column("path2", default_width=230), ] dupeguru-4.3.1/qt/me/000077500000000000000000000000001426171743600144175ustar00rootroot00000000000000dupeguru-4.3.1/qt/me/__init__.py000066400000000000000000000000001426171743600165160ustar00rootroot00000000000000dupeguru-4.3.1/qt/me/details_dialog.py000066400000000000000000000016651426171743600177450ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import QSize from PyQt5.QtWidgets import QAbstractItemView from hscommon.trans import trget from qt.details_dialog import DetailsDialog as DetailsDialogBase from qt.details_table import DetailsTable tr = trget("ui") class DetailsDialog(DetailsDialogBase): def _setupUi(self): self.setWindowTitle(tr("Details")) self.resize(502, 295) self.setMinimumSize(QSize(250, 250)) self.tableView = DetailsTable(self) self.tableView.setAlternatingRowColors(True) self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) self.tableView.setShowGrid(False) self.setWidget(self.tableView) dupeguru-4.3.1/qt/me/preferences_dialog.py000066400000000000000000000117711426171743600206200ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import QSize from PyQt5.QtWidgets import ( QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget, ) from hscommon.trans import trget from core.app import AppMode from core.scanner import ScanType from qt.preferences_dialog import PreferencesDialogBase tr = trget("ui") class PreferencesDialog(PreferencesDialogBase): def _setupPreferenceWidgets(self): self._setupFilterHardnessBox() self.widgetsVLayout.addLayout(self.filterHardnessHLayout) self.widget = QWidget(self) self.widget.setMinimumSize(QSize(0, 40)) self.verticalLayout_4 = QVBoxLayout(self.widget) self.verticalLayout_4.setSpacing(0) self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) self.label_6 = QLabel(self.widget) self.label_6.setText(tr("Tags to scan:")) self.verticalLayout_4.addWidget(self.label_6) self.horizontalLayout_2 = QHBoxLayout() self.horizontalLayout_2.setSpacing(0) spacer_item = QSpacerItem(15, 20, QSizePolicy.Fixed, QSizePolicy.Minimum) self.horizontalLayout_2.addItem(spacer_item) self._setupAddCheckbox("tagTrackBox", tr("Track"), self.widget) self.horizontalLayout_2.addWidget(self.tagTrackBox) self._setupAddCheckbox("tagArtistBox", tr("Artist"), self.widget) self.horizontalLayout_2.addWidget(self.tagArtistBox) self._setupAddCheckbox("tagAlbumBox", tr("Album"), self.widget) self.horizontalLayout_2.addWidget(self.tagAlbumBox) self._setupAddCheckbox("tagTitleBox", tr("Title"), self.widget) self.horizontalLayout_2.addWidget(self.tagTitleBox) self._setupAddCheckbox("tagGenreBox", tr("Genre"), self.widget) self.horizontalLayout_2.addWidget(self.tagGenreBox) self._setupAddCheckbox("tagYearBox", tr("Year"), self.widget) self.horizontalLayout_2.addWidget(self.tagYearBox) self.verticalLayout_4.addLayout(self.horizontalLayout_2) self.widgetsVLayout.addWidget(self.widget) self._setupAddCheckbox("wordWeightingBox", tr("Word weighting")) self.widgetsVLayout.addWidget(self.wordWeightingBox) self._setupAddCheckbox("matchSimilarBox", tr("Match similar words")) self.widgetsVLayout.addWidget(self.matchSimilarBox) self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind")) self.widgetsVLayout.addWidget(self.mixFileKindBox) self._setupAddCheckbox("useRegexpBox", tr("Use regular expressions when filtering")) self.widgetsVLayout.addWidget(self.useRegexpBox) self._setupAddCheckbox("removeEmptyFoldersBox", tr("Remove empty folders on delete or move")) self.widgetsVLayout.addWidget(self.removeEmptyFoldersBox) self._setupAddCheckbox( "ignoreHardlinkMatches", tr("Ignore duplicates hardlinking to the same file"), ) self.widgetsVLayout.addWidget(self.ignoreHardlinkMatches) self._setupBottomPart() def _load(self, prefs, setchecked, section): setchecked(self.tagTrackBox, prefs.scan_tag_track) setchecked(self.tagArtistBox, prefs.scan_tag_artist) setchecked(self.tagAlbumBox, prefs.scan_tag_album) setchecked(self.tagTitleBox, prefs.scan_tag_title) setchecked(self.tagGenreBox, prefs.scan_tag_genre) setchecked(self.tagYearBox, prefs.scan_tag_year) setchecked(self.matchSimilarBox, prefs.match_similar) setchecked(self.wordWeightingBox, prefs.word_weighting) # Update UI state based on selected scan type scan_type = prefs.get_scan_type(AppMode.MUSIC) word_based = scan_type in ( ScanType.FILENAME, ScanType.FIELDS, ScanType.FIELDSNOORDER, ScanType.TAG, ) tag_based = scan_type == ScanType.TAG self.filterHardnessSlider.setEnabled(word_based) self.matchSimilarBox.setEnabled(word_based) self.wordWeightingBox.setEnabled(word_based) self.tagTrackBox.setEnabled(tag_based) self.tagArtistBox.setEnabled(tag_based) self.tagAlbumBox.setEnabled(tag_based) self.tagTitleBox.setEnabled(tag_based) self.tagGenreBox.setEnabled(tag_based) self.tagYearBox.setEnabled(tag_based) def _save(self, prefs, ischecked): prefs.scan_tag_track = ischecked(self.tagTrackBox) prefs.scan_tag_artist = ischecked(self.tagArtistBox) prefs.scan_tag_album = ischecked(self.tagAlbumBox) prefs.scan_tag_title = ischecked(self.tagTitleBox) prefs.scan_tag_genre = ischecked(self.tagGenreBox) prefs.scan_tag_year = ischecked(self.tagYearBox) prefs.match_similar = ischecked(self.matchSimilarBox) prefs.word_weighting = ischecked(self.wordWeightingBox) dupeguru-4.3.1/qt/me/results_model.py000066400000000000000000000024061426171743600176540ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from qt.column import Column from qt.results_model import ResultsModel as ResultsModelBase class ResultsModel(ResultsModelBase): COLUMNS = [ Column("marked", default_width=30), Column("name", default_width=200), Column("folder_path", default_width=180), Column("size", default_width=60), Column("duration", default_width=60), Column("bitrate", default_width=50), Column("samplerate", default_width=60), Column("extension", default_width=40), Column("mtime", default_width=120), Column("title", default_width=120), Column("artist", default_width=120), Column("album", default_width=120), Column("genre", default_width=80), Column("year", default_width=40), Column("track", default_width=40), Column("comment", default_width=120), Column("percentage", default_width=60), Column("words", default_width=120), Column("dupe_count", default_width=80), ] dupeguru-4.3.1/qt/pe/000077500000000000000000000000001426171743600144225ustar00rootroot00000000000000dupeguru-4.3.1/qt/pe/__init__.py000066400000000000000000000000001426171743600165210ustar00rootroot00000000000000dupeguru-4.3.1/qt/pe/block.py000066400000000000000000000030321426171743600160640ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2009-05-10 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from qt.pe._block_qt import getblocks # NOQA # Converted to C # def getblock(image): # width = image.width() # height = image.height() # if width: # pixel_count = width * height # red = green = blue = 0 # s = image.bits().asstring(image.numBytes()) # for i in xrange(pixel_count): # offset = i * 3 # red += ord(s[offset]) # green += ord(s[offset + 1]) # blue += ord(s[offset + 2]) # return (red // pixel_count, green // pixel_count, blue // pixel_count) # else: # return (0, 0, 0) # # def getblocks(image, block_count_per_side): # width = image.width() # height = image.height() # if not width: # return [] # block_width = max(width // block_count_per_side, 1) # block_height = max(height // block_count_per_side, 1) # result = [] # for ih in xrange(block_count_per_side): # top = min(ih * block_height, height - block_height) # for iw in range(block_count_per_side): # left = min(iw * block_width, width - block_width) # crop = image.copy(left, top, block_width, block_height) # result.append(getblock(crop)) # return result dupeguru-4.3.1/qt/pe/block.pyi000066400000000000000000000003701426171743600162370ustar00rootroot00000000000000from typing import Tuple, List, Union from PyQt5.QtGui import QImage _block = Tuple[int, int, int] def getblock(image: QImage) -> _block: ... # noqa: E302 def getblocks(image: QImage, block_count_per_side: int) -> Union[List[_block], None]: ... dupeguru-4.3.1/qt/pe/details_dialog.py000066400000000000000000000141201426171743600177360ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import QAbstractItemView, QSizePolicy, QGridLayout, QSplitter, QFrame from PyQt5.QtGui import QResizeEvent from hscommon.trans import trget from qt.details_dialog import DetailsDialog as DetailsDialogBase from qt.details_table import DetailsTable from qt.pe.image_viewer import ViewerToolBar, ScrollAreaImageViewer, ScrollAreaController tr = trget("ui") class DetailsDialog(DetailsDialogBase): def __init__(self, parent, app): self.vController = None super().__init__(parent, app) def _setupUi(self): self.setWindowTitle(tr("Details")) self.resize(502, 502) self.setMinimumSize(QSize(250, 250)) self.splitter = QSplitter(Qt.Vertical) self.topFrame = EmittingFrame() self.topFrame.setFrameShape(QFrame.StyledPanel) self.horizontalLayout = QGridLayout() # Minimum width for the toolbar in the middle: self.horizontalLayout.setColumnMinimumWidth(1, 10) self.horizontalLayout.setContentsMargins(0, 0, 0, 0) self.horizontalLayout.setColumnStretch(0, 32) # Smaller value for the toolbar in the middle to avoid excessive resize self.horizontalLayout.setColumnStretch(1, 2) self.horizontalLayout.setColumnStretch(2, 32) # This avoids toolbar getting incorrectly partially hidden when window resizes self.horizontalLayout.setRowStretch(0, 1) self.horizontalLayout.setRowStretch(1, 24) self.horizontalLayout.setRowStretch(2, 1) self.horizontalLayout.setSpacing(1) # probably not important self.selectedImageViewer = ScrollAreaImageViewer(self, "selectedImage") self.horizontalLayout.addWidget(self.selectedImageViewer, 0, 0, 3, 1) # Use a specific type of controller depending on the underlying viewer type self.vController = ScrollAreaController(self) self.verticalToolBar = ViewerToolBar(self, self.vController) self.verticalToolBar.setOrientation(Qt.Orientation(Qt.Vertical)) self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter) self.referenceImageViewer = ScrollAreaImageViewer(self, "referenceImage") self.horizontalLayout.addWidget(self.referenceImageViewer, 0, 2, 3, 1) self.topFrame.setLayout(self.horizontalLayout) self.splitter.addWidget(self.topFrame) self.splitter.setStretchFactor(0, 8) self.tableView = DetailsTable(self) size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) size_policy.setHorizontalStretch(0) size_policy.setVerticalStretch(0) self.tableView.setSizePolicy(size_policy) self.tableView.setAlternatingRowColors(True) self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) self.tableView.setShowGrid(False) self.splitter.addWidget(self.tableView) self.splitter.setStretchFactor(1, 1) # Late population needed here for connections to the toolbar self.vController.setupViewers(self.selectedImageViewer, self.referenceImageViewer) # self.setCentralWidget(self.splitter) # only as QMainWindow self.setWidget(self.splitter) # only as QDockWidget self.topFrame.resized.connect(self.resizeEvent) def _update(self): if self.vController is None: # Not yet constructed! return if not self.app.model.selected_dupes: # No item from the model, disable and clear everything. self.vController.resetViewersState() return dupe = self.app.model.selected_dupes[0] group = self.app.model.results.get_group_of_duplicate(dupe) ref = group.ref self.vController.updateView(ref, dupe, group) # --- Override @pyqtSlot(QResizeEvent) def resizeEvent(self, event): self.ensure_same_sizes() if self.vController is None or not self.vController.bestFit: return # Only update the scaled down pixmaps self.vController.updateBothImages() def show(self): # Give the splitter a maximum height to reach. This is assuming that # all rows below their headers have the same height self.tableView.setMaximumHeight( self.tableView.rowHeight(1) * self.tableModel.model.row_count() + self.tableView.verticalHeader().sectionSize(0) # looks like the handle is taken into account by the splitter + self.splitter.handle(1).size().height() ) DetailsDialogBase.show(self) self.ensure_same_sizes() self._update() def ensure_same_sizes(self): # HACK This ensures same size while shrinking. # ReferenceViewer might be 1 pixel shorter in width # due to the toolbar in the middle keeping the same width, # so resizing in the GridLayout's engine leads to not enough space # left for the panel on the right. # This work as a QMainWindow, but doesn't work as a QDockWidget: # resize can only grow. Might need some custom sizeHint somewhere... # self.horizontalLayout.setColumnMinimumWidth( # 0, self.selectedImageViewer.size().width()) # self.horizontalLayout.setColumnMinimumWidth( # 2, self.selectedImageViewer.size().width()) # This works when expanding but it's ugly: if self.selectedImageViewer.size().width() > self.referenceImageViewer.size().width(): self.selectedImageViewer.resize(self.referenceImageViewer.size()) # model --> view def refresh(self): DetailsDialogBase.refresh(self) if self.isVisible(): self._update() class EmittingFrame(QFrame): """Emits a signal whenever is resized""" resized = pyqtSignal(QResizeEvent) def resizeEvent(self, event): self.resized.emit(event) dupeguru-4.3.1/qt/pe/image_viewer.py000066400000000000000000001432651426171743600174520ustar00rootroot00000000000000# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import QObject, Qt, QSize, QRectF, QPointF, QPoint, pyqtSlot, pyqtSignal, QEvent from PyQt5.QtGui import QPixmap, QPainter, QPalette, QCursor, QIcon, QKeySequence from PyQt5.QtWidgets import ( QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QToolBar, QToolButton, QAction, QWidget, QScrollArea, QApplication, QAbstractScrollArea, QStyle, ) from hscommon.trans import trget from hscommon.plat import ISLINUX tr = trget("ui") MAX_SCALE = 12.0 MIN_SCALE = 0.1 def create_actions(actions, target): # actions are list of (name, shortcut, icon, desc, func) for name, shortcut, icon, desc, func in actions: action = QAction(target) if icon: action.setIcon(icon) if shortcut: action.setShortcut(shortcut) action.setText(desc) action.triggered.connect(func) setattr(target, name, action) class ViewerToolBar(QToolBar): def __init__(self, parent, controller): super().__init__(parent) self.parent = parent self.controller = controller self.setupActions(controller) self.createButtons() self.buttonImgSwap.setEnabled(False) self.buttonZoomIn.setEnabled(False) self.buttonZoomOut.setEnabled(False) self.buttonNormalSize.setEnabled(False) self.buttonBestFit.setEnabled(False) def setupActions(self, controller): # actions are list of (name, shortcut, icon, desc, func) ACTIONS = [ ( "actionZoomIn", QKeySequence.ZoomIn, QIcon.fromTheme("zoom-in") if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons else QIcon(QPixmap(":/" + "zoom_in")), tr("Increase zoom"), controller.zoomIn, ), ( "actionZoomOut", QKeySequence.ZoomOut, QIcon.fromTheme("zoom-out") if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons else QIcon(QPixmap(":/" + "zoom_out")), tr("Decrease zoom"), controller.zoomOut, ), ( "actionNormalSize", tr("Ctrl+/"), QIcon.fromTheme("zoom-original") if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons else QIcon(QPixmap(":/" + "zoom_original")), tr("Normal size"), controller.zoomNormalSize, ), ( "actionBestFit", tr("Ctrl+*"), QIcon.fromTheme("zoom-best-fit") if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons else QIcon(QPixmap(":/" + "zoom_best_fit")), tr("Best fit"), controller.zoomBestFit, ), ] # TODO try with QWidgetAction() instead in order to have # the popup menu work in the toolbar (if resized below minimum height) create_actions(ACTIONS, self) def createButtons(self): self.buttonImgSwap = QToolButton(self) self.buttonImgSwap.setToolButtonStyle(Qt.ToolButtonIconOnly) self.buttonImgSwap.setIcon( QIcon.fromTheme("view-refresh", self.style().standardIcon(QStyle.SP_BrowserReload)) if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons else QIcon(QPixmap(":/" + "exchange")) ) self.buttonImgSwap.setText("Swap images") self.buttonImgSwap.setToolTip("Swap images") self.buttonImgSwap.pressed.connect(self.controller.swapImages) self.buttonImgSwap.released.connect(self.controller.swapImages) self.buttonZoomIn = QToolButton(self) self.buttonZoomIn.setToolButtonStyle(Qt.ToolButtonIconOnly) self.buttonZoomIn.setDefaultAction(self.actionZoomIn) self.buttonZoomIn.setEnabled(False) self.buttonZoomOut = QToolButton(self) self.buttonZoomOut.setToolButtonStyle(Qt.ToolButtonIconOnly) self.buttonZoomOut.setDefaultAction(self.actionZoomOut) self.buttonZoomOut.setEnabled(False) self.buttonNormalSize = QToolButton(self) self.buttonNormalSize.setToolButtonStyle(Qt.ToolButtonIconOnly) self.buttonNormalSize.setDefaultAction(self.actionNormalSize) self.buttonNormalSize.setEnabled(True) self.buttonBestFit = QToolButton(self) self.buttonBestFit.setToolButtonStyle(Qt.ToolButtonIconOnly) self.buttonBestFit.setDefaultAction(self.actionBestFit) self.buttonBestFit.setEnabled(False) self.addWidget(self.buttonImgSwap) self.addWidget(self.buttonZoomIn) self.addWidget(self.buttonZoomOut) self.addWidget(self.buttonNormalSize) self.addWidget(self.buttonBestFit) class BaseController(QObject): """Abstract Base class. Singleton. Base proxy interface to keep image viewers synchronized. Relays function calls, keep tracks of things.""" def __init__(self, parent): super().__init__() self.selectedViewer = None self.referenceViewer = None # cached pixmaps self.selectedPixmap = QPixmap() self.referencePixmap = QPixmap() self.scaledSelectedPixmap = QPixmap() self.scaledReferencePixmap = QPixmap() self.current_scale = 1.0 self.bestFit = True self.parent = parent # To change buttons' states self.cached_group = None self.same_dimensions = True def setupViewers(self, selected_viewer, reference_viewer): self.selectedViewer = selected_viewer self.referenceViewer = reference_viewer self.selectedViewer.controller = self self.referenceViewer.controller = self self._setupConnections() def _setupConnections(self): self.selectedViewer.connectMouseSignals() self.referenceViewer.connectMouseSignals() def updateView(self, ref, dupe, group): # To keep current scale accross dupes from the same group previous_same_dimensions = self.same_dimensions self.same_dimensions = True same_group = True if group != self.cached_group: same_group = False self.resetState() self.cached_group = group self.selectedPixmap = QPixmap(str(dupe.path)) if ref is dupe: # currently selected file is the actual reference file self.referencePixmap = QPixmap() self.scaledReferencePixmap = QPixmap() self.parent.verticalToolBar.buttonImgSwap.setEnabled(False) self.parent.verticalToolBar.buttonNormalSize.setEnabled(True) else: self.referencePixmap = QPixmap(str(ref.path)) self.parent.verticalToolBar.buttonImgSwap.setEnabled(True) if ref.dimensions != dupe.dimensions: self.same_dimensions = False self.parent.verticalToolBar.buttonNormalSize.setEnabled(True) self.updateButtonsAsPerDimensions(previous_same_dimensions) self.updateBothImages(same_group) self.centerViews(same_group and self.referencePixmap.isNull()) def updateBothImages(self, same_group=False): # WARNING this is called on every resize event, ignore_update = self.referencePixmap.isNull() if ignore_update: self.selectedViewer.ignore_signal = True # the SelectedImageViewer widget sometimes ends up being bigger # than the ReferenceImageViewer by one pixel, which distorts the # scaled down pixmap for the reference, hence we'll reuse its size here. self._updateImage(self.selectedPixmap, self.selectedViewer, same_group) self._updateImage(self.referencePixmap, self.referenceViewer, same_group) if ignore_update: self.selectedViewer.ignore_signal = False def _updateImage(self, pixmap, viewer, same_group=False): # WARNING this is called on every resize event, might need to split # into a separate function depending on the implementation used if pixmap.isNull(): # This should disable the blank widget viewer.setImage(pixmap) return target_size = viewer.size() if not viewer.bestFit: if same_group: viewer.setImage(pixmap) return target_size # zoomed in state, expand # only if not same_group, we need full update scaledpixmap = pixmap.scaled(target_size, Qt.KeepAspectRatioByExpanding, Qt.FastTransformation) else: # best fit, keep ratio always scaledpixmap = pixmap.scaled(target_size, Qt.KeepAspectRatio, Qt.FastTransformation) viewer.setImage(scaledpixmap) return target_size def resetState(self): """Only called when the group of dupes has changed. We reset our controller internal state and buttons, center view on viewers.""" self.selectedPixmap = QPixmap() self.scaledSelectedPixmap = QPixmap() self.referencePixmap = QPixmap() self.scaledReferencePixmap = QPixmap() self.setBestFit(True) self.current_scale = 1.0 self.selectedViewer.current_scale = 1.0 self.referenceViewer.current_scale = 1.0 self.selectedViewer.resetCenter() self.referenceViewer.resetCenter() self.selectedViewer.scaleAt(1.0) self.referenceViewer.scaleAt(1.0) self.centerViews() self.parent.verticalToolBar.buttonZoomIn.setEnabled(False) self.parent.verticalToolBar.buttonZoomOut.setEnabled(False) self.parent.verticalToolBar.buttonNormalSize.setEnabled(True) self.parent.verticalToolBar.buttonBestFit.setEnabled(False) # active mode by default def resetViewersState(self): """No item from the model, disable and clear everything.""" # only called by the details dialog self.selectedPixmap = QPixmap() self.scaledSelectedPixmap = QPixmap() self.referencePixmap = QPixmap() self.scaledReferencePixmap = QPixmap() self.setBestFit(True) self.current_scale = 1.0 self.selectedViewer.current_scale = 1.0 self.referenceViewer.current_scale = 1.0 self.selectedViewer.resetCenter() self.referenceViewer.resetCenter() self.selectedViewer.scaleAt(1.0) self.referenceViewer.scaleAt(1.0) self.centerViews() self.parent.verticalToolBar.buttonImgSwap.setEnabled(False) self.parent.verticalToolBar.buttonZoomIn.setEnabled(False) self.parent.verticalToolBar.buttonZoomOut.setEnabled(False) self.parent.verticalToolBar.buttonNormalSize.setEnabled(False) self.parent.verticalToolBar.buttonBestFit.setEnabled(False) # active mode by default self.selectedViewer.setImage(self.selectedPixmap) # null self.selectedViewer.setEnabled(False) self.referenceViewer.setImage(self.referencePixmap) # null self.referenceViewer.setEnabled(False) @pyqtSlot() def zoomIn(self): self.scaleImagesBy(1.25) @pyqtSlot() def zoomOut(self): self.scaleImagesBy(0.8) @pyqtSlot(float) def scaleImagesBy(self, factor): """Compute new scale from factor and scale.""" self.current_scale *= factor self.selectedViewer.scaleBy(factor) self.referenceViewer.scaleBy(factor) self.updateButtons() @pyqtSlot(float) def scaleImagesAt(self, scale): """Scale at a pre-computed scale.""" self.current_scale = scale self.selectedViewer.scaleAt(scale) self.referenceViewer.scaleAt(scale) self.updateButtons() def updateButtons(self): self.parent.verticalToolBar.buttonZoomIn.setEnabled(self.current_scale < MAX_SCALE) self.parent.verticalToolBar.buttonZoomOut.setEnabled(self.current_scale > MIN_SCALE) self.parent.verticalToolBar.buttonNormalSize.setEnabled(round(self.current_scale, 1) != 1.0) self.parent.verticalToolBar.buttonBestFit.setEnabled(self.bestFit is False) def updateButtonsAsPerDimensions(self, previous_same_dimensions): if not self.same_dimensions: self.parent.verticalToolBar.buttonZoomIn.setEnabled(False) self.parent.verticalToolBar.buttonZoomOut.setEnabled(False) if not self.bestFit: self.zoomBestFit() self.parent.verticalToolBar.buttonNormalSize.setEnabled(True) if not self.referencePixmap.isNull(): self.parent.verticalToolBar.buttonImgSwap.setEnabled(True) return if not self.bestFit and not previous_same_dimensions: self.zoomBestFit() self.parent.verticalToolBar.buttonNormalSize.setEnabled(True) if self.referencePixmap.isNull(): self.parent.verticalToolBar.buttonImgSwap.setEnabled(False) @pyqtSlot() def zoomBestFit(self): """Setup before scaling to bestfit""" self.setBestFit(True) self.current_scale = 1.0 self.selectedViewer.current_scale = 1.0 self.referenceViewer.current_scale = 1.0 self.selectedViewer.scaleAt(1.0) self.referenceViewer.scaleAt(1.0) self.selectedViewer.resetCenter() self.referenceViewer.resetCenter() self._updateImage(self.selectedPixmap, self.selectedViewer, True) self._updateImage(self.referencePixmap, self.referenceViewer, True) self.centerViews() self.parent.verticalToolBar.buttonZoomIn.setEnabled(False) self.parent.verticalToolBar.buttonZoomOut.setEnabled(False) self.parent.verticalToolBar.buttonNormalSize.setEnabled(True) self.parent.verticalToolBar.buttonBestFit.setEnabled(False) self.parent.verticalToolBar.buttonImgSwap.setEnabled(True) def setBestFit(self, value): self.bestFit = value self.selectedViewer.bestFit = value self.referenceViewer.bestFit = value @pyqtSlot() def zoomNormalSize(self): self.setBestFit(False) self.current_scale = 1.0 self.selectedViewer.setImage(self.selectedPixmap) self.referenceViewer.setImage(self.referencePixmap) self.centerViews() self.selectedViewer.scaleToNormalSize() self.referenceViewer.scaleToNormalSize() if self.same_dimensions: self.parent.verticalToolBar.buttonZoomIn.setEnabled(True) self.parent.verticalToolBar.buttonZoomOut.setEnabled(True) else: # we can't allow swapping pixmaps of different dimensions self.parent.verticalToolBar.buttonImgSwap.setEnabled(False) self.parent.verticalToolBar.buttonNormalSize.setEnabled(False) self.parent.verticalToolBar.buttonBestFit.setEnabled(True) def centerViews(self, only_selected=False): self.selectedViewer.centerViewAndUpdate() if only_selected: return self.referenceViewer.centerViewAndUpdate() @pyqtSlot() def swapImages(self): # swap the columns in the details table as well self.parent.tableView.horizontalHeader().swapSections(0, 1) class QWidgetController(BaseController): """Specialized version for QWidget-based viewers.""" def __init__(self, parent): super().__init__(parent) def _updateImage(self, *args): ret = super()._updateImage(*args) # Fix alignment when resizing window self.centerViews() return ret @pyqtSlot(QPointF) def onDraggedMouse(self, delta): if not self.same_dimensions: return if self.sender() is self.referenceViewer: self.selectedViewer.onDraggedMouse(delta) else: self.referenceViewer.onDraggedMouse(delta) @pyqtSlot() def swapImages(self): self.selectedViewer._pixmap.swap(self.referenceViewer._pixmap) self.selectedViewer.centerViewAndUpdate() self.referenceViewer.centerViewAndUpdate() super().swapImages() class ScrollAreaController(BaseController): """Specialized version fro QLabel-based viewers.""" def __init__(self, parent): super().__init__(parent) def _setupConnections(self): super()._setupConnections() self.selectedViewer.connectScrollBars() self.referenceViewer.connectScrollBars() def updateBothImages(self, same_group=False): super().updateBothImages(same_group) if not self.referenceViewer.isEnabled(): return self.referenceViewer._horizontalScrollBar.setValue(self.selectedViewer._horizontalScrollBar.value()) self.referenceViewer._verticalScrollBar.setValue(self.selectedViewer._verticalScrollBar.value()) @pyqtSlot(QPoint) def onDraggedMouse(self, delta): self.selectedViewer.ignore_signal = True self.referenceViewer.ignore_signal = True if self.same_dimensions: self.selectedViewer.onDraggedMouse(delta) self.referenceViewer.onDraggedMouse(delta) else: if self.sender() is self.selectedViewer: self.selectedViewer.onDraggedMouse(delta) else: self.referenceViewer.onDraggedMouse(delta) self.selectedViewer.ignore_signal = False self.referenceViewer.ignore_signal = False @pyqtSlot() def swapImages(self): self.referenceViewer._pixmap.swap(self.selectedViewer._pixmap) self.referenceViewer.setCachedPixmap() self.selectedViewer.setCachedPixmap() super().swapImages() @pyqtSlot(float, QPointF) def onMouseWheel(self, scale, delta): self.scaleImagesAt(scale) self.selectedViewer.adjustScrollBarsScaled(delta) # Signal from scrollbars will automatically change the other: # self.referenceViewer.adjustScrollBarsScaled(delta) @pyqtSlot(int) def onVScrollBarChanged(self, value): if not self.same_dimensions: return if self.sender() is self.referenceViewer._verticalScrollBar: if not self.selectedViewer.ignore_signal: self.selectedViewer._verticalScrollBar.setValue(value) else: if not self.referenceViewer.ignore_signal: self.referenceViewer._verticalScrollBar.setValue(value) @pyqtSlot(int) def onHScrollBarChanged(self, value): if not self.same_dimensions: return if self.sender() is self.referenceViewer._horizontalScrollBar: if not self.selectedViewer.ignore_signal: self.selectedViewer._horizontalScrollBar.setValue(value) else: if not self.referenceViewer.ignore_signal: self.referenceViewer._horizontalScrollBar.setValue(value) @pyqtSlot(float) def scaleImagesBy(self, factor): super().scaleImagesBy(factor) # The other is automatically updated via sigals self.selectedViewer.adjustScrollBarsFactor(factor) @pyqtSlot() def zoomBestFit(self): # Disable scrollbars to avoid GridLayout size rounding glitch super().zoomBestFit() if self.referencePixmap.isNull(): self.parent.verticalToolBar.buttonImgSwap.setEnabled(False) self.selectedViewer.toggleScrollBars() self.referenceViewer.toggleScrollBars() class GraphicsViewController(BaseController): """Specialized version fro QGraphicsView-based viewers.""" def __init__(self, parent): super().__init__(parent) def _setupConnections(self): super()._setupConnections() self.selectedViewer.connectScrollBars() self.referenceViewer.connectScrollBars() # Special case for mouse wheel event conflicting with scrollbar adjustments self.selectedViewer.other_viewer = self.referenceViewer self.referenceViewer.other_viewer = self.selectedViewer @pyqtSlot() def syncCenters(self): if self.sender() is self.referenceViewer: self.selectedViewer.setCenter(self.referenceViewer._centerPoint) else: self.referenceViewer.setCenter(self.selectedViewer._centerPoint) @pyqtSlot(float, QPointF) def onMouseWheel(self, factor, new_center): self.current_scale *= factor if self.sender() is self.referenceViewer: self.selectedViewer.scaleBy(factor) self.selectedViewer.setCenter(new_center) else: self.referenceViewer.scaleBy(factor) self.referenceViewer.setCenter(new_center) @pyqtSlot(int) def onVScrollBarChanged(self, value): if not self.same_dimensions: return if self.sender() is self.referenceViewer._verticalScrollBar: if not self.selectedViewer.ignore_signal: self.selectedViewer._verticalScrollBar.setValue(value) else: if not self.referenceViewer.ignore_signal: self.referenceViewer._verticalScrollBar.setValue(value) @pyqtSlot(int) def onHScrollBarChanged(self, value): if not self.same_dimensions: return if self.sender() is self.referenceViewer._horizontalScrollBar: if not self.selectedViewer.ignore_signal: self.selectedViewer._horizontalScrollBar.setValue(value) else: if not self.referenceViewer.ignore_signal: self.referenceViewer._horizontalScrollBar.setValue(value) @pyqtSlot() def swapImages(self): self.referenceViewer._pixmap.swap(self.selectedViewer._pixmap) self.referenceViewer.setCachedPixmap() self.selectedViewer.setCachedPixmap() super().swapImages() @pyqtSlot() def zoomBestFit(self): """Setup before scaling to bestfit""" self.setBestFit(True) self.current_scale = 1.0 self.selectedViewer.fitScale() self.referenceViewer.fitScale() self.parent.verticalToolBar.buttonBestFit.setEnabled(False) self.parent.verticalToolBar.buttonZoomOut.setEnabled(False) self.parent.verticalToolBar.buttonZoomIn.setEnabled(False) self.parent.verticalToolBar.buttonNormalSize.setEnabled(True) if not self.referencePixmap.isNull(): self.parent.verticalToolBar.buttonImgSwap.setEnabled(True) # else: # self.referenceViewer.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # self.referenceViewer.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) def updateView(self, ref, dupe, group): # Keep current scale accross dupes from the same group previous_same_dimensions = self.same_dimensions self.same_dimensions = True same_group = True if group != self.cached_group: same_group = False self.resetState() self.cached_group = group self.selectedPixmap = QPixmap(str(dupe.path)) if ref is dupe: # currently selected file is the actual reference file self.same_dimensions = False self.referencePixmap = QPixmap() self.parent.verticalToolBar.buttonImgSwap.setEnabled(False) self.parent.verticalToolBar.buttonNormalSize.setEnabled(True) else: self.referencePixmap = QPixmap(str(ref.path)) self.parent.verticalToolBar.buttonImgSwap.setEnabled(True) if ref.dimensions != dupe.dimensions: self.same_dimensions = False self.parent.verticalToolBar.buttonNormalSize.setEnabled(True) self.updateButtonsAsPerDimensions(previous_same_dimensions) self.updateBothImages(same_group) def updateBothImages(self, same_group=False): """This is called only during resize events and while bestFit.""" ignore_update = self.referencePixmap.isNull() if ignore_update: self.selectedViewer.ignore_signal = True self._updateFitImage(self.selectedPixmap, self.selectedViewer) self._updateFitImage(self.referencePixmap, self.referenceViewer) if ignore_update: self.selectedViewer.ignore_signal = False def _updateFitImage(self, pixmap, viewer): # If not same_group, we need full update""" viewer.setImage(pixmap) if pixmap.isNull(): return if viewer.bestFit: viewer.fitScale() def resetState(self): """Only called when the group of dupes has changed. We reset our controller internal state and buttons, center view on viewers.""" self.selectedPixmap = QPixmap() self.referencePixmap = QPixmap() self.setBestFit(True) self.current_scale = 1.0 self.selectedViewer.current_scale = 1.0 self.referenceViewer.current_scale = 1.0 self.selectedViewer.resetCenter() self.referenceViewer.resetCenter() self.selectedViewer.fitScale() self.referenceViewer.fitScale() # self.centerViews() self.parent.verticalToolBar.buttonZoomIn.setEnabled(False) self.parent.verticalToolBar.buttonZoomOut.setEnabled(False) self.parent.verticalToolBar.buttonBestFit.setEnabled(False) self.parent.verticalToolBar.buttonNormalSize.setEnabled(True) def resetViewersState(self): """No item from the model, disable and clear everything.""" # only called by the details dialog self.selectedPixmap = QPixmap() self.scaledSelectedPixmap = QPixmap() self.referencePixmap = QPixmap() self.scaledReferencePixmap = QPixmap() self.setBestFit(True) self.current_scale = 1.0 self.selectedViewer.current_scale = 1.0 self.referenceViewer.current_scale = 1.0 self.selectedViewer.resetCenter() self.referenceViewer.resetCenter() # self.centerViews() self.parent.verticalToolBar.buttonZoomIn.setEnabled(False) self.parent.verticalToolBar.buttonZoomOut.setEnabled(False) self.parent.verticalToolBar.buttonBestFit.setEnabled(False) self.parent.verticalToolBar.buttonImgSwap.setEnabled(False) self.parent.verticalToolBar.buttonNormalSize.setEnabled(False) self.selectedViewer.setImage(self.selectedPixmap) # null self.selectedViewer.setEnabled(False) self.referenceViewer.setImage(self.referencePixmap) # null self.referenceViewer.setEnabled(False) @pyqtSlot(float) def scaleImagesBy(self, factor): self.selectedViewer.updateCenterPoint() self.referenceViewer.updateCenterPoint() super().scaleImagesBy(factor) self.selectedViewer.centerOn(self.selectedViewer._centerPoint) # Scrollbars sync themselves here class QWidgetImageViewer(QWidget): """Use a QPixmap, but no scrollbars and no keyboard key sequence for navigation.""" # FIXME: panning while zoomed-in is broken (due to delta not interpolated right? mouseDragged = pyqtSignal(QPointF) mouseWheeled = pyqtSignal(float) def __init__(self, parent, name=""): super().__init__(parent) self._app = QApplication self._pixmap = QPixmap() self._rect = QRectF() self._lastMouseClickPoint = QPointF() self._mousePanningDelta = QPointF() self.current_scale = 1.0 self._drag = False self._dragConnection = None self._wheelConnection = None self._instance_name = name self.bestFit = True self.controller = None self.setMouseTracking(False) def __repr__(self): return f"{self._instance_name}" def connectMouseSignals(self): if not self._dragConnection: self._dragConnection = self.mouseDragged.connect(self.controller.onDraggedMouse) if not self._wheelConnection: self._wheelConnection = self.mouseWheeled.connect(self.controller.scaleImagesBy) def disconnectMouseSignals(self): if self._dragConnection: self.mouseDragged.disconnect() self._dragConnection = None if self._wheelConnection: self.mouseWheeled.disconnect() self._wheelConnection = None def paintEvent(self, event): painter = QPainter(self) painter.translate(self.rect().center()) painter.scale(self.current_scale, self.current_scale) painter.translate(self._mousePanningDelta) painter.drawPixmap(self._rect.topLeft(), self._pixmap) def resetCenter(self): """Resets origin""" # Make sure we are not still panning around self._mousePanningDelta = QPointF() self.update() def changeEvent(self, event): if event.type() == QEvent.EnabledChange: if self.isEnabled(): self.connectMouseSignals() return self.disconnectMouseSignals() def contextMenuEvent(self, event): """Block parent's (main window) context menu on right click.""" event.accept() def mousePressEvent(self, event): if self.bestFit or not self.isEnabled(): event.ignore() return if event.button() & (Qt.LeftButton | Qt.MidButton | Qt.RightButton): self._drag = True else: self._drag = False event.ignore() return self._lastMouseClickPoint = event.pos() self._app.setOverrideCursor(Qt.ClosedHandCursor) self.setMouseTracking(True) event.accept() def mouseMoveEvent(self, event): if self.bestFit or not self.isEnabled(): event.ignore() return self._mousePanningDelta += (event.pos() - self._lastMouseClickPoint) * 1.0 / self.current_scale self._lastMouseClickPoint = event.pos() if self._drag: self.mouseDragged.emit(self._mousePanningDelta) self.update() def mouseReleaseEvent(self, event): if self.bestFit or not self.isEnabled(): event.ignore() return # if event.button() == Qt.LeftButton: self._drag = False self._app.restoreOverrideCursor() self.setMouseTracking(False) def wheelEvent(self, event): if self.bestFit or not self.controller.same_dimensions or not self.isEnabled(): event.ignore() return if event.angleDelta().y() > 0: if self.current_scale > MAX_SCALE: return self.mouseWheeled.emit(1.25) # zoom-in else: if self.current_scale < MIN_SCALE: return self.mouseWheeled.emit(0.8) # zoom-out def setImage(self, pixmap): if pixmap.isNull(): if not self._pixmap.isNull(): self._pixmap = pixmap self.disconnectMouseSignals() self.setEnabled(False) self.update() return elif not self.isEnabled(): self.setEnabled(True) self.connectMouseSignals() self._pixmap = pixmap def centerViewAndUpdate(self): self._rect = self._pixmap.rect() self._rect.translate(-self._rect.center()) self.update() def shouldBeActive(self): return True if not self.pixmap.isNull() else False def scaleBy(self, factor): self.current_scale *= factor self.update() def scaleAt(self, scale): self.current_scale = scale self.update() def sizeHint(self): return QSize(400, 400) @pyqtSlot() def scaleToNormalSize(self): """Called when the pixmap is set back to original size.""" self.current_scale = 1.0 self.update() @pyqtSlot(QPointF) def onDraggedMouse(self, delta): self._mousePanningDelta = delta self.update() class ScalablePixmap(QWidget): """Container for a pixmap that scales up very fast, used in ScrollAreaImageViewer.""" def __init__(self, parent): super().__init__(parent) self._pixmap = QPixmap() self.current_scale = 1.0 def paintEvent(self, event): painter = QPainter(self) painter.scale(self.current_scale, self.current_scale) # painter.drawPixmap(self.rect().topLeft(), self._pixmap) # should be the same as: painter.drawPixmap(0, 0, self._pixmap) def sizeHint(self): return self._pixmap.size() * self.current_scale def minimumSizeHint(self): return self.sizeHint() class ScrollAreaImageViewer(QScrollArea): """Implementation using a pixmap container in a simple scroll area.""" mouseDragged = pyqtSignal(QPoint) mouseWheeled = pyqtSignal(float, QPointF) def __init__(self, parent, name=""): super().__init__(parent) self._parent = parent self._app = QApplication self._pixmap = QPixmap() self._scaledpixmap = None self._rect = QRectF() self._lastMouseClickPoint = QPointF() self._mousePanningDelta = QPoint() self.current_scale = 1.0 self._drag = False self._dragConnection = None self._wheelConnection = None self._instance_name = name self.prefs = parent.app.prefs self.bestFit = True self.controller = None self.label = ScalablePixmap(self) # This is to avoid sending signals twice on scrollbar updates self.ignore_signal = False self.setBackgroundRole(QPalette.Dark) self.setWidgetResizable(False) self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents) self.setAlignment(Qt.AlignCenter) self._verticalScrollBar = self.verticalScrollBar() self._horizontalScrollBar = self.horizontalScrollBar() if self.prefs.details_dialog_viewers_show_scrollbars: self.toggleScrollBars() else: self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setWidget(self.label) self.setVisible(True) def __repr__(self): return f"{self._instance_name}" def toggleScrollBars(self, force_on=False): if not self.prefs.details_dialog_viewers_show_scrollbars: return # Ensure that it's off on the first run if self.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded: if force_on: return self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) else: self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) def connectMouseSignals(self): if not self._dragConnection: self._dragConnection = self.mouseDragged.connect(self.controller.onDraggedMouse) if not self._wheelConnection: self._wheelConnection = self.mouseWheeled.connect(self.controller.onMouseWheel) def disconnectMouseSignals(self): if self._dragConnection: self.mouseDragged.disconnect() self._dragConnection = None if self._wheelConnection: self.mouseWheeled.disconnect() self._wheelConnection = None def connectScrollBars(self): """Only call once controller is connected.""" # Cyclic connections are handled by Qt self._verticalScrollBar.valueChanged.connect(self.controller.onVScrollBarChanged, Qt.UniqueConnection) self._horizontalScrollBar.valueChanged.connect(self.controller.onHScrollBarChanged, Qt.UniqueConnection) def contextMenuEvent(self, event): """Block parent's (main window) context menu on right click.""" # Even though we don't have a context menu right now, and the default # contextMenuPolicy is DefaultContextMenu, we leverage that handler to # avoid raising the Result window's Actions menu event.accept() def mousePressEvent(self, event): if self.bestFit: event.ignore() return if event.button() & (Qt.LeftButton | Qt.MidButton | Qt.RightButton): self._drag = True else: self._drag = False event.ignore() return self._lastMouseClickPoint = event.pos() self._app.setOverrideCursor(Qt.ClosedHandCursor) self.setMouseTracking(True) super().mousePressEvent(event) def mouseMoveEvent(self, event): if self.bestFit: event.ignore() return if self._drag: delta = event.pos() - self._lastMouseClickPoint self._lastMouseClickPoint = event.pos() self.mouseDragged.emit(delta) super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): if self.bestFit: event.ignore() return self._drag = False self._app.restoreOverrideCursor() self.setMouseTracking(False) super().mouseReleaseEvent(event) def wheelEvent(self, event): if self.bestFit or not self.controller.same_dimensions: event.ignore() return old_scale = self.current_scale if event.angleDelta().y() > 0: # zoom-in if old_scale < MAX_SCALE: self.current_scale *= 1.25 else: if old_scale > MIN_SCALE: # zoom-out self.current_scale *= 0.8 if old_scale == self.current_scale: return delta_to_pos = (event.position() / old_scale) - (self.label.pos() / old_scale) delta = (delta_to_pos * self.current_scale) - (delta_to_pos * old_scale) self.mouseWheeled.emit(self.current_scale, delta) def setImage(self, pixmap): self._pixmap = pixmap self.label._pixmap = pixmap self.label.update() self.label.adjustSize() if pixmap.isNull(): self.setEnabled(False) self.disconnectMouseSignals() elif not self.isEnabled(): self.setEnabled(True) self.connectMouseSignals() def centerViewAndUpdate(self): self._rect = self.label.rect() self.label.rect().translate(-self._rect.center()) self.label.current_scale = self.current_scale self.label.update() # self.viewport().update() def setCachedPixmap(self): """In case we have changed the cached pixmap, reset it.""" self.label._pixmap = self._pixmap self.label.update() def shouldBeActive(self): return True if not self.pixmap.isNull() else False def scaleBy(self, factor): self.current_scale *= factor # factor has to be either 1.25 or 0.8 here self.label.resize(self.label.size().__imul__(factor)) self.label.current_scale = self.current_scale self.label.update() def scaleAt(self, scale): self.current_scale = scale self.label.resize(self._pixmap.size().__imul__(scale)) self.label.current_scale = scale self.label.update() # self.label.adjustSize() def adjustScrollBarsFactor(self, factor): """After scaling, no mouse position, default to center.""" # scrollBar.setMaximum(scrollBar.maximum() - scrollBar.minimum() + scrollBar.pageStep()) self._horizontalScrollBar.setValue( int(factor * self._horizontalScrollBar.value() + ((factor - 1) * self._horizontalScrollBar.pageStep() / 2)) ) self._verticalScrollBar.setValue( int(factor * self._verticalScrollBar.value() + ((factor - 1) * self._verticalScrollBar.pageStep() / 2)) ) def adjustScrollBarsScaled(self, delta): """After scaling with the mouse, update relative to mouse position.""" self._horizontalScrollBar.setValue(int(self._horizontalScrollBar.value() + delta.x())) self._verticalScrollBar.setValue(int(self._verticalScrollBar.value() + delta.y())) def adjustScrollBarsAuto(self): """After panning, update accordingly.""" self.horizontalScrollBar().setValue(int(self.horizontalScrollBar().value() - self._mousePanningDelta.x())) self.verticalScrollBar().setValue(int(self.verticalScrollBar().value() - self._mousePanningDelta.y())) def adjustScrollBarCentered(self): """Just center in the middle.""" self._horizontalScrollBar.setValue(int(self._horizontalScrollBar.maximum() / 2)) self._verticalScrollBar.setValue(int(self._verticalScrollBar.maximum() / 2)) def resetCenter(self): """Resets origin""" self._mousePanningDelta = QPoint() self.current_scale = 1.0 # self.scaleAt(1.0) def setCenter(self, point): self._lastMouseClickPoint = point def sizeHint(self): return self.viewport().rect().size() @pyqtSlot() def scaleToNormalSize(self): """Called when the pixmap is set back to original size.""" self.scaleAt(1.0) self.ensureWidgetVisible(self.label) # needed for centering self.toggleScrollBars(True) @pyqtSlot(QPoint) def onDraggedMouse(self, delta): """Update position from mouse delta sent by the other panel.""" self._mousePanningDelta = delta # Signal from scrollbars had already synced the values here self.adjustScrollBarsAuto() class GraphicsViewViewer(QGraphicsView): """Re-Implementation a full-fledged GraphicsView but is a bit buggy.""" mouseDragged = pyqtSignal() mouseWheeled = pyqtSignal(float, QPointF) def __init__(self, parent, name=""): super().__init__(parent) self._parent = parent self._app = QApplication self._pixmap = QPixmap() self._scaledpixmap = None self._rect = QRectF() self._lastMouseClickPoint = QPointF() self._mousePanningDelta = QPointF() self._scaleFactor = 1.3 self.zoomInFactor = self._scaleFactor self.zoomOutFactor = 1.0 / self._scaleFactor self.current_scale = 1.0 self._drag = False self._dragConnection = None self._wheelConnection = None self._instance_name = name self.prefs = parent.app.prefs self.bestFit = True self.controller = None self._centerPoint = QPointF() self.centerOn(self._centerPoint) self.other_viewer = None # specific to this class self._scene = QGraphicsScene() self._scene.setBackgroundBrush(Qt.black) self._item = QGraphicsPixmapItem() self.setScene(self._scene) self._scene.addItem(self._item) self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag) self._horizontalScrollBar = self.horizontalScrollBar() self._verticalScrollBar = self.verticalScrollBar() self.ignore_signal = False if self.prefs.details_dialog_viewers_show_scrollbars: self.toggleScrollBars() else: self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setResizeAnchor(QGraphicsView.AnchorViewCenter) self.setAlignment(Qt.AlignCenter) self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate) self.setMouseTracking(True) def connectMouseSignals(self): if not self._dragConnection: self._dragConnection = self.mouseDragged.connect(self.controller.syncCenters) if not self._wheelConnection: self._wheelConnection = self.mouseWheeled.connect(self.controller.onMouseWheel) def disconnectMouseSignals(self): if self._dragConnection: self.mouseDragged.disconnect() self._dragConnection = None if self._wheelConnection: self.mouseWheeled.disconnect() self._wheelConnection = None def connectScrollBars(self): """Only call once controller is connected.""" # Cyclic connections are handled by Qt self._verticalScrollBar.valueChanged.connect(self.controller.onVScrollBarChanged, Qt.UniqueConnection) self._horizontalScrollBar.valueChanged.connect(self.controller.onHScrollBarChanged, Qt.UniqueConnection) def toggleScrollBars(self, force_on=False): if not self.prefs.details_dialog_viewers_show_scrollbars: return # Ensure that it's off on the first run if self.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded: if force_on: return self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) else: self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) def contextMenuEvent(self, event): """Block parent's (main window) context menu on right click.""" event.accept() def mousePressEvent(self, event): if self.bestFit: event.ignore() return if event.button() & (Qt.LeftButton | Qt.MidButton | Qt.RightButton): self._drag = True else: self._drag = False event.ignore() return self._lastMouseClickPoint = event.pos() self._app.setOverrideCursor(Qt.ClosedHandCursor) self.setMouseTracking(True) # We need to propagate to scrollbars, so we send back up super().mousePressEvent(event) def mouseReleaseEvent(self, event): if self.bestFit: event.ignore() return self._drag = False self._app.restoreOverrideCursor() self.setMouseTracking(False) self.updateCenterPoint() super().mouseReleaseEvent(event) def mouseMoveEvent(self, event): if self.bestFit: event.ignore() return if self._drag: self._lastMouseClickPoint = event.pos() # We can simply rely on the scrollbar updating each other here # self.mouseDragged.emit() self.updateCenterPoint() super().mouseMoveEvent(event) def updateCenterPoint(self): self._centerPoint = self.mapToScene(self.rect().center()) def wheelEvent(self, event): if self.bestFit or MIN_SCALE > self.current_scale > MAX_SCALE or not self.controller.same_dimensions: event.ignore() return point_before_scale = QPointF(self.mapToScene(self.mapFromGlobal(QCursor.pos()))) # Get the original screen centerpoint screen_center = QPointF(self.mapToScene(self.rect().center())) if event.angleDelta().y() > 0: factor = self.zoomInFactor else: factor = self.zoomOutFactor # Avoid scrollbars conflict: self.other_viewer.ignore_signal = True self.scaleBy(factor) point_after_scale = QPointF(self.mapToScene(self.mapFromGlobal(QCursor.pos()))) # Get the offset of how the screen moved offset = point_before_scale - point_after_scale # Adjust to the new center for correct zooming new_center = screen_center + offset self.setCenter(new_center) self.mouseWheeled.emit(factor, new_center) self.other_viewer.ignore_signal = False def setImage(self, pixmap): if pixmap.isNull(): self.ignore_signal = True elif self.ignore_signal: self.ignore_signal = False self._pixmap = pixmap self._item.setPixmap(pixmap) self.translate(1, 1) def centerViewAndUpdate(self): # Called from the base controller for Normal Size pass def setCenter(self, point): self._centerPoint = point self.centerOn(self._centerPoint) def resetCenter(self): """Resets origin""" self._mousePanningDelta = QPointF() self.current_scale = 1.0 def setNewCenter(self, position): self._centerPoint = position self.centerOn(self._centerPoint) def setCachedPixmap(self): """In case we have changed the cached pixmap, reset it.""" self._item.setPixmap(self._pixmap) self._item.update() def scaleAt(self, scale): if scale == 1.0: self.resetScale() # self.setTransform( QTransform() ) self.scale(scale, scale) def getScale(self): return self.transform().m22() def scaleBy(self, factor): self.current_scale *= factor super().scale(factor, factor) def resetScale(self): # self.setTransform( QTransform() ) self.resetTransform() # probably same as above self.setCenter(self.scene().sceneRect().center()) def fitScale(self): self.bestFit = True super().fitInView(self._scene.sceneRect(), Qt.KeepAspectRatio) self.setNewCenter(self._scene.sceneRect().center()) @pyqtSlot() def scaleToNormalSize(self): """Called when the pixmap is set back to original size.""" self.bestFit = False self.scaleAt(1.0) self.toggleScrollBars(True) self.update() def adjustScrollBarsScaled(self, delta): """After scaling with the mouse, update relative to mouse position.""" self._horizontalScrollBar.setValue(self._horizontalScrollBar.value() + delta.x()) self._verticalScrollBar.setValue(self._verticalScrollBar.value() + delta.y()) def sizeHint(self): return self.viewport().rect().size() def adjustScrollBarsFactor(self, factor): """After scaling, no mouse position, default to center.""" self._horizontalScrollBar.setValue( int(factor * self._horizontalScrollBar.value() + ((factor - 1) * self._horizontalScrollBar.pageStep() / 2)) ) self._verticalScrollBar.setValue( int(factor * self._verticalScrollBar.value() + ((factor - 1) * self._verticalScrollBar.pageStep() / 2)) ) def adjustScrollBarsAuto(self): """After panning, update accordingly.""" self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - self._mousePanningDelta.x()) self.verticalScrollBar().setValue(self.verticalScrollBar().value() - self._mousePanningDelta.y()) dupeguru-4.3.1/qt/pe/modules/000077500000000000000000000000001426171743600160725ustar00rootroot00000000000000dupeguru-4.3.1/qt/pe/modules/block.c000066400000000000000000000116541426171743600173370ustar00rootroot00000000000000/* Created By: Virgil Dupras * Created On: 2010-01-31 * Copyright 2014 Hardcoded Software (http://www.hardcoded.net) * * This software is licensed under the "BSD" License as described in the *"LICENSE" file, which should be included with this package. The terms are also *available at http://www.hardcoded.net/licenses/bsd_license **/ #define PY_SSIZE_T_CLEAN #include "Python.h" /* It seems like MS VC defines min/max already */ #ifndef _MSC_VER static int max(int a, int b) { return b > a ? b : a; } static int min(int a, int b) { return b < a ? b : a; } #endif static PyObject *getblock(PyObject *image, int width, int height) { int pixel_count, red, green, blue, bytes_per_line; PyObject *pred, *pgreen, *pblue; PyObject *result; red = green = blue = 0; pixel_count = width * height; if (pixel_count) { PyObject *sipptr, *bits_capsule, *pi; char *s; int i; pi = PyObject_CallMethod(image, "bytesPerLine", NULL); bytes_per_line = PyLong_AsLong(pi); Py_DECREF(pi); sipptr = PyObject_CallMethod(image, "bits", NULL); bits_capsule = PyObject_CallMethod(sipptr, "ascapsule", NULL); Py_DECREF(sipptr); s = (char *)PyCapsule_GetPointer(bits_capsule, NULL); Py_DECREF(bits_capsule); /* Qt aligns all its lines on 32bit, which means that if the number of bytes *per line for image is not divisible by 4, there's going to be crap *inserted in "s" We have to take this into account when calculating offsets **/ for (i = 0; i < height; i++) { int j; for (j = 0; j < width; j++) { int offset; unsigned char r, g, b; offset = i * bytes_per_line + j * 3; r = s[offset]; g = s[offset + 1]; b = s[offset + 2]; red += r; green += g; blue += b; } } red /= pixel_count; green /= pixel_count; blue /= pixel_count; } pred = PyLong_FromLong(red); pgreen = PyLong_FromLong(green); pblue = PyLong_FromLong(blue); result = PyTuple_Pack(3, pred, pgreen, pblue); Py_DECREF(pred); Py_DECREF(pgreen); Py_DECREF(pblue); return result; } /* block_getblocks(QImage image, int block_count_per_side) -> [(int r, int g, *int b), ...] * * Compute blocks out of `image`. Note the use of min/max when compes the time *of computing widths and heights and positions. This is to cover the case where *the width or height of the image is smaller than `block_count_per_side`. In *these cases, blocks will be, of course, 1 pixel big. But also, because all *compared block lists are required to be of the same size, any block that has * no pixel to be assigned to will simply be assigned the last pixel. This is *why we have min(..., height-block_height-1) and stuff like that. **/ static PyObject *block_getblocks(PyObject *self, PyObject *args) { int block_count_per_side, width, height, block_width, block_height, ih; PyObject *image; PyObject *pi; PyObject *result; if (!PyArg_ParseTuple(args, "Oi", &image, &block_count_per_side)) { return NULL; } pi = PyObject_CallMethod(image, "width", NULL); width = PyLong_AsLong(pi); Py_DECREF(pi); pi = PyObject_CallMethod(image, "height", NULL); height = PyLong_AsLong(pi); Py_DECREF(pi); if (!(width && height)) { return PyList_New(0); } block_width = max(width / block_count_per_side, 1); block_height = max(height / block_count_per_side, 1); result = PyList_New((Py_ssize_t)block_count_per_side * block_count_per_side); if (result == NULL) { return NULL; } for (ih = 0; ih < block_count_per_side; ih++) { int top, iw; top = min(ih * block_height, height - block_height - 1); for (iw = 0; iw < block_count_per_side; iw++) { int left; PyObject *pcrop; PyObject *pblock; left = min(iw * block_width, width - block_width - 1); pcrop = PyObject_CallMethod(image, "copy", "iiii", left, top, block_width, block_height); if (pcrop == NULL) { Py_DECREF(result); return NULL; } pblock = getblock(pcrop, block_width, block_height); Py_DECREF(pcrop); if (pblock == NULL) { Py_DECREF(result); return NULL; } PyList_SET_ITEM(result, ih * block_count_per_side + iw, pblock); } } return result; } static PyMethodDef BlockMethods[] = { {"getblocks", block_getblocks, METH_VARARGS, ""}, {NULL, NULL, 0, NULL} /* Sentinel */ }; static struct PyModuleDef BlockDef = {PyModuleDef_HEAD_INIT, "_block_qt", NULL, -1, BlockMethods, NULL, NULL, NULL, NULL}; PyObject *PyInit__block_qt(void) { PyObject *m = PyModule_Create(&BlockDef); if (m == NULL) { return NULL; } return m; }dupeguru-4.3.1/qt/pe/photo.py000066400000000000000000000052261426171743600161320ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import logging from PyQt5.QtGui import QImage, QImageReader, QTransform from core.pe.photo import Photo as PhotoBase from qt.pe.block import getblocks class File(PhotoBase): def _plat_get_dimensions(self): try: ir = QImageReader(str(self.path)) size = ir.size() if size.isValid(): return (size.width(), size.height()) else: return (0, 0) except OSError: logging.warning("Could not read image '%s'", str(self.path)) return (0, 0) def _plat_get_blocks(self, block_count_per_side, orientation): image = QImage(str(self.path)) image = image.convertToFormat(QImage.Format_RGB888) if type(orientation) != int: logging.warning( "Orientation for file '%s' was a %s '%s', not an int.", str(self.path), type(orientation), orientation ) try: orientation = int(orientation) except Exception as e: logging.exception( "Skipping transformation because could not convert %s to int. %s", type(orientation), e, ) return getblocks(image, block_count_per_side) # MYSTERY TO SOLVE: For reasons I cannot explain, orientations 5 and 7 don't work for # duplicate scanning. The transforms seems to work fine (if I try to save the image after # the transform, we see that the image has been correctly flipped and rotated), but the # analysis part yields wrong blocks. I spent enought time with this feature, so I'll leave # like that for now. (by the way, orientations 5 and 7 work fine under Cocoa) if 2 <= orientation <= 8: t = QTransform() if orientation == 2: t.scale(-1, 1) elif orientation == 3: t.rotate(180) elif orientation == 4: t.scale(1, -1) elif orientation == 5: t.scale(-1, 1) t.rotate(90) elif orientation == 6: t.rotate(90) elif orientation == 7: t.scale(-1, 1) t.rotate(270) elif orientation == 8: t.rotate(270) image = image.transformed(t) return getblocks(image, block_count_per_side) dupeguru-4.3.1/qt/pe/preferences_dialog.py000066400000000000000000000101351426171743600206140ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtWidgets import QFormLayout from PyQt5.QtCore import Qt from hscommon.trans import trget from hscommon.plat import ISLINUX from qt.radio_box import RadioBox from core.scanner import ScanType from core.app import AppMode from qt.preferences_dialog import PreferencesDialogBase tr = trget("ui") class PreferencesDialog(PreferencesDialogBase): def _setupPreferenceWidgets(self): self._setupFilterHardnessBox() self.widgetsVLayout.addLayout(self.filterHardnessHLayout) self._setupAddCheckbox("matchScaledBox", tr("Match pictures of different dimensions")) self.widgetsVLayout.addWidget(self.matchScaledBox) self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind")) self.widgetsVLayout.addWidget(self.mixFileKindBox) self._setupAddCheckbox("useRegexpBox", tr("Use regular expressions when filtering")) self.widgetsVLayout.addWidget(self.useRegexpBox) self._setupAddCheckbox("removeEmptyFoldersBox", tr("Remove empty folders on delete or move")) self.widgetsVLayout.addWidget(self.removeEmptyFoldersBox) self._setupAddCheckbox( "ignoreHardlinkMatches", tr("Ignore duplicates hardlinking to the same file"), ) self.widgetsVLayout.addWidget(self.ignoreHardlinkMatches) self.cacheTypeRadio = RadioBox(self, items=["Sqlite", "Shelve"], spread=False) cache_form = QFormLayout() cache_form.setLabelAlignment(Qt.AlignLeft) cache_form.addRow(tr("Picture cache mode:"), self.cacheTypeRadio) self.widgetsVLayout.addLayout(cache_form) self._setupBottomPart() def _setupDisplayPage(self): super()._setupDisplayPage() self._setupAddCheckbox("details_dialog_override_theme_icons", tr("Override theme icons in viewer toolbar")) self.details_dialog_override_theme_icons.setToolTip( tr("Use our own internal icons instead of those provided by the theme engine") ) # Prevent changing this on platforms where themes are unpredictable self.details_dialog_override_theme_icons.setEnabled(False if not ISLINUX else True) # Insert this right after the vertical title bar option index = self.details_groupbox_layout.indexOf(self.details_dialog_vertical_titlebar) self.details_groupbox_layout.insertWidget(index + 1, self.details_dialog_override_theme_icons) self._setupAddCheckbox("details_dialog_viewers_show_scrollbars", tr("Show scrollbars in image viewers")) self.details_dialog_viewers_show_scrollbars.setToolTip( tr( "When the image displayed doesn't fit the viewport, \ show scrollbars to span the view around" ) ) self.details_groupbox_layout.insertWidget(index + 2, self.details_dialog_viewers_show_scrollbars) def _load(self, prefs, setchecked, section): setchecked(self.matchScaledBox, prefs.match_scaled) self.cacheTypeRadio.selected_index = 1 if prefs.picture_cache_type == "shelve" else 0 # Update UI state based on selected scan type scan_type = prefs.get_scan_type(AppMode.PICTURE) fuzzy_scan = scan_type == ScanType.FUZZYBLOCK self.filterHardnessSlider.setEnabled(fuzzy_scan) setchecked(self.details_dialog_override_theme_icons, prefs.details_dialog_override_theme_icons) setchecked(self.details_dialog_viewers_show_scrollbars, prefs.details_dialog_viewers_show_scrollbars) def _save(self, prefs, ischecked): prefs.match_scaled = ischecked(self.matchScaledBox) prefs.picture_cache_type = "shelve" if self.cacheTypeRadio.selected_index == 1 else "sqlite" prefs.details_dialog_override_theme_icons = ischecked(self.details_dialog_override_theme_icons) prefs.details_dialog_viewers_show_scrollbars = ischecked(self.details_dialog_viewers_show_scrollbars) dupeguru-4.3.1/qt/pe/results_model.py000066400000000000000000000016021426171743600176540ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from qt.column import Column from qt.results_model import ResultsModel as ResultsModelBase class ResultsModel(ResultsModelBase): COLUMNS = [ Column("marked", default_width=30), Column("name", default_width=200), Column("folder_path", default_width=180), Column("size", default_width=60), Column("extension", default_width=40), Column("dimensions", default_width=100), Column("exif_timestamp", default_width=120), Column("mtime", default_width=120), Column("percentage", default_width=60), Column("dupe_count", default_width=80), ] dupeguru-4.3.1/qt/platform.py000066400000000000000000000021441426171743600162150ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import os.path as op from hscommon.plat import ISWINDOWS, ISOSX, ISLINUX if op.exists(__file__): # We want to get the absolute path or our root folder. We know that in that folder we're # inside qt/, so we just go back one level. BASE_PATH = op.abspath(op.join(op.dirname(__file__), "..")) else: # Should be a frozen environment if ISOSX: BASE_PATH = op.abspath(op.join(op.dirname(__file__), "..", "..", "Resources")) else: # For others our base path is ''. BASE_PATH = "" HELP_PATH = op.join(BASE_PATH, "help", "en") if ISWINDOWS: INITIAL_FOLDER_IN_DIALOGS = "C:\\" elif ISOSX: INITIAL_FOLDER_IN_DIALOGS = "/" elif ISLINUX: INITIAL_FOLDER_IN_DIALOGS = "/" else: # unsupported platform, however '/' is a good guess for a path which is available INITIAL_FOLDER_IN_DIALOGS = "/" dupeguru-4.3.1/qt/preferences.py000066400000000000000000000357701426171743600167050ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtWidgets import QApplication, QDockWidget from PyQt5.QtCore import Qt, QRect, QObject, pyqtSignal from PyQt5.QtGui import QColor from hscommon import trans from hscommon.plat import ISLINUX from core.app import AppMode from core.scanner import ScanType from hscommon.util import tryint from qt.util import create_qsettings def get_langnames(): tr = trans.trget("ui") return { "cs": tr("Czech"), "de": tr("German"), "el": tr("Greek"), "en": tr("English"), "es": tr("Spanish"), "fr": tr("French"), "hy": tr("Armenian"), "it": tr("Italian"), "ja": tr("Japanese"), "ko": tr("Korean"), "ms": tr("Malay"), "nl": tr("Dutch"), "pl_PL": tr("Polish"), "pt_BR": tr("Brazilian"), "ru": tr("Russian"), "tr": tr("Turkish"), "uk": tr("Ukrainian"), "vi": tr("Vietnamese"), "zh_CN": tr("Chinese (Simplified)"), } def _normalize_for_serialization(v): # QSettings doesn't consider set/tuple as "native" typs for serialization, so if we don't # change them into a list, we get a weird serialized QVariant value which isn't a very # "portable" value. if isinstance(v, (set, tuple)): v = list(v) if isinstance(v, list): v = [_normalize_for_serialization(item) for item in v] return v def _adjust_after_deserialization(v): # In some cases, when reading from prefs, we end up with strings that are supposed to be # bool or int. Convert these. if isinstance(v, list): return [_adjust_after_deserialization(sub) for sub in v] if isinstance(v, str): # might be bool or int, try them if v == "true": return True elif v == "false": return False else: return tryint(v, v) return v class PreferencesBase(QObject): prefsChanged = pyqtSignal() def __init__(self): QObject.__init__(self) self.reset() self._settings = create_qsettings() def _load_values(self, settings): # Implemented in subclasses pass def get_rect(self, name, default=None): r = self.get_value(name, default) if r is not None: return QRect(*r) else: return None def get_value(self, name, default=None): if self._settings.contains(name): result = _adjust_after_deserialization(self._settings.value(name)) if result is not None: return result else: # If result is None, but still present in self._settings, it usually means a value # like "@Invalid". return default else: return default def load(self): self.reset() self._load_values(self._settings) def reset(self): # Implemented in subclasses pass def _save_values(self, settings): # Implemented in subclasses pass def save(self): self._save_values(self._settings) self._settings.sync() def set_rect(self, name, r): # About QRect conversion: # I think Qt supports putting basic structures like QRect directly in QSettings, but I prefer not # to rely on it and stay with generic structures. if isinstance(r, QRect): rect_as_list = [r.x(), r.y(), r.width(), r.height()] self.set_value(name, rect_as_list) def set_value(self, name, value): self._settings.setValue(name, _normalize_for_serialization(value)) def saveGeometry(self, name, widget): # We save geometry under a 7-sized int array: first item is a flag # for whether the widget is maximized, second item is a flag for whether # the widget is docked, third item is a Qt::DockWidgetArea enum value, # and the other 4 are (x, y, w, h). m = 1 if widget.isMaximized() else 0 d = 1 if isinstance(widget, QDockWidget) and not widget.isFloating() else 0 area = widget.parent.dockWidgetArea(widget) if d else 0 r = widget.geometry() rect_as_list = [r.x(), r.y(), r.width(), r.height()] self.set_value(name, [m, d, area] + rect_as_list) def restoreGeometry(self, name, widget): geometry = self.get_value(name) if geometry and len(geometry) == 7: m, d, area, x, y, w, h = geometry if m: widget.setWindowState(Qt.WindowMaximized) else: r = QRect(x, y, w, h) widget.setGeometry(r) if isinstance(widget, QDockWidget): # Inform of the previous dock state and the area used return bool(d), area return False, 0 class Preferences(PreferencesBase): def _load_values(self, settings): get = self.get_value self.filter_hardness = get("FilterHardness", self.filter_hardness) self.mix_file_kind = get("MixFileKind", self.mix_file_kind) self.ignore_hardlink_matches = get("IgnoreHardlinkMatches", self.ignore_hardlink_matches) self.use_regexp = get("UseRegexp", self.use_regexp) self.remove_empty_folders = get("RemoveEmptyFolders", self.remove_empty_folders) self.debug_mode = get("DebugMode", self.debug_mode) self.profile_scan = get("ProfileScan", self.profile_scan) self.destination_type = get("DestinationType", self.destination_type) self.custom_command = get("CustomCommand", self.custom_command) self.language = get("Language", self.language) if not self.language and trans.installed_lang: self.language = trans.installed_lang self.portable = get("Portable", False) self.use_dark_style = get("UseDarkStyle", False) self.use_native_dialogs = get("UseNativeDialogs", True) self.tableFontSize = get("TableFontSize", self.tableFontSize) self.reference_bold_font = get("ReferenceBoldFont", self.reference_bold_font) self.details_dialog_titlebar_enabled = get("DetailsDialogTitleBarEnabled", self.details_dialog_titlebar_enabled) self.details_dialog_vertical_titlebar = get( "DetailsDialogVerticalTitleBar", self.details_dialog_vertical_titlebar ) # On Windows and MacOS, use internal icons by default self.details_dialog_override_theme_icons = ( get("DetailsDialogOverrideThemeIcons", self.details_dialog_override_theme_icons) if ISLINUX else True ) self.details_table_delta_foreground_color = get( "DetailsTableDeltaForegroundColor", self.details_table_delta_foreground_color ) self.details_dialog_viewers_show_scrollbars = get( "DetailsDialogViewersShowScrollbars", self.details_dialog_viewers_show_scrollbars ) self.result_table_ref_foreground_color = get( "ResultTableRefForegroundColor", self.result_table_ref_foreground_color ) self.result_table_ref_background_color = get( "ResultTableRefBackgroundColor", self.result_table_ref_background_color ) self.result_table_delta_foreground_color = get( "ResultTableDeltaForegroundColor", self.result_table_delta_foreground_color ) self.resultWindowIsMaximized = get("ResultWindowIsMaximized", self.resultWindowIsMaximized) self.resultWindowRect = self.get_rect("ResultWindowRect", self.resultWindowRect) self.mainWindowIsMaximized = get("MainWindowIsMaximized", self.mainWindowIsMaximized) self.mainWindowRect = self.get_rect("MainWindowRect", self.mainWindowRect) self.directoriesWindowRect = self.get_rect("DirectoriesWindowRect", self.directoriesWindowRect) self.recentResults = get("RecentResults", self.recentResults) self.recentFolders = get("RecentFolders", self.recentFolders) self.tabs_default_pos = get("TabsDefaultPosition", self.tabs_default_pos) self.word_weighting = get("WordWeighting", self.word_weighting) self.match_similar = get("MatchSimilar", self.match_similar) self.ignore_small_files = get("IgnoreSmallFiles", self.ignore_small_files) self.small_file_threshold = get("SmallFileThreshold", self.small_file_threshold) self.ignore_large_files = get("IgnoreLargeFiles", self.ignore_large_files) self.large_file_threshold = get("LargeFileThreshold", self.large_file_threshold) self.big_file_partial_hashes = get("BigFilePartialHashes", self.big_file_partial_hashes) self.big_file_size_threshold = get("BigFileSizeThreshold", self.big_file_size_threshold) self.scan_tag_track = get("ScanTagTrack", self.scan_tag_track) self.scan_tag_artist = get("ScanTagArtist", self.scan_tag_artist) self.scan_tag_album = get("ScanTagAlbum", self.scan_tag_album) self.scan_tag_title = get("ScanTagTitle", self.scan_tag_title) self.scan_tag_genre = get("ScanTagGenre", self.scan_tag_genre) self.scan_tag_year = get("ScanTagYear", self.scan_tag_year) self.match_scaled = get("MatchScaled", self.match_scaled) self.picture_cache_type = get("PictureCacheType", self.picture_cache_type) def reset(self): self.filter_hardness = 95 self.mix_file_kind = True self.use_regexp = False self.ignore_hardlink_matches = False self.remove_empty_folders = False self.debug_mode = False self.profile_scan = False self.destination_type = 1 self.custom_command = "" self.language = trans.installed_lang if trans.installed_lang else "" self.use_dark_style = False self.use_native_dialogs = True self.tableFontSize = QApplication.font().pointSize() self.reference_bold_font = True self.details_dialog_titlebar_enabled = True self.details_dialog_vertical_titlebar = True self.details_table_delta_foreground_color = QColor(250, 20, 20) # red # By default use internal icons on platforms other than Linux for now self.details_dialog_override_theme_icons = False if not ISLINUX else True self.details_dialog_viewers_show_scrollbars = True self.result_table_ref_foreground_color = QColor(Qt.blue) self.result_table_ref_background_color = QColor(Qt.lightGray) self.result_table_delta_foreground_color = QColor(255, 142, 40) # orange self.resultWindowIsMaximized = False self.resultWindowRect = None self.directoriesWindowRect = None self.mainWindowRect = None self.mainWindowIsMaximized = False self.recentResults = [] self.recentFolders = [] self.tabs_default_pos = True self.word_weighting = True self.match_similar = False self.ignore_small_files = True self.small_file_threshold = 10 # KB self.ignore_large_files = False self.large_file_threshold = 1000 # MB self.big_file_partial_hashes = False self.big_file_size_threshold = 100 # MB self.scan_tag_track = False self.scan_tag_artist = True self.scan_tag_album = True self.scan_tag_title = True self.scan_tag_genre = False self.scan_tag_year = False self.match_scaled = False self.picture_cache_type = "sqlite" def _save_values(self, settings): set_ = self.set_value set_("FilterHardness", self.filter_hardness) set_("MixFileKind", self.mix_file_kind) set_("IgnoreHardlinkMatches", self.ignore_hardlink_matches) set_("UseRegexp", self.use_regexp) set_("RemoveEmptyFolders", self.remove_empty_folders) set_("DebugMode", self.debug_mode) set_("ProfileScan", self.profile_scan) set_("DestinationType", self.destination_type) set_("CustomCommand", self.custom_command) set_("Language", self.language) set_("Portable", self.portable) set_("UseDarkStyle", self.use_dark_style) set_("UseNativeDialogs", self.use_native_dialogs) set_("TableFontSize", self.tableFontSize) set_("ReferenceBoldFont", self.reference_bold_font) set_("DetailsDialogTitleBarEnabled", self.details_dialog_titlebar_enabled) set_("DetailsDialogVerticalTitleBar", self.details_dialog_vertical_titlebar) set_("DetailsDialogOverrideThemeIcons", self.details_dialog_override_theme_icons) set_("DetailsDialogViewersShowScrollbars", self.details_dialog_viewers_show_scrollbars) set_("DetailsTableDeltaForegroundColor", self.details_table_delta_foreground_color) set_("ResultTableRefForegroundColor", self.result_table_ref_foreground_color) set_("ResultTableRefBackgroundColor", self.result_table_ref_background_color) set_("ResultTableDeltaForegroundColor", self.result_table_delta_foreground_color) set_("ResultWindowIsMaximized", self.resultWindowIsMaximized) set_("MainWindowIsMaximized", self.mainWindowIsMaximized) self.set_rect("ResultWindowRect", self.resultWindowRect) self.set_rect("MainWindowRect", self.mainWindowRect) self.set_rect("DirectoriesWindowRect", self.directoriesWindowRect) set_("RecentResults", self.recentResults) set_("RecentFolders", self.recentFolders) set_("TabsDefaultPosition", self.tabs_default_pos) set_("WordWeighting", self.word_weighting) set_("MatchSimilar", self.match_similar) set_("IgnoreSmallFiles", self.ignore_small_files) set_("SmallFileThreshold", self.small_file_threshold) set_("IgnoreLargeFiles", self.ignore_large_files) set_("LargeFileThreshold", self.large_file_threshold) set_("BigFilePartialHashes", self.big_file_partial_hashes) set_("BigFileSizeThreshold", self.big_file_size_threshold) set_("ScanTagTrack", self.scan_tag_track) set_("ScanTagArtist", self.scan_tag_artist) set_("ScanTagAlbum", self.scan_tag_album) set_("ScanTagTitle", self.scan_tag_title) set_("ScanTagGenre", self.scan_tag_genre) set_("ScanTagYear", self.scan_tag_year) set_("MatchScaled", self.match_scaled) set_("PictureCacheType", self.picture_cache_type) # scan_type is special because we save it immediately when we set it. def get_scan_type(self, app_mode): if app_mode == AppMode.PICTURE: return self.get_value("ScanTypePicture", ScanType.FUZZYBLOCK) elif app_mode == AppMode.MUSIC: return self.get_value("ScanTypeMusic", ScanType.TAG) else: return self.get_value("ScanTypeStandard", ScanType.CONTENTS) def set_scan_type(self, app_mode, value): if app_mode == AppMode.PICTURE: self.set_value("ScanTypePicture", value) elif app_mode == AppMode.MUSIC: self.set_value("ScanTypeMusic", value) else: self.set_value("ScanTypeStandard", value) dupeguru-4.3.1/qt/preferences_dialog.py000066400000000000000000000445201426171743600202150ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import Qt, QSize, pyqtSlot from PyQt5.QtWidgets import ( QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QComboBox, QSlider, QSizePolicy, QSpacerItem, QCheckBox, QLineEdit, QMessageBox, QSpinBox, QLayout, QTabWidget, QWidget, QColorDialog, QPushButton, QGroupBox, QFormLayout, ) from PyQt5.QtGui import QPixmap, QIcon from hscommon import desktop, plat from hscommon.trans import trget from hscommon.plat import ISLINUX from qt.util import horizontal_wrap, move_to_screen_center from qt.preferences import get_langnames from enum import Flag, auto from qt.preferences import Preferences tr = trget("ui") class Sections(Flag): """Filter blocks of preferences when reset or loaded""" GENERAL = auto() DISPLAY = auto() DEBUG = auto() ALL = GENERAL | DISPLAY | DEBUG class PreferencesDialogBase(QDialog): def __init__(self, parent, app, **kwargs): flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint super().__init__(parent, flags, **kwargs) self.app = app self.supportedLanguages = dict(sorted(get_langnames().items(), key=lambda item: item[1])) self._setupUi() self.filterHardnessSlider.valueChanged["int"].connect(self.filterHardnessLabel.setNum) self.debug_location_label.linkActivated.connect(desktop.open_path) self.buttonBox.clicked.connect(self.buttonClicked) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) def _setupFilterHardnessBox(self): self.filterHardnessHLayout = QHBoxLayout() self.filterHardnessLabel = QLabel(self) self.filterHardnessLabel.setText(tr("Filter Hardness:")) self.filterHardnessLabel.setMinimumSize(QSize(0, 0)) self.filterHardnessHLayout.addWidget(self.filterHardnessLabel) self.filterHardnessVLayout = QVBoxLayout() self.filterHardnessVLayout.setSpacing(0) self.filterHardnessHLayoutSub1 = QHBoxLayout() self.filterHardnessHLayoutSub1.setSpacing(12) self.filterHardnessSlider = QSlider(self) size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) size_policy.setHorizontalStretch(0) size_policy.setVerticalStretch(0) size_policy.setHeightForWidth(self.filterHardnessSlider.sizePolicy().hasHeightForWidth()) self.filterHardnessSlider.setSizePolicy(size_policy) self.filterHardnessSlider.setMinimum(1) self.filterHardnessSlider.setMaximum(100) self.filterHardnessSlider.setTracking(True) self.filterHardnessSlider.setOrientation(Qt.Horizontal) self.filterHardnessHLayoutSub1.addWidget(self.filterHardnessSlider) self.filterHardnessLabel = QLabel(self) self.filterHardnessLabel.setText("100") self.filterHardnessLabel.setMinimumSize(QSize(21, 0)) self.filterHardnessHLayoutSub1.addWidget(self.filterHardnessLabel) self.filterHardnessVLayout.addLayout(self.filterHardnessHLayoutSub1) self.filterHardnessHLayoutSub2 = QHBoxLayout() self.filterHardnessHLayoutSub2.setContentsMargins(-1, 0, -1, -1) self.moreResultsLabel = QLabel(self) self.moreResultsLabel.setText(tr("More Results")) self.filterHardnessHLayoutSub2.addWidget(self.moreResultsLabel) spacer_item = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.filterHardnessHLayoutSub2.addItem(spacer_item) self.fewerResultsLabel = QLabel(self) self.fewerResultsLabel.setText(tr("Fewer Results")) self.filterHardnessHLayoutSub2.addWidget(self.fewerResultsLabel) self.filterHardnessVLayout.addLayout(self.filterHardnessHLayoutSub2) self.filterHardnessHLayout.addLayout(self.filterHardnessVLayout) def _setupBottomPart(self): # The bottom part of the pref panel is always the same in all editions. self.copyMoveLabel = QLabel(self) self.copyMoveLabel.setText(tr("Copy and Move:")) self.widgetsVLayout.addWidget(self.copyMoveLabel) self.copyMoveDestinationComboBox = QComboBox(self) self.copyMoveDestinationComboBox.addItem(tr("Right in destination")) self.copyMoveDestinationComboBox.addItem(tr("Recreate relative path")) self.copyMoveDestinationComboBox.addItem(tr("Recreate absolute path")) self.widgetsVLayout.addWidget(self.copyMoveDestinationComboBox) self.customCommandLabel = QLabel(self) self.customCommandLabel.setText(tr("Custom Command (arguments: %d for dupe, %r for ref):")) self.widgetsVLayout.addWidget(self.customCommandLabel) self.customCommandEdit = QLineEdit(self) self.widgetsVLayout.addWidget(self.customCommandEdit) def _setupDisplayPage(self): self.ui_groupbox = QGroupBox("&" + tr("General Interface")) layout = QVBoxLayout() self.languageLabel = QLabel(tr("Language:"), self) self.languageComboBox = QComboBox(self) for lang_code, lang_str in self.supportedLanguages.items(): self.languageComboBox.addItem(lang_str, userData=lang_code) layout.addLayout(horizontal_wrap([self.languageLabel, self.languageComboBox, None])) self._setupAddCheckbox( "tabs_default_pos", tr("Use default position for tab bar (requires restart)"), ) self.tabs_default_pos.setToolTip( tr( "Place the tab bar below the main menu instead of next to it\n\ On MacOS, the tab bar will fill up the window's width instead." ) ) layout.addWidget(self.tabs_default_pos) self._setupAddCheckbox( "use_native_dialogs", tr("Use native OS dialogs"), ) self.use_native_dialogs.setToolTip( tr( "For actions such as file/folder selection use the OS native dialogs.\nSome native dialogs have limited functionality." ) ) layout.addWidget(self.use_native_dialogs) if plat.ISWINDOWS: self._setupAddCheckbox("use_dark_style", tr("Use dark style")) layout.addWidget(self.use_dark_style) self.ui_groupbox.setLayout(layout) self.displayVLayout.addWidget(self.ui_groupbox) gridlayout = QGridLayout() gridlayout.setColumnStretch(2, 2) formlayout = QFormLayout() result_groupbox = QGroupBox("&" + tr("Result Table")) self.fontSizeSpinBox = QSpinBox() self.fontSizeSpinBox.setMinimum(5) formlayout.addRow(tr("Font size:"), self.fontSizeSpinBox) self._setupAddCheckbox("reference_bold_font", tr("Use bold font for references")) formlayout.addRow(self.reference_bold_font) self.result_table_ref_foreground_color = ColorPickerButton(self) formlayout.addRow(tr("Reference foreground color:"), self.result_table_ref_foreground_color) self.result_table_ref_background_color = ColorPickerButton(self) formlayout.addRow(tr("Reference background color:"), self.result_table_ref_background_color) self.result_table_delta_foreground_color = ColorPickerButton(self) formlayout.addRow(tr("Delta foreground color:"), self.result_table_delta_foreground_color) formlayout.setLabelAlignment(Qt.AlignLeft) # Keep same vertical spacing as parent layout for consistency formlayout.setVerticalSpacing(self.displayVLayout.spacing()) gridlayout.addLayout(formlayout, 0, 0) result_groupbox.setLayout(gridlayout) self.displayVLayout.addWidget(result_groupbox) details_groupbox = QGroupBox("&" + tr("Details Window")) self.details_groupbox_layout = QVBoxLayout() self._setupAddCheckbox( "details_dialog_titlebar_enabled", tr("Show the title bar and can be docked"), ) self.details_dialog_titlebar_enabled.setToolTip( tr( "While the title bar is hidden, \ use the modifier key to drag the floating window around" ) if ISLINUX else tr("The title bar can only be disabled while the window is docked") ) self.details_groupbox_layout.addWidget(self.details_dialog_titlebar_enabled) self._setupAddCheckbox("details_dialog_vertical_titlebar", tr("Vertical title bar")) self.details_dialog_vertical_titlebar.setToolTip( tr("Change the title bar from horizontal on top, to vertical on the left side") ) self.details_groupbox_layout.addWidget(self.details_dialog_vertical_titlebar) self.details_dialog_vertical_titlebar.setEnabled(self.details_dialog_titlebar_enabled.isChecked()) self.details_dialog_titlebar_enabled.stateChanged.connect(self.details_dialog_vertical_titlebar.setEnabled) gridlayout = QGridLayout() formlayout = QFormLayout() self.details_table_delta_foreground_color = ColorPickerButton(self) # Padding on the right side and space between label and widget to keep it somewhat consistent across themes gridlayout.setColumnStretch(1, 1) formlayout.setHorizontalSpacing(50) formlayout.addRow(tr("Delta foreground color:"), self.details_table_delta_foreground_color) gridlayout.addLayout(formlayout, 0, 0) self.details_groupbox_layout.addLayout(gridlayout) details_groupbox.setLayout(self.details_groupbox_layout) self.displayVLayout.addWidget(details_groupbox) def _setupDebugPage(self): self._setupAddCheckbox("debugModeBox", tr("Debug mode (restart required)")) self._setupAddCheckbox("profile_scan_box", tr("Profile scan operation")) self.profile_scan_box.setToolTip(tr("Profile the scan operation and save logs for optimization.")) self.debugVLayout.addWidget(self.debugModeBox) self.debugVLayout.addWidget(self.profile_scan_box) self.debug_location_label = QLabel( tr('Logs located in: {}').format(self.app.model.appdata, self.app.model.appdata), wordWrap=True, ) self.debugVLayout.addWidget(self.debug_location_label) def _setupAddCheckbox(self, name, label, parent=None): if parent is None: parent = self cb = QCheckBox(parent) cb.setText(label) setattr(self, name, cb) def _setupPreferenceWidgets(self): # Edition-specific pass def _setupUi(self): self.setWindowTitle(tr("Options")) self.setSizeGripEnabled(False) self.setModal(True) self.mainVLayout = QVBoxLayout(self) self.tabwidget = QTabWidget() self.page_general = QWidget() self.page_display = QWidget() self.page_debug = QWidget() self.widgetsVLayout = QVBoxLayout() self.page_general.setLayout(self.widgetsVLayout) self.displayVLayout = QVBoxLayout() self.displayVLayout.setSpacing(5) # arbitrary value, might conflict with style self.page_display.setLayout(self.displayVLayout) self.debugVLayout = QVBoxLayout() self.page_debug.setLayout(self.debugVLayout) self._setupPreferenceWidgets() self._setupDisplayPage() self._setupDebugPage() # self.mainVLayout.addLayout(self.widgetsVLayout) self.buttonBox = QDialogButtonBox(self) self.buttonBox.setStandardButtons( QDialogButtonBox.Cancel | QDialogButtonBox.Ok | QDialogButtonBox.RestoreDefaults ) self.mainVLayout.addWidget(self.tabwidget) self.mainVLayout.addWidget(self.buttonBox) self.layout().setSizeConstraint(QLayout.SetFixedSize) self.tabwidget.addTab(self.page_general, tr("General")) self.tabwidget.addTab(self.page_display, tr("Display")) self.tabwidget.addTab(self.page_debug, tr("Debug")) self.displayVLayout.addStretch(0) self.widgetsVLayout.addStretch(0) self.debugVLayout.addStretch(0) def _load(self, prefs, setchecked, section): # Edition-specific pass def _save(self, prefs, ischecked): # Edition-specific pass def load(self, prefs=None, section=Sections.ALL): if prefs is None: prefs = self.app.prefs def setchecked(cb, b): cb.setCheckState(Qt.Checked if b else Qt.Unchecked) if section & Sections.GENERAL: self.filterHardnessSlider.setValue(prefs.filter_hardness) self.filterHardnessLabel.setNum(prefs.filter_hardness) setchecked(self.mixFileKindBox, prefs.mix_file_kind) setchecked(self.useRegexpBox, prefs.use_regexp) setchecked(self.removeEmptyFoldersBox, prefs.remove_empty_folders) setchecked(self.ignoreHardlinkMatches, prefs.ignore_hardlink_matches) self.copyMoveDestinationComboBox.setCurrentIndex(prefs.destination_type) self.customCommandEdit.setText(prefs.custom_command) if section & Sections.DISPLAY: setchecked(self.reference_bold_font, prefs.reference_bold_font) setchecked(self.tabs_default_pos, prefs.tabs_default_pos) setchecked(self.use_native_dialogs, prefs.use_native_dialogs) if plat.ISWINDOWS: setchecked(self.use_dark_style, prefs.use_dark_style) setchecked( self.details_dialog_titlebar_enabled, prefs.details_dialog_titlebar_enabled, ) setchecked( self.details_dialog_vertical_titlebar, prefs.details_dialog_vertical_titlebar, ) self.fontSizeSpinBox.setValue(prefs.tableFontSize) self.details_table_delta_foreground_color.setColor(prefs.details_table_delta_foreground_color) self.result_table_ref_foreground_color.setColor(prefs.result_table_ref_foreground_color) self.result_table_ref_background_color.setColor(prefs.result_table_ref_background_color) self.result_table_delta_foreground_color.setColor(prefs.result_table_delta_foreground_color) try: selected_lang = self.supportedLanguages[self.app.prefs.language] except KeyError: selected_lang = self.supportedLanguages["en"] self.languageComboBox.setCurrentText(selected_lang) if section & Sections.DEBUG: setchecked(self.debugModeBox, prefs.debug_mode) setchecked(self.profile_scan_box, prefs.profile_scan) self._load(prefs, setchecked, section) def save(self): prefs = self.app.prefs prefs.filter_hardness = self.filterHardnessSlider.value() def ischecked(cb): return cb.checkState() == Qt.Checked prefs.mix_file_kind = ischecked(self.mixFileKindBox) prefs.use_regexp = ischecked(self.useRegexpBox) prefs.remove_empty_folders = ischecked(self.removeEmptyFoldersBox) prefs.ignore_hardlink_matches = ischecked(self.ignoreHardlinkMatches) prefs.debug_mode = ischecked(self.debugModeBox) prefs.profile_scan = ischecked(self.profile_scan_box) prefs.reference_bold_font = ischecked(self.reference_bold_font) prefs.details_dialog_titlebar_enabled = ischecked(self.details_dialog_titlebar_enabled) prefs.details_dialog_vertical_titlebar = ischecked(self.details_dialog_vertical_titlebar) prefs.details_table_delta_foreground_color = self.details_table_delta_foreground_color.color prefs.result_table_ref_foreground_color = self.result_table_ref_foreground_color.color prefs.result_table_ref_background_color = self.result_table_ref_background_color.color prefs.result_table_delta_foreground_color = self.result_table_delta_foreground_color.color prefs.destination_type = self.copyMoveDestinationComboBox.currentIndex() prefs.custom_command = str(self.customCommandEdit.text()) prefs.tableFontSize = self.fontSizeSpinBox.value() prefs.tabs_default_pos = ischecked(self.tabs_default_pos) prefs.use_native_dialogs = ischecked(self.use_native_dialogs) if plat.ISWINDOWS: prefs.use_dark_style = ischecked(self.use_dark_style) lang_code = self.languageComboBox.currentData() old_lang_code = self.app.prefs.language if old_lang_code not in self.supportedLanguages.keys(): old_lang_code = "en" if lang_code != old_lang_code: QMessageBox.information( self, "", tr("dupeGuru has to restart for language changes to take effect."), ) self.app.prefs.language = lang_code self._save(prefs, ischecked) def resetToDefaults(self, section_to_update): self.load(Preferences(), section_to_update) # --- Events def buttonClicked(self, button): role = self.buttonBox.buttonRole(button) if role == QDialogButtonBox.ResetRole: current_tab = self.tabwidget.currentWidget() section_to_update = Sections.ALL if current_tab is self.page_general: section_to_update = Sections.GENERAL if current_tab is self.page_display: section_to_update = Sections.DISPLAY if current_tab is self.page_debug: section_to_update = Sections.DEBUG self.resetToDefaults(section_to_update) def showEvent(self, event): # have to do this here as the frameGeometry is not correct until shown move_to_screen_center(self) super().showEvent(event) class ColorPickerButton(QPushButton): def __init__(self, parent): super().__init__(parent) self.parent = parent self.color = None self.clicked.connect(self.onClicked) @pyqtSlot() def onClicked(self): color = QColorDialog.getColor(self.color if self.color is not None else Qt.white, self.parent) self.setColor(color) def setColor(self, color): size = QSize(16, 16) px = QPixmap(size) if color is None: size.width = 0 size.height = 0 elif not color.isValid(): return else: self.color = color px.fill(color) self.setIcon(QIcon(px)) dupeguru-4.3.1/qt/prioritize_dialog.py000066400000000000000000000142471426171743600201170ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2011-09-06 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import Qt, QMimeData, QByteArray from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QListView, QDialogButtonBox, QAbstractItemView, QLabel, QStyle, QSplitter, QWidget, QSizePolicy, ) from hscommon.trans import trget from qt.selectable_list import ComboboxModel, ListviewModel from qt.util import vertical_spacer from core.gui.prioritize_dialog import PrioritizeDialog as PrioritizeDialogModel tr = trget("ui") MIME_INDEXES = "application/dupeguru.rowindexes" class PrioritizationList(ListviewModel): def flags(self, index): if not index.isValid(): return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled # --- Drag & Drop def dropMimeData(self, mime_data, action, row, column, parent_index): if not mime_data.hasFormat(MIME_INDEXES): return False # Since we only drop in between items, parentIndex must be invalid, and we use the row arg # to know where the drop took place. if parent_index.isValid(): return False # "When row and column are -1 it means that the dropped data should be considered as # dropped directly on parent." # Moving items to row -1 would put them before the last item. Fix the row to drop the # dragged items after the last item. if row < 0: row = len(self.model) - 1 str_mime_data = bytes(mime_data.data(MIME_INDEXES)).decode() indexes = list(map(int, str_mime_data.split(","))) self.model.move_indexes(indexes, row) self.view.selectionModel().clearSelection() return True def mimeData(self, indexes): rows = {str(index.row()) for index in indexes} data = ",".join(rows) mime_data = QMimeData() mime_data.setData(MIME_INDEXES, QByteArray(data.encode())) return mime_data def mimeTypes(self): return [MIME_INDEXES] def supportedDropActions(self): return Qt.MoveAction class PrioritizeDialog(QDialog): def __init__(self, parent, app, **kwargs): flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint super().__init__(parent, flags, **kwargs) self._setupUi() self.model = PrioritizeDialogModel(app=app.model) self.categoryList = ComboboxModel(model=self.model.category_list, view=self.categoryCombobox) self.criteriaList = ListviewModel(model=self.model.criteria_list, view=self.criteriaListView) self.prioritizationList = PrioritizationList( model=self.model.prioritization_list, view=self.prioritizationListView ) self.model.view = self self.addCriteriaButton.clicked.connect(self.model.add_selected) self.criteriaListView.doubleClicked.connect(self.model.add_selected) self.removeCriteriaButton.clicked.connect(self.model.remove_selected) self.prioritizationListView.doubleClicked.connect(self.model.remove_selected) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) def _setupUi(self): self.setWindowTitle(tr("Re-Prioritize duplicates")) self.resize(700, 400) # widgets msg = tr( "Add criteria to the right box and click OK to send the dupes that correspond the " "best to these criteria to their respective group's " "reference position. Read the help file for more information." ) self.promptLabel = QLabel(msg) self.promptLabel.setWordWrap(True) self.categoryCombobox = QComboBox() self.criteriaListView = QListView() self.criteriaListView.setSelectionMode(QAbstractItemView.ExtendedSelection) self.addCriteriaButton = QPushButton(self.style().standardIcon(QStyle.SP_ArrowRight), "") self.removeCriteriaButton = QPushButton(self.style().standardIcon(QStyle.SP_ArrowLeft), "") self.prioritizationListView = QListView() self.prioritizationListView.setAcceptDrops(True) self.prioritizationListView.setDragEnabled(True) self.prioritizationListView.setDragDropMode(QAbstractItemView.InternalMove) self.prioritizationListView.setSelectionBehavior(QAbstractItemView.SelectRows) self.prioritizationListView.setSelectionMode(QAbstractItemView.ExtendedSelection) self.buttonBox = QDialogButtonBox() self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) # layout self.mainLayout = QVBoxLayout(self) self.mainLayout.addWidget(self.promptLabel) self.splitter = QSplitter() sp = self.splitter.sizePolicy() sp.setVerticalPolicy(QSizePolicy.Expanding) self.splitter.setSizePolicy(sp) self.leftSide = QWidget() self.leftWidgetsLayout = QVBoxLayout() self.leftWidgetsLayout.addWidget(self.categoryCombobox) self.leftWidgetsLayout.addWidget(self.criteriaListView) self.leftSide.setLayout(self.leftWidgetsLayout) self.splitter.addWidget(self.leftSide) self.rightSide = QWidget() self.rightWidgetsLayout = QHBoxLayout() self.addRemoveButtonsLayout = QVBoxLayout() self.addRemoveButtonsLayout.addItem(vertical_spacer()) self.addRemoveButtonsLayout.addWidget(self.addCriteriaButton) self.addRemoveButtonsLayout.addWidget(self.removeCriteriaButton) self.addRemoveButtonsLayout.addItem(vertical_spacer()) self.rightWidgetsLayout.addLayout(self.addRemoveButtonsLayout) self.rightWidgetsLayout.addWidget(self.prioritizationListView) self.rightSide.setLayout(self.rightWidgetsLayout) self.splitter.addWidget(self.rightSide) self.mainLayout.addWidget(self.splitter) self.mainLayout.addWidget(self.buttonBox) dupeguru-4.3.1/qt/problem_dialog.py000066400000000000000000000057751426171743600173650ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2010-04-12 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QSpacerItem, QSizePolicy, QLabel, QTableView, QAbstractItemView, ) from qt.util import move_to_screen_center from hscommon.trans import trget from qt.problem_table import ProblemTable tr = trget("ui") class ProblemDialog(QDialog): def __init__(self, parent, model, **kwargs): flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint super().__init__(parent, flags, **kwargs) self._setupUi() self.model = model self.model.view = self self.table = ProblemTable(self.model.problem_table, view=self.tableView) self.revealButton.clicked.connect(self.model.reveal_selected_dupe) self.closeButton.clicked.connect(self.accept) def _setupUi(self): self.setWindowTitle(tr("Problems!")) self.resize(413, 323) self.verticalLayout = QVBoxLayout(self) self.label = QLabel(self) msg = tr( "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " "removed from your results." ) self.label.setText(msg) self.label.setWordWrap(True) self.verticalLayout.addWidget(self.label) self.tableView = QTableView(self) self.tableView.setEditTriggers(QAbstractItemView.NoEditTriggers) self.tableView.setSelectionMode(QAbstractItemView.SingleSelection) self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) self.tableView.setShowGrid(False) self.tableView.horizontalHeader().setStretchLastSection(True) self.tableView.verticalHeader().setDefaultSectionSize(18) self.tableView.verticalHeader().setHighlightSections(False) self.verticalLayout.addWidget(self.tableView) self.horizontalLayout = QHBoxLayout() self.revealButton = QPushButton(self) self.revealButton.setText(tr("Reveal Selected")) self.horizontalLayout.addWidget(self.revealButton) spacer_item = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacer_item) self.closeButton = QPushButton(self) self.closeButton.setText(tr("Close")) self.closeButton.setDefault(True) self.horizontalLayout.addWidget(self.closeButton) self.verticalLayout.addLayout(self.horizontalLayout) def showEvent(self, event): # have to do this here as the frameGeometry is not correct until shown move_to_screen_center(self) super().showEvent(event) dupeguru-4.3.1/qt/problem_table.py000066400000000000000000000013271426171743600172020ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2010-04-12 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from qt.column import Column from qt.table import Table class ProblemTable(Table): COLUMNS = [ Column("path", default_width=150), Column("msg", default_width=150), ] def __init__(self, model, view, **kwargs): super().__init__(model, view, **kwargs) # we have to prevent Return from initiating editing. # self.view.editSelected = lambda: None dupeguru-4.3.1/qt/progress_window.py000066400000000000000000000046361426171743600176340ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import Qt, QTimer from PyQt5.QtWidgets import QProgressDialog from hscommon.trans import tr class ProgressWindow: def __init__(self, parent, model): self._window = None self.parent = parent self.model = model model.view = self # We don't have access to QProgressDialog's labels directly, so we se the model label's view # to self and we'll refresh them together. self.model.jobdesc_textfield.view = self self.model.progressdesc_textfield.view = self # --- Callbacks def refresh(self): # Labels if self._window is not None: self._window.setWindowTitle(self.model.jobdesc_textfield.text) self._window.setLabelText(self.model.progressdesc_textfield.text) def set_progress(self, last_progress): if self._window is not None: if last_progress < 0: self._window.setRange(0, 0) else: self._window.setRange(0, 100) self._window.setValue(last_progress) def show(self): flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint self._window = QProgressDialog("", tr("Cancel"), 0, 100, self.parent, flags) self._window.setModal(True) self._window.setAutoReset(False) self._window.setAutoClose(False) self._timer = QTimer(self._window) self._timer.timeout.connect(self.model.pulse) self._window.show() self._window.canceled.connect(self.model.cancel) self._timer.start(500) def close(self): # it seems it is possible for close to be called without a corresponding # show, only perform a close if there is a window to close if self._window is not None: self._timer.stop() del self._timer # For some weird reason, canceled() signal is sent upon close, whether the user canceled # or not. If we don't want a false cancellation, we have to disconnect it. self._window.canceled.disconnect() self._window.close() self._window.setParent(None) self._window = None dupeguru-4.3.1/qt/radio_box.py000066400000000000000000000054711426171743600163450ustar00rootroot00000000000000# Created On: 2010-06-02 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QWidget, QHBoxLayout, QRadioButton from qt.util import horizontal_spacer class RadioBox(QWidget): def __init__(self, parent=None, items=None, spread=True, **kwargs): # If spread is False, insert a spacer in the layout so that the items don't use all the # space they're given but rather align left. if items is None: items = [] super().__init__(parent, **kwargs) self._buttons = [] self._labels = items self._selected_index = 0 self._spacer = horizontal_spacer() if not spread else None self._layout = QHBoxLayout(self) self._update_buttons() # --- Private def _update_buttons(self): if self._spacer is not None: self._layout.removeItem(self._spacer) to_remove = self._buttons[len(self._labels) :] for button in to_remove: self._layout.removeWidget(button) button.setParent(None) del self._buttons[len(self._labels) :] to_add = self._labels[len(self._buttons) :] for _ in to_add: button = QRadioButton(self) self._buttons.append(button) self._layout.addWidget(button) button.toggled.connect(self.buttonToggled) if self._spacer is not None: self._layout.addItem(self._spacer) if not self._buttons: return for button, label in zip(self._buttons, self._labels): button.setText(label) self._update_selection() def _update_selection(self): self._selected_index = max(0, min(self._selected_index, len(self._buttons) - 1)) selected = self._buttons[self._selected_index] selected.setChecked(True) # --- Event Handlers def buttonToggled(self): for i, button in enumerate(self._buttons): if button.isChecked(): self._selected_index = i self.itemSelected.emit(i) break # --- Signals itemSelected = pyqtSignal(int) # --- Properties @property def buttons(self): return self._buttons[:] @property def items(self): return self._labels[:] @items.setter def items(self, value): self._labels = value self._update_buttons() @property def selected_index(self): return self._selected_index @selected_index.setter def selected_index(self, value): self._selected_index = value self._update_selection() dupeguru-4.3.1/qt/recent.py000066400000000000000000000054231426171743600156540ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2009-11-12 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from collections import namedtuple from PyQt5.QtCore import pyqtSignal, QObject from PyQt5.QtWidgets import QAction from hscommon.trans import trget from hscommon.util import dedupe tr = trget("ui") MenuEntry = namedtuple("MenuEntry", "menu fixedItemCount") class Recent(QObject): def __init__(self, app, pref_name, max_item_count=10, **kwargs): super().__init__(**kwargs) self._app = app self._menuEntries = [] self._prefName = pref_name self._maxItemCount = max_item_count self._items = [] self._loadFromPrefs() self._app.willSavePrefs.connect(self._saveToPrefs) # --- Private def _loadFromPrefs(self): items = getattr(self._app.prefs, self._prefName) if not isinstance(items, list): items = [] self._items = items def _insertItem(self, item): self._items = dedupe([item] + self._items)[: self._maxItemCount] def _refreshMenu(self, menu_entry): menu, fixed_item_count = menu_entry for action in menu.actions()[fixed_item_count:]: menu.removeAction(action) for item in self._items: action = QAction(item, menu) action.setData(item) action.triggered.connect(self.menuItemWasClicked) menu.addAction(action) menu.addSeparator() action = QAction(tr("Clear List"), menu) action.triggered.connect(self.clear) menu.addAction(action) def _refreshAllMenus(self): for menu_entry in self._menuEntries: self._refreshMenu(menu_entry) def _saveToPrefs(self): setattr(self._app.prefs, self._prefName, self._items) # --- Public def addMenu(self, menu): menu_entry = MenuEntry(menu, len(menu.actions())) self._menuEntries.append(menu_entry) self._refreshMenu(menu_entry) def clear(self): self._items = [] self._refreshAllMenus() self.itemsChanged.emit() def insertItem(self, item): self._insertItem(str(item)) self._refreshAllMenus() self.itemsChanged.emit() def isEmpty(self): return not bool(self._items) # --- Event Handlers def menuItemWasClicked(self): action = self.sender() if action is not None: item = action.data() self.mustOpenItem.emit(item) self._refreshAllMenus() # --- Signals mustOpenItem = pyqtSignal(str) itemsChanged = pyqtSignal() dupeguru-4.3.1/qt/result_window.py000066400000000000000000000456131426171743600173060ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2009-04-25 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import Qt, QRect from PyQt5.QtWidgets import ( QMainWindow, QMenu, QLabel, QFileDialog, QMenuBar, QWidget, QVBoxLayout, QAbstractItemView, QStatusBar, QDialog, QPushButton, QCheckBox, QDesktopWidget, ) from hscommon.trans import trget from qt.util import move_to_screen_center, horizontal_wrap, create_actions from qt.search_edit import SearchEdit from core.app import AppMode from qt.results_model import ResultsView from qt.stats_label import StatsLabel from qt.prioritize_dialog import PrioritizeDialog from qt.se.results_model import ResultsModel as ResultsModelStandard from qt.me.results_model import ResultsModel as ResultsModelMusic from qt.pe.results_model import ResultsModel as ResultsModelPicture tr = trget("ui") class ResultWindow(QMainWindow): def __init__(self, parent, app, **kwargs): super().__init__(parent, **kwargs) self.app = app self.specific_actions = set() self._setupUi() if app.model.app_mode == AppMode.PICTURE: MODEL_CLASS = ResultsModelPicture elif app.model.app_mode == AppMode.MUSIC: MODEL_CLASS = ResultsModelMusic else: MODEL_CLASS = ResultsModelStandard self.resultsModel = MODEL_CLASS(self.app, self.resultsView) self.stats = StatsLabel(app.model.stats_label, self.statusLabel) self._update_column_actions_status() self.menuColumns.triggered.connect(self.columnToggled) self.resultsView.doubleClicked.connect(self.resultsDoubleClicked) self.resultsView.spacePressed.connect(self.resultsSpacePressed) self.detailsButton.clicked.connect(self.actionDetails.triggered) self.dupesOnlyCheckBox.stateChanged.connect(self.powerMarkerTriggered) self.deltaValuesCheckBox.stateChanged.connect(self.deltaTriggered) self.searchEdit.searchChanged.connect(self.searchChanged) self.app.willSavePrefs.connect(self.appWillSavePrefs) def _setupActions(self): # (name, shortcut, icon, desc, func) ACTIONS = [ ("actionDetails", "Ctrl+I", "", tr("Details"), self.detailsTriggered), ("actionActions", "", "", tr("Actions"), self.actionsTriggered), ( "actionPowerMarker", "Ctrl+1", "", tr("Show Dupes Only"), self.powerMarkerTriggered, ), ("actionDelta", "Ctrl+2", "", tr("Show Delta Values"), self.deltaTriggered), ( "actionDeleteMarked", "Ctrl+D", "", tr("Send Marked to Recycle Bin..."), self.deleteTriggered, ), ( "actionMoveMarked", "Ctrl+M", "", tr("Move Marked to..."), self.moveTriggered, ), ( "actionCopyMarked", "Ctrl+Shift+M", "", tr("Copy Marked to..."), self.copyTriggered, ), ( "actionRemoveMarked", "Ctrl+R", "", tr("Remove Marked from Results"), self.removeMarkedTriggered, ), ( "actionReprioritize", "", "", tr("Re-Prioritize Results..."), self.reprioritizeTriggered, ), ( "actionRemoveSelected", "Ctrl+Del", "", tr("Remove Selected from Results"), self.removeSelectedTriggered, ), ( "actionIgnoreSelected", "Ctrl+Shift+Del", "", tr("Add Selected to Ignore List"), self.addToIgnoreListTriggered, ), ( "actionMakeSelectedReference", "Ctrl+Space", "", tr("Make Selected into Reference"), self.app.model.make_selected_reference, ), ( "actionOpenSelected", "Ctrl+O", "", tr("Open Selected with Default Application"), self.openTriggered, ), ( "actionRevealSelected", "Ctrl+Shift+O", "", tr("Open Containing Folder of Selected"), self.revealTriggered, ), ( "actionRenameSelected", "F2", "", tr("Rename Selected"), self.renameTriggered, ), ("actionMarkAll", "Ctrl+A", "", tr("Mark All"), self.markAllTriggered), ( "actionMarkNone", "Ctrl+Shift+A", "", tr("Mark None"), self.markNoneTriggered, ), ( "actionInvertMarking", "Ctrl+Alt+A", "", tr("Invert Marking"), self.markInvertTriggered, ), ( "actionMarkSelected", Qt.Key_Space, "", tr("Mark Selected"), self.markSelectedTriggered, ), ( "actionExportToHTML", "", "", tr("Export To HTML"), self.app.model.export_to_xhtml, ), ( "actionExportToCSV", "", "", tr("Export To CSV"), self.app.model.export_to_csv, ), ( "actionSaveResults", "Ctrl+S", "", tr("Save Results..."), self.saveResultsTriggered, ), ( "actionInvokeCustomCommand", "Ctrl+Alt+I", "", tr("Invoke Custom Command"), self.app.invokeCustomCommand, ), ] create_actions(ACTIONS, self) self.actionDelta.setCheckable(True) self.actionPowerMarker.setCheckable(True) if self.app.main_window: # We use tab widgets in this case # Keep track of actions which should only be accessible from this class for action, _, _, _, _ in ACTIONS: self.specific_actions.add(getattr(self, action)) def _setupMenu(self): if not self.app.use_tabs: # we are our own QMainWindow, we need our own menu bar self.menubar = QMenuBar() # self.menuBar() works as well here self.menubar.setGeometry(QRect(0, 0, 630, 22)) self.menuFile = QMenu(self.menubar) self.menuFile.setTitle(tr("File")) self.menuMark = QMenu(self.menubar) self.menuMark.setTitle(tr("Mark")) self.menuActions = QMenu(self.menubar) self.menuActions.setTitle(tr("Actions")) self.menuColumns = QMenu(self.menubar) self.menuColumns.setTitle(tr("Columns")) self.menuView = QMenu(self.menubar) self.menuView.setTitle(tr("View")) self.menuHelp = QMenu(self.menubar) self.menuHelp.setTitle(tr("Help")) self.setMenuBar(self.menubar) menubar = self.menubar else: # we are part of a tab widget, we populate its window's menubar instead self.menuFile = self.app.main_window.menuFile self.menuMark = self.app.main_window.menuMark self.menuActions = self.app.main_window.menuActions self.menuColumns = self.app.main_window.menuColumns self.menuView = self.app.main_window.menuView self.menuHelp = self.app.main_window.menuHelp menubar = self.app.main_window.menubar self.menuActions.addAction(self.actionDeleteMarked) self.menuActions.addAction(self.actionMoveMarked) self.menuActions.addAction(self.actionCopyMarked) self.menuActions.addAction(self.actionRemoveMarked) self.menuActions.addAction(self.actionReprioritize) self.menuActions.addSeparator() self.menuActions.addAction(self.actionRemoveSelected) self.menuActions.addAction(self.actionIgnoreSelected) self.menuActions.addAction(self.actionMakeSelectedReference) self.menuActions.addSeparator() self.menuActions.addAction(self.actionOpenSelected) self.menuActions.addAction(self.actionRevealSelected) self.menuActions.addAction(self.actionInvokeCustomCommand) self.menuActions.addAction(self.actionRenameSelected) self.menuMark.addAction(self.actionMarkAll) self.menuMark.addAction(self.actionMarkNone) self.menuMark.addAction(self.actionInvertMarking) self.menuMark.addAction(self.actionMarkSelected) self.menuView.addAction(self.actionDetails) self.menuView.addSeparator() self.menuView.addAction(self.actionPowerMarker) self.menuView.addAction(self.actionDelta) self.menuView.addSeparator() if not self.app.use_tabs: self.menuView.addAction(self.app.actionIgnoreList) # This also pushes back the options entry to the bottom of the menu self.menuView.addSeparator() self.menuView.addAction(self.app.actionPreferences) self.menuHelp.addAction(self.app.actionShowHelp) self.menuHelp.addAction(self.app.actionOpenDebugLog) self.menuHelp.addAction(self.app.actionAbout) self.menuFile.addAction(self.actionSaveResults) self.menuFile.addAction(self.actionExportToHTML) self.menuFile.addAction(self.actionExportToCSV) self.menuFile.addSeparator() self.menuFile.addAction(self.app.actionQuit) menubar.addAction(self.menuFile.menuAction()) menubar.addAction(self.menuMark.menuAction()) menubar.addAction(self.menuActions.menuAction()) menubar.addAction(self.menuColumns.menuAction()) menubar.addAction(self.menuView.menuAction()) menubar.addAction(self.menuHelp.menuAction()) # Columns menu menu = self.menuColumns # Avoid adding duplicate actions in tab widget menu in case we recreated # the Result Window instance. if menu.actions(): menu.clear() self._column_actions = [] for index, (display, visible) in enumerate(self.app.model.result_table._columns.menu_items()): action = menu.addAction(display) action.setCheckable(True) action.setChecked(visible) action.item_index = index self._column_actions.append(action) menu.addSeparator() action = menu.addAction(tr("Reset to Defaults")) action.item_index = -1 # Action menu action_menu = QMenu(tr("Actions"), menubar) action_menu.addAction(self.actionDeleteMarked) action_menu.addAction(self.actionMoveMarked) action_menu.addAction(self.actionCopyMarked) action_menu.addAction(self.actionRemoveMarked) action_menu.addSeparator() action_menu.addAction(self.actionRemoveSelected) action_menu.addAction(self.actionIgnoreSelected) action_menu.addAction(self.actionMakeSelectedReference) action_menu.addSeparator() action_menu.addAction(self.actionOpenSelected) action_menu.addAction(self.actionRevealSelected) action_menu.addAction(self.actionInvokeCustomCommand) action_menu.addAction(self.actionRenameSelected) self.actionActions.setMenu(action_menu) self.actionsButton.setMenu(self.actionActions.menu()) def _setupUi(self): self.setWindowTitle(tr("{} Results").format(self.app.NAME)) self.resize(630, 514) self.centralwidget = QWidget(self) self.verticalLayout = QVBoxLayout(self.centralwidget) self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.verticalLayout.setSpacing(0) self.actionsButton = QPushButton(tr("Actions")) self.detailsButton = QPushButton(tr("Details")) self.dupesOnlyCheckBox = QCheckBox(tr("Dupes Only")) self.deltaValuesCheckBox = QCheckBox(tr("Delta Values")) self.searchEdit = SearchEdit() self.searchEdit.setMaximumWidth(300) self.horizontalLayout = horizontal_wrap( [ self.actionsButton, self.detailsButton, self.dupesOnlyCheckBox, self.deltaValuesCheckBox, None, self.searchEdit, 8, ] ) self.horizontalLayout.setSpacing(8) self.verticalLayout.addLayout(self.horizontalLayout) self.resultsView = ResultsView(self.centralwidget) self.resultsView.setSelectionMode(QAbstractItemView.ExtendedSelection) self.resultsView.setSelectionBehavior(QAbstractItemView.SelectRows) self.resultsView.setSortingEnabled(True) self.resultsView.setWordWrap(False) self.resultsView.verticalHeader().setVisible(False) h = self.resultsView.horizontalHeader() h.setHighlightSections(False) h.setSectionsMovable(True) h.setStretchLastSection(False) h.setDefaultAlignment(Qt.AlignLeft) self.verticalLayout.addWidget(self.resultsView) self.setCentralWidget(self.centralwidget) self._setupActions() self._setupMenu() self.statusbar = QStatusBar(self) self.statusbar.setSizeGripEnabled(True) self.setStatusBar(self.statusbar) self.statusLabel = QLabel(self) self.statusbar.addPermanentWidget(self.statusLabel, 1) if self.app.prefs.resultWindowIsMaximized: self.setWindowState(self.windowState() | Qt.WindowMaximized) else: if self.app.prefs.resultWindowRect is not None: self.setGeometry(self.app.prefs.resultWindowRect) # if not on any screen move to center of default screen # moves to center of closest screen if partially off screen frame = self.frameGeometry() if QDesktopWidget().screenNumber(self) == -1: move_to_screen_center(self) elif QDesktopWidget().availableGeometry(self).contains(frame) is False: frame.moveCenter(QDesktopWidget().availableGeometry(self).center()) self.move(frame.topLeft()) else: move_to_screen_center(self) # --- Private def _update_column_actions_status(self): # Update menu checked state menu_items = self.app.model.result_table._columns.menu_items() for action, (display, visible) in zip(self._column_actions, menu_items): action.setChecked(visible) # --- Actions def actionsTriggered(self): self.actionsButton.showMenu() def addToIgnoreListTriggered(self): self.app.model.add_selected_to_ignore_list() def copyTriggered(self): self.app.model.copy_or_move_marked(True) def deleteTriggered(self): self.app.model.delete_marked() def deltaTriggered(self, state=None): # The sender can be either the action or the checkbox, but both have a isChecked() method. self.resultsModel.delta_values = self.sender().isChecked() self.actionDelta.setChecked(self.resultsModel.delta_values) self.deltaValuesCheckBox.setChecked(self.resultsModel.delta_values) def detailsTriggered(self): self.app.show_details() def markAllTriggered(self): self.app.model.mark_all() def markInvertTriggered(self): self.app.model.mark_invert() def markNoneTriggered(self): self.app.model.mark_none() def markSelectedTriggered(self): self.app.model.toggle_selected_mark_state() def moveTriggered(self): self.app.model.copy_or_move_marked(False) def openTriggered(self): self.app.model.open_selected() def powerMarkerTriggered(self, state=None): # see deltaTriggered self.resultsModel.power_marker = self.sender().isChecked() self.actionPowerMarker.setChecked(self.resultsModel.power_marker) self.dupesOnlyCheckBox.setChecked(self.resultsModel.power_marker) def preferencesTriggered(self): self.app.show_preferences() def removeMarkedTriggered(self): self.app.model.remove_marked() def removeSelectedTriggered(self): self.app.model.remove_selected() def renameTriggered(self): index = self.resultsView.selectionModel().currentIndex() # Our index is the current row, with column set to 0. Our filename column is 1 and that's # what we want. index = index.sibling(index.row(), 1) self.resultsView.edit(index) def reprioritizeTriggered(self): dlg = PrioritizeDialog(self, self.app) result = dlg.exec() if result == QDialog.Accepted: dlg.model.perform_reprioritization() def revealTriggered(self): self.app.model.reveal_selected() def saveResultsTriggered(self): title = tr("Select a file to save your results to") files = tr("dupeGuru Results (*.dupeguru)") destination, chosen_filter = QFileDialog.getSaveFileName(self, title, "", files) if destination: if not destination.endswith(".dupeguru"): destination = f"{destination}.dupeguru" self.app.model.save_as(destination) self.app.recentResults.insertItem(destination) # --- Events def appWillSavePrefs(self): prefs = self.app.prefs prefs.resultWindowIsMaximized = self.isMaximized() prefs.resultWindowRect = self.geometry() def columnToggled(self, action): index = action.item_index if index == -1: self.app.model.result_table._columns.reset_to_defaults() self._update_column_actions_status() else: visible = self.app.model.result_table._columns.toggle_menu_item(index) action.setChecked(visible) def contextMenuEvent(self, event): self.actionActions.menu().exec_(event.globalPos()) def resultsDoubleClicked(self, model_index): self.app.model.open_selected() def resultsSpacePressed(self): self.app.model.toggle_selected_mark_state() def searchChanged(self): self.app.model.apply_filter(self.searchEdit.text()) def closeEvent(self, event): # this saves the location of the results window when it is closed self.appWillSavePrefs() dupeguru-4.3.1/qt/results_model.py000066400000000000000000000101641426171743600172530ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2009-04-23 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import Qt, pyqtSignal, QModelIndex from PyQt5.QtGui import QBrush, QFont, QFontMetrics from PyQt5.QtWidgets import QTableView from qt.table import Table class ResultsModel(Table): def __init__(self, app, view, **kwargs): model = app.model.result_table super().__init__(model, view, **kwargs) view.horizontalHeader().setSortIndicator(1, Qt.AscendingOrder) font = view.font() font.setPointSize(app.prefs.tableFontSize) view.setFont(font) fm = QFontMetrics(font) view.verticalHeader().setDefaultSectionSize(fm.height() + 2) app.willSavePrefs.connect(self.appWillSavePrefs) self.prefs = app.prefs def _getData(self, row, column, role): if column.name == "marked": if role == Qt.BackgroundRole and row.isref: return QBrush(self.prefs.result_table_ref_background_color) if role == Qt.CheckStateRole and row.markable: return Qt.Checked if row.marked else Qt.Unchecked return None if role == Qt.DisplayRole: data = row.data_delta if self.model.delta_values else row.data return data[column.name] elif role == Qt.ForegroundRole: if row.isref: return QBrush(self.prefs.result_table_ref_foreground_color) elif row.is_cell_delta(column.name): return QBrush(self.prefs.result_table_delta_foreground_color) elif role == Qt.BackgroundRole: if row.isref: return QBrush(self.prefs.result_table_ref_background_color) elif role == Qt.FontRole: font = QFont(self.view.font()) if self.prefs.reference_bold_font: font.setBold(row.isref) return font elif role == Qt.EditRole and column.name == "name": return row.data[column.name] return None def _getFlags(self, row, column): flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable if column.name == "marked": if row.markable: flags |= Qt.ItemIsUserCheckable elif column.name == "name": flags |= Qt.ItemIsEditable return flags def _setData(self, row, column, value, role): if role == Qt.CheckStateRole: if column.name == "marked": row.marked = bool(value) return True elif role == Qt.EditRole and column.name == "name": return self.model.rename_selected(value) return False def sort(self, column, order): column = self.model.COLUMNS[column] self.model.sort(column.name, order == Qt.AscendingOrder) # --- Properties @property def power_marker(self): return self.model.power_marker @power_marker.setter def power_marker(self, value): self.model.power_marker = value @property def delta_values(self): return self.model.delta_values @delta_values.setter def delta_values(self, value): self.model.delta_values = value # --- Events def appWillSavePrefs(self): self.model._columns.save_columns() # --- model --> view def invalidate_markings(self): # redraw view # HACK. this is the only way I found to update the widget without reseting everything self.view.scroll(0, 1) self.view.scroll(0, -1) class ResultsView(QTableView): # --- Override def keyPressEvent(self, event): if event.text() == " ": self.spacePressed.emit() return super().keyPressEvent(event) def mouseDoubleClickEvent(self, event): self.doubleClicked.emit(QModelIndex()) # We don't call the superclass' method because the default behavior is to rename the cell. # --- Signals spacePressed = pyqtSignal() dupeguru-4.3.1/qt/se/000077500000000000000000000000001426171743600144255ustar00rootroot00000000000000dupeguru-4.3.1/qt/se/__init__.py000066400000000000000000000000001426171743600165240ustar00rootroot00000000000000dupeguru-4.3.1/qt/se/details_dialog.py000066400000000000000000000016631426171743600177510ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import QSize from PyQt5.QtWidgets import QAbstractItemView from hscommon.trans import trget from qt.details_dialog import DetailsDialog as DetailsDialogBase from qt.details_table import DetailsTable tr = trget("ui") class DetailsDialog(DetailsDialogBase): def _setupUi(self): self.setWindowTitle(tr("Details")) self.resize(502, 186) self.setMinimumSize(QSize(200, 0)) self.tableView = DetailsTable(self) self.tableView.setAlternatingRowColors(True) self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) self.tableView.setShowGrid(False) self.setWidget(self.tableView) dupeguru-4.3.1/qt/se/preferences_dialog.py000066400000000000000000000151101426171743600206150ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import QSize from PyQt5.QtWidgets import ( QSpinBox, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget, ) from hscommon.trans import trget from core.app import AppMode from core.scanner import ScanType from qt.preferences_dialog import PreferencesDialogBase tr = trget("ui") class PreferencesDialog(PreferencesDialogBase): def _setupPreferenceWidgets(self): self._setupFilterHardnessBox() self.widgetsVLayout.addLayout(self.filterHardnessHLayout) self.widget = QWidget(self) self.widget.setMinimumSize(QSize(0, 136)) self.verticalLayout_4 = QVBoxLayout(self.widget) self._setupAddCheckbox("wordWeightingBox", tr("Word weighting"), self.widget) self.verticalLayout_4.addWidget(self.wordWeightingBox) self._setupAddCheckbox("matchSimilarBox", tr("Match similar words"), self.widget) self.verticalLayout_4.addWidget(self.matchSimilarBox) self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind"), self.widget) self.verticalLayout_4.addWidget(self.mixFileKindBox) self._setupAddCheckbox("useRegexpBox", tr("Use regular expressions when filtering"), self.widget) self.verticalLayout_4.addWidget(self.useRegexpBox) self._setupAddCheckbox( "removeEmptyFoldersBox", tr("Remove empty folders on delete or move"), self.widget, ) self.verticalLayout_4.addWidget(self.removeEmptyFoldersBox) self.horizontalLayout_2 = QHBoxLayout() self._setupAddCheckbox("ignoreSmallFilesBox", tr("Ignore files smaller than"), self.widget) self.horizontalLayout_2.addWidget(self.ignoreSmallFilesBox) self.sizeThresholdSpinBox = QSpinBox(self.widget) size_policy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed) size_policy.setHorizontalStretch(0) size_policy.setVerticalStretch(0) size_policy.setHeightForWidth(self.sizeThresholdSpinBox.sizePolicy().hasHeightForWidth()) self.sizeThresholdSpinBox.setSizePolicy(size_policy) self.sizeThresholdSpinBox.setMaximumSize(QSize(300, 16777215)) self.sizeThresholdSpinBox.setRange(0, 1000000) self.horizontalLayout_2.addWidget(self.sizeThresholdSpinBox) self.label_6 = QLabel(self.widget) self.label_6.setText(tr("KB")) self.horizontalLayout_2.addWidget(self.label_6) spacer_item1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout_2.addItem(spacer_item1) self.verticalLayout_4.addLayout(self.horizontalLayout_2) self.horizontalLayout_2a = QHBoxLayout() self._setupAddCheckbox("ignoreLargeFilesBox", tr("Ignore files larger than"), self.widget) self.horizontalLayout_2a.addWidget(self.ignoreLargeFilesBox) self.sizeSaturationSpinBox = QSpinBox(self.widget) size_policy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed) self.sizeSaturationSpinBox.setSizePolicy(size_policy) self.sizeSaturationSpinBox.setMaximumSize(QSize(300, 16777215)) self.sizeSaturationSpinBox.setRange(0, 1000000) self.horizontalLayout_2a.addWidget(self.sizeSaturationSpinBox) self.label_6a = QLabel(self.widget) self.label_6a.setText(tr("MB")) self.horizontalLayout_2a.addWidget(self.label_6a) spacer_item3 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout_2a.addItem(spacer_item3) self.verticalLayout_4.addLayout(self.horizontalLayout_2a) self.horizontalLayout_2b = QHBoxLayout() self._setupAddCheckbox( "bigFilePartialHashesBox", tr("Partially hash files bigger than"), self.widget, ) self.horizontalLayout_2b.addWidget(self.bigFilePartialHashesBox) self.bigSizeThresholdSpinBox = QSpinBox(self.widget) self.bigSizeThresholdSpinBox.setSizePolicy(size_policy) self.bigSizeThresholdSpinBox.setMaximumSize(QSize(300, 16777215)) self.bigSizeThresholdSpinBox.setRange(0, 1000000) self.horizontalLayout_2b.addWidget(self.bigSizeThresholdSpinBox) self.label_6b = QLabel(self.widget) self.label_6b.setText(tr("MB")) self.horizontalLayout_2b.addWidget(self.label_6b) spacer_item2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout_2b.addItem(spacer_item2) self.verticalLayout_4.addLayout(self.horizontalLayout_2b) self._setupAddCheckbox( "ignoreHardlinkMatches", tr("Ignore duplicates hardlinking to the same file"), self.widget, ) self.verticalLayout_4.addWidget(self.ignoreHardlinkMatches) self.widgetsVLayout.addWidget(self.widget) self._setupBottomPart() def _load(self, prefs, setchecked, section): setchecked(self.matchSimilarBox, prefs.match_similar) setchecked(self.wordWeightingBox, prefs.word_weighting) setchecked(self.ignoreSmallFilesBox, prefs.ignore_small_files) self.sizeThresholdSpinBox.setValue(prefs.small_file_threshold) setchecked(self.ignoreLargeFilesBox, prefs.ignore_large_files) self.sizeSaturationSpinBox.setValue(prefs.large_file_threshold) setchecked(self.bigFilePartialHashesBox, prefs.big_file_partial_hashes) self.bigSizeThresholdSpinBox.setValue(prefs.big_file_size_threshold) # Update UI state based on selected scan type scan_type = prefs.get_scan_type(AppMode.STANDARD) word_based = scan_type == ScanType.FILENAME self.filterHardnessSlider.setEnabled(word_based) self.matchSimilarBox.setEnabled(word_based) self.wordWeightingBox.setEnabled(word_based) def _save(self, prefs, ischecked): prefs.match_similar = ischecked(self.matchSimilarBox) prefs.word_weighting = ischecked(self.wordWeightingBox) prefs.ignore_small_files = ischecked(self.ignoreSmallFilesBox) prefs.small_file_threshold = self.sizeThresholdSpinBox.value() prefs.ignore_large_files = ischecked(self.ignoreLargeFilesBox) prefs.large_file_threshold = self.sizeSaturationSpinBox.value() prefs.big_file_partial_hashes = ischecked(self.bigFilePartialHashesBox) prefs.big_file_size_threshold = self.bigSizeThresholdSpinBox.value() dupeguru-4.3.1/qt/se/results_model.py000066400000000000000000000015101426171743600176550ustar00rootroot00000000000000# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from qt.column import Column from qt.results_model import ResultsModel as ResultsModelBase class ResultsModel(ResultsModelBase): COLUMNS = [ Column("marked", default_width=30), Column("name", default_width=200), Column("folder_path", default_width=180), Column("size", default_width=60), Column("extension", default_width=40), Column("mtime", default_width=120), Column("percentage", default_width=60), Column("words", default_width=120), Column("dupe_count", default_width=80), ] dupeguru-4.3.1/qt/search_edit.py000066400000000000000000000105541426171743600166470ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2009-12-10 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import pyqtSignal, Qt from PyQt5.QtGui import QIcon, QPixmap, QPainter, QPalette from PyQt5.QtWidgets import QToolButton, QLineEdit, QStyle, QStyleOptionFrame from hscommon.trans import trget tr = trget("ui") # IMPORTANT: For this widget to work propertly, you have to add "search_clear_13" from the # "images" folder in your resources. class LineEditButton(QToolButton): def __init__(self, parent, **kwargs): super().__init__(parent, **kwargs) pixmap = QPixmap(":/search_clear_13") self.setIcon(QIcon(pixmap)) self.setIconSize(pixmap.size()) self.setCursor(Qt.ArrowCursor) self.setPopupMode(QToolButton.InstantPopup) stylesheet = "QToolButton { border: none; padding: 0px; }" self.setStyleSheet(stylesheet) class ClearableEdit(QLineEdit): def __init__(self, parent=None, is_clearable=True, **kwargs): super().__init__(parent, **kwargs) self._is_clearable = is_clearable if is_clearable: self._clearButton = LineEditButton(self) frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) padding_right = self._clearButton.sizeHint().width() + frame_width + 1 stylesheet = f"QLineEdit {{ padding-right:{padding_right}px; }}" self.setStyleSheet(stylesheet) self._updateClearButton() self._clearButton.clicked.connect(self._clearSearch) self.textChanged.connect(self._textChanged) # --- Private def _clearSearch(self): self.clear() def _updateClearButton(self): self._clearButton.setVisible(self._hasClearableContent()) def _hasClearableContent(self): return bool(self.text()) # --- QLineEdit overrides def resizeEvent(self, event): if self._is_clearable: frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) rect = self.rect() right_hint = self._clearButton.sizeHint() right_x = rect.right() - frame_width - right_hint.width() right_y = (rect.bottom() - right_hint.height()) // 2 self._clearButton.move(right_x, right_y) # --- Event Handlers def _textChanged(self, text): if self._is_clearable: self._updateClearButton() class SearchEdit(ClearableEdit): def __init__(self, parent=None, immediate=False): # immediate: send searchChanged signals at each keystroke. ClearableEdit.__init__(self, parent, is_clearable=True) self.inactiveText = tr("Search...") self.immediate = immediate self.returnPressed.connect(self._returnPressed) # --- Overrides def _clearSearch(self): ClearableEdit._clearSearch(self) self.searchChanged.emit() def _textChanged(self, text): ClearableEdit._textChanged(self, text) if self.immediate: self.searchChanged.emit() def keyPressEvent(self, event): key = event.key() if key == Qt.Key_Escape: self._clearSearch() else: ClearableEdit.keyPressEvent(self, event) def paintEvent(self, event): ClearableEdit.paintEvent(self, event) if not bool(self.text()) and self.inactiveText and not self.hasFocus(): panel = QStyleOptionFrame() self.initStyleOption(panel) text_rect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self) left_margin = 2 right_margin = self._clearButton.iconSize().width() text_rect.adjust(left_margin, 0, -right_margin, 0) painter = QPainter(self) disabled_color = self.palette().brush(QPalette.Disabled, QPalette.Text).color() painter.setPen(disabled_color) painter.drawText(text_rect, Qt.AlignLeft | Qt.AlignVCenter, self.inactiveText) # --- Event Handlers def _returnPressed(self): if not self.immediate: self.searchChanged.emit() # --- Signals searchChanged = pyqtSignal() # Emitted when return is pressed or when the test is cleared dupeguru-4.3.1/qt/selectable_list.py000066400000000000000000000064241426171743600175340ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2011-09-06 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import Qt, QAbstractListModel, QItemSelection, QItemSelectionModel class SelectableList(QAbstractListModel): def __init__(self, model, view, **kwargs): super().__init__(**kwargs) self._updating = False self.view = view self.model = model self.view.setModel(self) self.model.view = self # --- Override def data(self, index, role): if not index.isValid(): return None # We need EditRole for QComboBoxes with setEditable(True) if role in {Qt.DisplayRole, Qt.EditRole}: return self.model[index.row()] return None def rowCount(self, index): if index.isValid(): return 0 return len(self.model) # --- Virtual def _updateSelection(self): raise NotImplementedError() def _restoreSelection(self): raise NotImplementedError() # --- model --> view def refresh(self): self._updating = True self.beginResetModel() self.endResetModel() self._updating = False self._restoreSelection() def update_selection(self): self._restoreSelection() class ComboboxModel(SelectableList): def __init__(self, model, view, **kwargs): super().__init__(model, view, **kwargs) self.view.currentIndexChanged[int].connect(self.selectionChanged) # --- Override def _updateSelection(self): index = self.view.currentIndex() if index != self.model.selected_index: self.model.select(index) def _restoreSelection(self): index = self.model.selected_index if index is not None: self.view.setCurrentIndex(index) # --- Events def selectionChanged(self, index): if not self._updating: self._updateSelection() class ListviewModel(SelectableList): def __init__(self, model, view, **kwargs): super().__init__(model, view, **kwargs) self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged) # --- Override def _updateSelection(self): new_indexes = [modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()] if new_indexes != self.model.selected_indexes: self.model.select(new_indexes) def _restoreSelection(self): new_selection = QItemSelection() for index in self.model.selected_indexes: new_selection.select(self.createIndex(index, 0), self.createIndex(index, 0)) self.view.selectionModel().select(new_selection, QItemSelectionModel.ClearAndSelect) if len(new_selection.indexes()): current_index = new_selection.indexes()[0] self.view.selectionModel().setCurrentIndex(current_index, QItemSelectionModel.Current) self.view.scrollTo(current_index) # --- Events def selectionChanged(self, index): if not self._updating: self._updateSelection() dupeguru-4.3.1/qt/stats_label.py000066400000000000000000000010331426171743600166620ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2010-02-12 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html class StatsLabel: def __init__(self, model, view): self.view = view self.model = model self.model.view = self def refresh(self): self.view.setText(self.model.display) dupeguru-4.3.1/qt/tabbed_window.py000066400000000000000000000337431426171743600172120ustar00rootroot00000000000000# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from PyQt5.QtCore import QRect, pyqtSlot, Qt, QEvent from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QMainWindow, QTabWidget, QMenu, QTabBar, QStackedWidget, ) from hscommon.trans import trget from qt.util import move_to_screen_center, create_actions from qt.directories_dialog import DirectoriesDialog from qt.result_window import ResultWindow from qt.ignore_list_dialog import IgnoreListDialog from qt.exclude_list_dialog import ExcludeListDialog tr = trget("ui") class TabWindow(QMainWindow): def __init__(self, app, **kwargs): super().__init__(None, **kwargs) self.app = app self.pages = {} # This is currently not used anywhere self.menubar = None self.menuList = set() self.last_index = -1 self.previous_widget_actions = set() self._setupUi() self.app.willSavePrefs.connect(self.appWillSavePrefs) def _setupActions(self): # (name, shortcut, icon, desc, func) ACTIONS = [ ( "actionToggleTabs", "", "", tr("Show tab bar"), self.toggleTabBar, ), ] create_actions(ACTIONS, self) self.actionToggleTabs.setCheckable(True) self.actionToggleTabs.setChecked(True) def _setupUi(self): self.setWindowTitle(self.app.NAME) self.resize(640, 480) self.tabWidget = QTabWidget() # self.tabWidget.setTabPosition(QTabWidget.South) self.tabWidget.setContentsMargins(0, 0, 0, 0) # self.tabWidget.setTabBarAutoHide(True) # This gets rid of the annoying margin around the TabWidget: self.tabWidget.setDocumentMode(True) self._setupActions() self._setupMenu() # This should be the same as self.centralWidget.setLayout(self.verticalLayout) self.verticalLayout = QVBoxLayout(self.tabWidget) # self.verticalLayout.addWidget(self.tabWidget) self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.tabWidget.setTabsClosable(True) self.setCentralWidget(self.tabWidget) # only for QMainWindow self.tabWidget.currentChanged.connect(self.updateMenuBar) self.tabWidget.tabCloseRequested.connect(self.onTabCloseRequested) self.updateMenuBar(self.tabWidget.currentIndex()) self.restoreGeometry() def restoreGeometry(self): if self.app.prefs.mainWindowRect is not None: self.setGeometry(self.app.prefs.mainWindowRect) if self.app.prefs.mainWindowIsMaximized: self.showMaximized() def _setupMenu(self): """Setup the menubar boiler plates which will be filled by the underlying tab's widgets whenever they are instantiated.""" self.menubar = self.menuBar() # QMainWindow, similar to just QMenuBar() here # self.setMenuBar(self.menubar) # already set if QMainWindow class self.menubar.setGeometry(QRect(0, 0, 100, 22)) self.menuFile = QMenu(self.menubar) self.menuFile.setTitle(tr("File")) self.menuMark = QMenu(self.menubar) self.menuMark.setTitle(tr("Mark")) self.menuActions = QMenu(self.menubar) self.menuActions.setTitle(tr("Actions")) self.menuColumns = QMenu(self.menubar) self.menuColumns.setTitle(tr("Columns")) self.menuView = QMenu(self.menubar) self.menuView.setTitle(tr("View")) self.menuHelp = QMenu(self.menubar) self.menuHelp.setTitle(tr("Help")) self.menuView.addAction(self.actionToggleTabs) self.menuView.addSeparator() self.menuList.add(self.menuFile) self.menuList.add(self.menuMark) self.menuList.add(self.menuActions) self.menuList.add(self.menuColumns) self.menuList.add(self.menuView) self.menuList.add(self.menuHelp) @pyqtSlot(int) def updateMenuBar(self, page_index=-1): if page_index < 0: return current_index = self.getCurrentIndex() active_widget = self.getWidgetAtIndex(current_index) if self.last_index < 0: self.last_index = current_index self.previous_widget_actions = active_widget.specific_actions return page_type = type(active_widget).__name__ for menu in self.menuList: if menu is self.menuColumns or menu is self.menuActions or menu is self.menuMark: if not isinstance(active_widget, ResultWindow): menu.setEnabled(False) continue else: menu.setEnabled(True) for action in menu.actions(): if action not in active_widget.specific_actions: if action in self.previous_widget_actions: action.setEnabled(False) continue action.setEnabled(True) self.app.directories_dialog.actionShowResultsWindow.setEnabled( False if page_type == "ResultWindow" else self.app.resultWindow is not None ) self.app.actionIgnoreList.setEnabled( True if self.app.ignoreListDialog is not None and not page_type == "IgnoreListDialog" else False ) self.app.actionDirectoriesWindow.setEnabled(False if page_type == "DirectoriesDialog" else True) self.app.actionExcludeList.setEnabled( True if self.app.excludeListDialog is not None and not page_type == "ExcludeListDialog" else False ) self.previous_widget_actions = active_widget.specific_actions self.last_index = current_index def createPage(self, cls, **kwargs): app = kwargs.get("app", self.app) page = None if cls == "DirectoriesDialog": page = DirectoriesDialog(app) elif cls == "ResultWindow": parent = kwargs.get("parent", self) page = ResultWindow(parent, app) elif cls == "IgnoreListDialog": parent = kwargs.get("parent", self) model = kwargs.get("model") page = IgnoreListDialog(parent, model) page.accepted.connect(self.onDialogAccepted) elif cls == "ExcludeListDialog": app = kwargs.get("app", app) parent = kwargs.get("parent", self) model = kwargs.get("model") page = ExcludeListDialog(app, parent, model) page.accepted.connect(self.onDialogAccepted) self.pages[cls] = page # Not used, might remove return page def addTab(self, page, title, switch=False): # Warning: this supposedly takes ownership of the page index = self.tabWidget.addTab(page, title) if isinstance(page, DirectoriesDialog): self.tabWidget.tabBar().setTabButton(index, QTabBar.RightSide, None) if switch: self.setCurrentIndex(index) return index def showTab(self, page): index = self.indexOfWidget(page) self.setCurrentIndex(index) def indexOfWidget(self, widget): return self.tabWidget.indexOf(widget) def setCurrentIndex(self, index): return self.tabWidget.setCurrentIndex(index) def removeTab(self, index): return self.tabWidget.removeTab(index) def isTabVisible(self, index): return self.tabWidget.isTabVisible(index) def getCurrentIndex(self): return self.tabWidget.currentIndex() def getWidgetAtIndex(self, index): return self.tabWidget.widget(index) def getCount(self): return self.tabWidget.count() # --- Events def appWillSavePrefs(self): # Right now this is useless since the first spawned dialog inside the # QTabWidget will assign its geometry after restoring it prefs = self.app.prefs prefs.mainWindowIsMaximized = self.isMaximized() if not self.isMaximized(): prefs.mainWindowRect = self.geometry() def showEvent(self, event): if not self.isMaximized(): # have to do this here as the frameGeometry is not correct until shown move_to_screen_center(self) super().showEvent(event) def changeEvent(self, event): if event.type() == QEvent.WindowStateChange and not self.isMaximized(): move_to_screen_center(self) super().changeEvent(event) def closeEvent(self, close_event): # Force closing of our tabbed widgets in reverse order so that the # directories dialog (which usually is at index 0) will be called last for index in range(self.getCount() - 1, -1, -1): self.getWidgetAtIndex(index).closeEvent(close_event) self.appWillSavePrefs() @pyqtSlot(int) def onTabCloseRequested(self, index): current_widget = self.getWidgetAtIndex(index) if isinstance(current_widget, DirectoriesDialog): # if we close this one, the application quits. Force user to use the # menu or shortcut. But this is useless if we don't have a button # set up to make a close request anyway. This check could be removed. return self.removeTab(index) @pyqtSlot() def onDialogAccepted(self): """Remove tabbed dialog when Accepted/Done (close button clicked).""" widget = self.sender() index = self.indexOfWidget(widget) if index > -1: self.removeTab(index) @pyqtSlot() def toggleTabBar(self): value = self.sender().isChecked() self.actionToggleTabs.setChecked(value) self.tabWidget.tabBar().setVisible(value) class TabBarWindow(TabWindow): """Implementation which uses a separate QTabBar and QStackedWidget. The Tab bar is placed next to the menu bar to save real estate.""" def __init__(self, app, **kwargs): super().__init__(app, **kwargs) def _setupUi(self): self.setWindowTitle(self.app.NAME) self.resize(640, 480) self.tabBar = QTabBar() self.verticalLayout = QVBoxLayout() self.verticalLayout.setContentsMargins(0, 0, 0, 0) self._setupActions() self._setupMenu() self.centralWidget = QWidget(self) self.setCentralWidget(self.centralWidget) self.stackedWidget = QStackedWidget() self.centralWidget.setLayout(self.verticalLayout) self.horizontalLayout = QHBoxLayout() self.horizontalLayout.addWidget(self.menubar, 0, Qt.AlignTop) self.horizontalLayout.addWidget(self.tabBar, 0, Qt.AlignTop) self.verticalLayout.addLayout(self.horizontalLayout) self.verticalLayout.addWidget(self.stackedWidget) self.tabBar.currentChanged.connect(self.showTabIndex) self.tabBar.tabCloseRequested.connect(self.onTabCloseRequested) self.stackedWidget.currentChanged.connect(self.updateMenuBar) self.stackedWidget.widgetRemoved.connect(self.onRemovedWidget) self.tabBar.setTabsClosable(True) self.restoreGeometry() def addTab(self, page, title, switch=True): stack_index = self.stackedWidget.addWidget(page) self.tabBar.insertTab(stack_index, title) if isinstance(page, DirectoriesDialog): self.tabBar.setTabButton(stack_index, QTabBar.RightSide, None) if switch: # switch to the added tab immediately upon creation self.setTabIndex(stack_index) return stack_index @pyqtSlot(int) def showTabIndex(self, index): # The tab bar's indices should be aligned with the stackwidget's if index >= 0 and index <= self.stackedWidget.count(): self.stackedWidget.setCurrentIndex(index) def indexOfWidget(self, widget): # Warning: this may return -1 if widget is not a child of stackedwidget return self.stackedWidget.indexOf(widget) def setCurrentIndex(self, tab_index): self.setTabIndex(tab_index) # The signal will handle switching the stackwidget's widget # self.stackedWidget.setCurrentWidget(self.stackedWidget.widget(tab_index)) def setCurrentWidget(self, widget): """Sets the current Tab on TabBar for this widget.""" self.tabBar.setCurrentIndex(self.indexOfWidget(widget)) @pyqtSlot(int) def setTabIndex(self, index): if index is None: return self.tabBar.setCurrentIndex(index) @pyqtSlot(int) def onRemovedWidget(self, index): self.removeTab(index) @pyqtSlot(int) def removeTab(self, index): """Remove the tab, but not the widget (it should already be removed)""" return self.tabBar.removeTab(index) @pyqtSlot(int) def removeWidget(self, widget): return self.stackedWidget.removeWidget(widget) def isTabVisible(self, index): return self.tabBar.isTabVisible(index) def getCurrentIndex(self): return self.stackedWidget.currentIndex() def getWidgetAtIndex(self, index): return self.stackedWidget.widget(index) def getCount(self): return self.stackedWidget.count() @pyqtSlot() def toggleTabBar(self): value = self.sender().isChecked() self.actionToggleTabs.setChecked(value) self.tabBar.setVisible(value) @pyqtSlot(int) def onTabCloseRequested(self, index): target_widget = self.getWidgetAtIndex(index) if isinstance(target_widget, DirectoriesDialog): # On MacOS, the tab has a close button even though we explicitely # set it to None in order to hide it. This should prevent # the "Directories" tab from closing by mistake. return # target_widget.close() # seems unnecessary # Removing the widget should trigger tab removal via the signal self.removeWidget(self.getWidgetAtIndex(index)) @pyqtSlot() def onDialogAccepted(self): """Remove tabbed dialog when Accepted/Done (close button clicked).""" widget = self.sender() self.removeWidget(widget) dupeguru-4.3.1/qt/table.py000066400000000000000000000127401426171743600154630ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2009-11-01 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import typing from PyQt5.QtCore import ( Qt, QAbstractTableModel, QModelIndex, QItemSelectionModel, QItemSelection, ) from qt.column import Columns, Column class Table(QAbstractTableModel): # Flags you want when index.isValid() is False. In those cases, _getFlags() is never called. INVALID_INDEX_FLAGS = Qt.ItemFlag.ItemIsEnabled COLUMNS: typing.List[Column] = [] def __init__(self, model, view, **kwargs): super().__init__(**kwargs) self.model = model self.view = view self.view.setModel(self) self.model.view = self if hasattr(self.model, "_columns"): self._columns = Columns(self.model._columns, self.COLUMNS, view.horizontalHeader()) self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged) def _updateModelSelection(self): # Takes the selection on the view's side and update the model with it. # an _updateViewSelection() call will normally result in an _updateModelSelection() call. # to avoid infinite loops, we check that the selection will actually change before calling # model.select() new_indexes = [modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()] if new_indexes != self.model.selected_indexes: self.model.select(new_indexes) def _updateViewSelection(self): # Takes the selection on the model's side and update the view with it. new_selection = QItemSelection() column_count = self.columnCount(QModelIndex()) for index in self.model.selected_indexes: new_selection.select(self.createIndex(index, 0), self.createIndex(index, column_count - 1)) self.view.selectionModel().select(new_selection, QItemSelectionModel.ClearAndSelect) if len(new_selection.indexes()): current_index = new_selection.indexes()[0] self.view.selectionModel().setCurrentIndex(current_index, QItemSelectionModel.Current) self.view.scrollTo(current_index) # --- Data Model methods # Virtual def _getData(self, row, column, role): if role in (Qt.DisplayRole, Qt.EditRole): attrname = column.name return row.get_cell_value(attrname) elif role == Qt.TextAlignmentRole: return column.alignment return None # Virtual def _getFlags(self, row, column): flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable if row.can_edit_cell(column.name): flags |= Qt.ItemIsEditable return flags # Virtual def _setData(self, row, column, value, role): if role == Qt.EditRole: attrname = column.name if attrname == "from": attrname = "from_" setattr(row, attrname, value) return True return False def columnCount(self, index): return self.model._columns.columns_count() def data(self, index, role): if not index.isValid(): return None row = self.model[index.row()] column = self.model._columns.column_by_index(index.column()) return self._getData(row, column, role) def flags(self, index): if not index.isValid(): return self.INVALID_INDEX_FLAGS row = self.model[index.row()] column = self.model._columns.column_by_index(index.column()) return self._getFlags(row, column) def headerData(self, section, orientation, role): if orientation != Qt.Horizontal: return None if section >= self.model._columns.columns_count(): return None column = self.model._columns.column_by_index(section) if role == Qt.DisplayRole: return column.display elif role == Qt.TextAlignmentRole: return column.alignment else: return None def revert(self): self.model.cancel_edits() def rowCount(self, index): if index.isValid(): return 0 return len(self.model) def setData(self, index, value, role): if not index.isValid(): return False row = self.model[index.row()] column = self.model._columns.column_by_index(index.column()) return self._setData(row, column, value, role) def sort(self, section, order): column = self.model._columns.column_by_index(section) attrname = column.name self.model.sort_by(attrname, desc=order == Qt.DescendingOrder) def submit(self): self.model.save_edits() return True # --- Events def selectionChanged(self, selected, deselected): self._updateModelSelection() # --- model --> view def refresh(self): self.beginResetModel() self.endResetModel() self._updateViewSelection() def show_selected_row(self): if self.model.selected_index is not None: self.view.showRow(self.model.selected_index) def start_editing(self): self.view.editSelected() def stop_editing(self): self.view.setFocus() # enough to stop editing def update_selection(self): self._updateViewSelection() dupeguru-4.3.1/qt/tree_model.py000066400000000000000000000141521426171743600165120ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2009-09-14 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import logging from PyQt5.QtCore import QAbstractItemModel, QModelIndex class NodeContainer: def __init__(self): self._subnodes = None self._ref2node = {} # --- Protected def _create_node(self, ref, row): # This returns a TreeNode instance from ref raise NotImplementedError() def _get_children(self): # This returns a list of ref instances, not TreeNode instances raise NotImplementedError() # --- Public def invalidate(self): # Invalidates cached data and list of subnodes without resetting ref2node. self._subnodes = None # --- Properties @property def subnodes(self): if self._subnodes is None: children = self._get_children() self._subnodes = [] for index, child in enumerate(children): if child in self._ref2node: node = self._ref2node[child] node.row = index else: node = self._create_node(child, index) self._ref2node[child] = node self._subnodes.append(node) return self._subnodes class TreeNode(NodeContainer): def __init__(self, model, parent, row): NodeContainer.__init__(self) self.model = model self.parent = parent self.row = row @property def index(self): return self.model.createIndex(self.row, 0, self) class RefNode(TreeNode): """Node pointing to a reference node. Use this if your Qt model wraps around a tree model that has iterable nodes. """ def __init__(self, model, parent, ref, row): TreeNode.__init__(self, model, parent, row) self.ref = ref def _create_node(self, ref, row): return RefNode(self.model, self, ref, row) def _get_children(self): return list(self.ref) # We use a specific TreeNode subclass to easily spot dummy nodes, especially in exception tracebacks. class DummyNode(TreeNode): pass class TreeModel(QAbstractItemModel, NodeContainer): def __init__(self, **kwargs): super().__init__(**kwargs) self._dummy_nodes = set() # dummy nodes' reference have to be kept to avoid segfault # --- Private def _create_dummy_node(self, parent, row): # In some cases (drag & drop row removal, to be precise), there's a temporary discrepancy # between a node's subnodes and what the model think it has. This leads to invalid indexes # being queried. Rather than going through complicated row removal crap, it's simpler to # just have rows with empty data replacing removed rows for the millisecond that the drag & # drop lasts. Override this to return a node of the correct type. return DummyNode(self, parent, row) def _last_index(self): """Index of the very last item in the tree.""" current_index = QModelIndex() row_count = self.rowCount(current_index) while row_count > 0: current_index = self.index(row_count - 1, 0, current_index) row_count = self.rowCount(current_index) return current_index # --- Overrides def index(self, row, column, parent): if not self.subnodes: return QModelIndex() node = parent.internalPointer() if parent.isValid() else self try: return self.createIndex(row, column, node.subnodes[row]) except IndexError: logging.debug( "Wrong tree index called (%r, %r, %r). Returning DummyNode", row, column, node, ) parent_node = parent.internalPointer() if parent.isValid() else None dummy = self._create_dummy_node(parent_node, row) self._dummy_nodes.add(dummy) return self.createIndex(row, column, dummy) def parent(self, index): if not index.isValid(): return QModelIndex() node = index.internalPointer() if node.parent is None: return QModelIndex() else: return self.createIndex(node.parent.row, 0, node.parent) def reset(self): super().beginResetModel() self.invalidate() self._ref2node = {} self._dummy_nodes = set() super().endResetModel() def rowCount(self, parent=QModelIndex()): node = parent.internalPointer() if parent.isValid() else self return len(node.subnodes) # --- Public def findIndex(self, row_path): """Returns the QModelIndex at `row_path` `row_path` is a sequence of node rows. For example, [1, 2, 1] is the 2nd child of the 3rd child of the 2nd child of the root. """ result = QModelIndex() for row in row_path: result = self.index(row, 0, result) return result @staticmethod def pathForIndex(index): reversed_path = [] while index.isValid(): reversed_path.append(index.row()) index = index.parent() return list(reversed(reversed_path)) def refreshData(self): """Updates the data on all nodes, but without having to perform a full reset. A full reset on a tree makes us lose selection and expansion states. When all we ant to do is to refresh the data on the nodes without adding or removing a node, a call on dataChanged() is better. But of course, Qt makes our life complicated by asking us topLeft and bottomRight indexes. This is a convenience method refreshing the whole tree. """ column_count = self.columnCount() top_left = self.index(0, 0, QModelIndex()) bottom_left = self._last_index() bottom_right = self.sibling(bottom_left.row(), column_count - 1, bottom_left) self.dataChanged.emit(top_left, bottom_right) dupeguru-4.3.1/qt/util.py000066400000000000000000000132251426171743600153500ustar00rootroot00000000000000# Created By: Virgil Dupras # Created On: 2011-02-01 # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import sys import io import os.path as op import os import logging from core.util import executable_folder from hscommon.util import first from hscommon.plat import ISWINDOWS from PyQt5.QtCore import QStandardPaths, QSettings from PyQt5.QtGui import QPixmap, QIcon, QGuiApplication from PyQt5.QtWidgets import ( QSpacerItem, QSizePolicy, QAction, QHBoxLayout, ) def move_to_screen_center(widget): frame = widget.frameGeometry() if QGuiApplication.screenAt(frame.center()) is None: # if center not on any screen use default screen screen = QGuiApplication.screens()[0].availableGeometry() else: screen = QGuiApplication.screenAt(frame.center()).availableGeometry() # moves to center of screen if partially off screen if screen.contains(frame) is False: # make sure the frame is not larger than screen # resize does not seem to take frame size into account (move does) widget.resize(frame.size().boundedTo(screen.size() - (frame.size() - widget.size()))) frame = widget.frameGeometry() frame.moveCenter(screen.center()) widget.move(frame.topLeft()) def vertical_spacer(size=None): if size: return QSpacerItem(1, size, QSizePolicy.Fixed, QSizePolicy.Fixed) else: return QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) def horizontal_spacer(size=None): if size: return QSpacerItem(size, 1, QSizePolicy.Fixed, QSizePolicy.Fixed) else: return QSpacerItem(1, 1, QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) def horizontal_wrap(widgets): """Wrap all widgets in `widgets` in a horizontal layout. If, instead of placing a widget in your list, you place an int or None, an horizontal spacer with the width corresponding to the int will be placed (0 or None means an expanding spacer). """ layout = QHBoxLayout() for widget in widgets: if widget is None or isinstance(widget, int): layout.addItem(horizontal_spacer(size=widget)) else: layout.addWidget(widget) return layout def create_actions(actions, target): # actions are list of (name, shortcut, icon, desc, func) for name, shortcut, icon, desc, func in actions: action = QAction(target) if icon: action.setIcon(QIcon(QPixmap(":/" + icon))) if shortcut: action.setShortcut(shortcut) action.setText(desc) action.triggered.connect(func) setattr(target, name, action) def set_accel_keys(menu): actions = menu.actions() titles = [a.text() for a in actions] available_characters = {c.lower() for s in titles for c in s if c.isalpha()} for action in actions: text = action.text() c = first(c for c in text if c.lower() in available_characters) if c is None: continue i = text.index(c) newtext = text[:i] + "&" + text[i:] available_characters.remove(c.lower()) action.setText(newtext) def get_appdata(portable=False): if portable: return op.join(executable_folder(), "data") else: return QStandardPaths.standardLocations(QStandardPaths.AppDataLocation)[0] class SysWrapper(io.IOBase): def write(self, s): if s.strip(): # don't log empty stuff logging.warning(s) def setup_qt_logging(level=logging.WARNING, log_to_stdout=False): # Under Qt, we log in "debug.log" in appdata. Moreover, when under cx_freeze, we have a # problem because sys.stdout and sys.stderr are None, so we need to replace them with a # wrapper that logs with the logging module. appdata = get_appdata() if not op.exists(appdata): os.makedirs(appdata) # Setup logging # Have to use full configuration over basicConfig as FileHandler encoding was not being set. filename = op.join(appdata, "debug.log") if not log_to_stdout else None log = logging.getLogger() handler = logging.FileHandler(filename, "a", "utf-8") formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") handler.setFormatter(formatter) log.addHandler(handler) if sys.stderr is None: # happens under a cx_freeze environment sys.stderr = SysWrapper() if sys.stdout is None: sys.stdout = SysWrapper() def escape_amp(s): # Returns `s` with escaped ampersand (& --> &&). QAction text needs to have & escaped because # that character is used to define "accel keys". return s.replace("&", "&&") def create_qsettings(): # Create a QSettings instance with the correct arguments. config_location = op.join(executable_folder(), "settings.ini") if op.isfile(config_location): settings = QSettings(config_location, QSettings.IniFormat) settings.setValue("Portable", True) elif ISWINDOWS: # On windows use an ini file in the AppDataLocation instead of registry if possible as it # makes it easier for a user to clear it out when there are issues. locations = QStandardPaths.standardLocations(QStandardPaths.AppDataLocation) if locations: settings = QSettings(op.join(locations[0], "settings.ini"), QSettings.IniFormat) else: settings = QSettings() settings.setValue("Portable", False) else: settings = QSettings() settings.setValue("Portable", False) return settings dupeguru-4.3.1/requirements-extra.txt000066400000000000000000000001101426171743600177670ustar00rootroot00000000000000pytest>=6,<7 flake8 black pyinstaller>=4.5,<5.0; sys_platform != 'linux'dupeguru-4.3.1/requirements.txt000066400000000000000000000003221426171743600166530ustar00rootroot00000000000000distro>=1.5.0 mutagen>=1.44.0 polib>=1.1.0 PyQt5 >=5.14.1,<6.0; sys_platform != 'linux' pywin32>=228; sys_platform == 'win32' semantic-version>=2.0.0,<3.0.0 Send2Trash>=1.3.0 sphinx>=3.0.0 xxhash>=3.0.0,<4.0.0 dupeguru-4.3.1/run.py000066400000000000000000000051041426171743600145500ustar00rootroot00000000000000#!/usr/bin/python3 # Copyright 2017 Virgil Dupras # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import sys import os.path as op import gc from PyQt5.QtCore import QCoreApplication from PyQt5.QtGui import QIcon, QPixmap from PyQt5.QtWidgets import QApplication from hscommon.trans import install_gettext_trans_under_qt from qt.error_report_dialog import install_excepthook from qt.util import setup_qt_logging, create_qsettings from qt import dg_rc # noqa: F401 from qt.platform import BASE_PATH from core import __version__, __appname__ # SIGQUIT is not defined on Windows if sys.platform == "win32": from signal import signal, SIGINT, SIGTERM SIGQUIT = SIGTERM else: from signal import signal, SIGINT, SIGTERM, SIGQUIT global dgapp dgapp = None def signal_handler(sig, frame): global dgapp if dgapp is None: return if sig in (SIGINT, SIGTERM, SIGQUIT): dgapp.SIGTERM.emit() def setup_signals(): signal(SIGINT, signal_handler) signal(SIGTERM, signal_handler) signal(SIGQUIT, signal_handler) def main(): app = QApplication(sys.argv) QCoreApplication.setOrganizationName("Hardcoded Software") QCoreApplication.setApplicationName(__appname__) QCoreApplication.setApplicationVersion(__version__) setup_qt_logging() settings = create_qsettings() lang = settings.value("Language") locale_folder = op.join(BASE_PATH, "locale") install_gettext_trans_under_qt(locale_folder, lang) # Handle OS signals setup_signals() # Let the Python interpreter runs every 500ms to handle signals. This is # required because Python cannot handle signals while the Qt event loop is # running. from PyQt5.QtCore import QTimer timer = QTimer() timer.start(500) timer.timeout.connect(lambda: None) # Many strings are translated at import time, so this is why we only import after the translator # has been installed from qt.app import DupeGuru app.setWindowIcon(QIcon(QPixmap(f":/{DupeGuru.LOGO_NAME}"))) global dgapp dgapp = DupeGuru() install_excepthook("https://github.com/arsenetar/dupeguru/issues") result = app.exec() # I was getting weird crashes when quitting under Windows, and manually deleting main app # references with gc.collect() in between seems to fix the problem. del dgapp gc.collect() del app gc.collect() return result if __name__ == "__main__": sys.exit(main()) dupeguru-4.3.1/setup.cfg000066400000000000000000000026671426171743600152260ustar00rootroot00000000000000[metadata] name = dupeGuru version = attr: core.__version__ url = https://github.com/arsenetar/dupeguru project_urls = Bug Reports = https://github.com/arsenetar/dupeguru/issues author = Andrew Senetar author_email = arsenetar@voltaicideas.net license = GPLv3 license_files = license description = dupeGuru is a tool to find duplicate files on your computer. long_description = file:README.md long_description_content_type = text/markdown classifiers = Development Status :: 5 - Production/Stable Intended Audience :: End Users/Desktop License :: OSI Approved :: GNU General Public License v3 (GPLv3) Operating System :: MacOS :: MacOS X Operating System :: Microsoft :: Windows Operating System :: POSIX Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3 :: Only Topic :: Desktop Environment :: File Managers [options] packages = find: python_requires = >=3.7 install_requires = Send2Trash>=1.3.0 mutagen>=1.45.1 distro>=1.5.0 PyQt5 >=5.14.1,<6.0; sys_platform != 'linux' pywin32>=228; sys_platform == 'win32' semantic-version>=2.0.0,<3.0.0 xxhash>=3.0.0,<4.0.0 setup_requires = sphinx>=3.0.0 polib>=1.1.0 tests_require = pytest >=6,<7 include_package_data = true [options.entry_points] console_scripts = dupeguru = run.py dupeguru-4.3.1/setup.nsi000066400000000000000000000262421426171743600152530ustar00rootroot00000000000000;============================================================================== ; dupeGuru Installer Script for Windows via NSIS ; ; When calling makensis use the following: ; makensis /DVERSIONMAJOR=x /DVERSIONMINOR=x /DVERSIONPATCH=x /DBITS=x \ ; /DSOURCEPATH=x ; NOTE: ; If SOURCEPATH is not set it will default to build (uses subdir based on app). ;============================================================================== Unicode true ; Compression Setting SetCompressor /SOLID lzma ; General Headers !include "FileFunc.nsh" !include "WinVer.nsh" !include "LogicLib.nsh" ;============================================================================== ; Configuration Defines ;============================================================================== ; Environment Defines !verbose push !verbose 4 !ifndef VERSIONMAJOR !echo "VERSIONMAJOR is NOT defined" !endif !ifndef VERSIONMINOR !echo "VERSIONMINOR is NOT defined" !endif !ifndef VERSIONPATCH !echo "VERSIONPATCH is NOT defined" !endif !ifndef BITS !echo "BITS is NOT defined" !endif !ifndef SOURCEPATH !echo "SOURCEPATH is NOT defined" !define SOURCEPATH "dist" !endif !ifndef VERSIONMAJOR | VERSIONMINOR | VERSIONPATCH | BITS | SOURCEPATH !error "Command line Defines missing use /DDEFINE=VALUE to define before script" !endif !verbose pop ; Application Specific Defines !define APPNAME "dupeGuru" !define COMPANYNAME "Hardcoded Software" !define DESCRIPTION "dupeGuru is a tool to find duplicate files on your computer." !define APPLICENSE "LICENSE" ; License is not in build directory !define APPICON "images\dgse_logo.ico" ; nor is the icon !define DISTDIR "dist" !define HELPURL "https://github.com/arsenetar/dupeguru/issues" !define UPDATEURL "https://dupeguru.voltaicideas.net/" !define ABOUTURL "https://dupeguru.voltaicideas.net/" ; Static Defines !define UNINSTALLREGBASE "Software\Microsoft\Windows\CurrentVersion\Uninstall" ; Derived Defines !define BASEREGKEY "Software\${COMPANYNAME}\${APPNAME}" ;without root key !define VENDORREGKEY "Software\${COMPANYNAME}" ;without root key !define UNINSTALLREG "${UNINSTALLREGBASE}\${APPNAME}" ;without root key !define INSTPATH "${COMPANYNAME}\${APPNAME}" ;without programs / appdata ; Global vars var StartMenuFolder var InstallSize ;============================================================================== ; Plugin Setup ;============================================================================== ; MultiUser Plugin - Allow single user or all install based on execution level !define MULTIUSER_EXECUTIONLEVEL Highest !define MULTIUSER_MUI !define MULTIUSER_INSTALLMODE_COMMANDLINE !define MULTIUSER_INSTALLMODE_INSTDIR "${INSTPATH}" ; without programs /appdata ; allow for next run of installer to automatically find install path and type !define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_KEY "${BASEREGKEY}" !define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME "InstallPath" !define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_KEY "${BASEREGKEY}" !define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME "InstallType" !if ${BITS} == "64" !define MULTIUSER_USE_PROGRAMFILES64 !endif !include MultiUser.nsh ; Modern UI 2 !include MUI2.nsh !define MUI_ICON "${APPICON}" !define MUI_ABORTWARNING !define MUI_UNABORTWARNING ;============================================================================== ; NSIS Variables ;============================================================================== Name "${APPNAME}" !system 'mkdir "${DISTDIR}"' OutFile "${DISTDIR}\${APPNAME}_win${BITS}_${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONPATCH}.exe" Icon "${APPICON}" ;============================================================================== ; Pages ;============================================================================== !insertmacro MUI_PAGE_WELCOME !insertmacro MUI_PAGE_LICENSE "${APPLICENSE}" !insertmacro MULTIUSER_PAGE_INSTALLMODE !insertmacro MUI_PAGE_DIRECTORY ; values for start menu page !define MUI_STARTMENUPAGE_REGISTRY_ROOT "SHCTX" ; uses shell context !define MUI_STARTMENUPAGE_REGISTRY_KEY "${BASEREGKEY}" !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" !insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder !insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_FINISH ; uninstaller pages !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES ;============================================================================== ; Languages ;============================================================================== !insertmacro MUI_LANGUAGE "English" ;first language is the default language !insertmacro MUI_LANGUAGE "French" !insertmacro MUI_LANGUAGE "German" !insertmacro MUI_LANGUAGE "Greek" !insertmacro MUI_LANGUAGE "Italian" !insertmacro MUI_LANGUAGE "Korean" !insertmacro MUI_LANGUAGE "Polish" !insertmacro MUI_LANGUAGE "Russian" !insertmacro MUI_LANGUAGE "Spanish" !insertmacro MUI_LANGUAGE "Ukrainian" !insertmacro MUI_LANGUAGE "Vietnamese" !insertmacro MUI_LANGUAGE "Dutch" !insertmacro MUI_LANGUAGE "Czech" ;!insertmacro MUI_LANGUAGE "Chinese" ; no NSIS builtin support ;!insertmacro MUI_LANGUAGE "Brazilian" ; no NSIS builtin support ;!insertmacro MUI_LANGUAGE "Armenian" ; requires UNICODE ;============================================================================== ; Reserve Files ;============================================================================== ; If you are using solid compression, files that are required before ; the actual installation should be stored first in the data block, ; because this will make your installer start faster. !insertmacro MUI_RESERVEFILE_LANGDLL ReserveFile /nonfatal "${NSISDIR}\Plugins\*.dll" ;reserve if needed ;============================================================================== ; Installer Sections ;============================================================================== Section "!Application" AppSec SetOutPath "$INSTDIR" ; set from result of installer pages ; Files to install File /r "${SOURCEPATH}\${APPNAME}-win${BITS}\*" ; Create Start Menu Items !insertmacro MUI_STARTMENU_WRITE_BEGIN Application CreateDirectory "$SMPROGRAMS\$StartMenuFolder" CreateShortcut "$SMPROGRAMS\$StartMenuFolder\${APPNAME}.lnk" "$INSTDIR\${APPNAME}-win${BITS}.exe" CreateShortcut "$SMPROGRAMS\$StartMenuFolder\Uninstall.lnk" "$INSTDIR\Uninstall.exe" !insertmacro MUI_STARTMENU_WRITE_END ; Store installation folder WriteRegStr SHCTX "${BASEREGKEY}" "InstallPath" $INSTDIR WriteRegStr SHCTX "${BASEREGKEY}" "InstallType" $MultiUser.InstallMode ; get installed size Push $R0 Push $R1 Push $R2 ${GetSize} "$INSTDIR" "/S=0K" $R0 $R1 $R2 ; look into locate IntFmt $InstallSize "0x%08X" $R0 Pop $R2 Pop $R1 Pop $R0 ; Set file association ReadRegStr $1 HKCR ".dupeguru" "" StrCmp $1 "" NoBackup ; is it empty StrCmp $1 "${APPNAME}.File" NoBackup ; is it our own WriteRegStr HKCR ".dupeguru" "backup_val" "$1" ; backup current value NoBackup: WriteRegStr HKCR ".dupeguru" "" "${APPNAME}.File" ; set our file association ReadRegStr $0 HKCR "${APPNAME}.File" "" StrCmp $0 "" 0 Skip WriteRegStr HKCR "${APPNAME}.File" "" "${APPNAME} File" WriteRegStr HKCR "${APPNAME}.File\shell" "" "open" WriteRegStr HKCR "${APPNAME}.File\DefaultIcon" "" "$INSTDIR\${APPNAME}-win${BITS}.exe,0" Skip: WriteRegStr HKCR "${APPNAME}.File\shell\open\command" "" '"$INSTDIR\${APPNAME}-win${BITS}.exe" "%1"' WriteRegStr HKCR "${APPNAME}.File\shell\edit" "" "Edit ${APPNAME} File" WriteRegStr HKCR "${APPNAME}.File\shell\edit\command" "" '"$INSTDIR\${APPNAME}-win${BITS}.exe" "%1"' ; Uninstall Entry WriteRegStr SHCTX "${UNINSTALLREG}" "DisplayName" "${APPNAME} ${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONPATCH}" WriteRegStr SHCTX "${UNINSTALLREG}" "DisplayVersion" "${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONPATCH}" WriteRegStr SHCTX "${UNINSTALLREG}" "DisplayIcon" "$INSTDIR\${APPNAME}.exe" WriteRegDWORD SHCTX "${UNINSTALLREG}" "VersionMajor" ${VERSIONMAJOR} WriteRegDWORD SHCTX "${UNINSTALLREG}" "VersionMinor" ${VERSIONMINOR} WriteRegDWORD SHCTX "${UNINSTALLREG}" "VersionPatch" ${VERSIONPATCH} WriteRegStr SHCTX "${UNINSTALLREG}" "Comments" "${APPNAME} installer" WriteRegStr SHCTX "${UNINSTALLREG}" "InstallLocation" "$INSTDIR" WriteRegStr SHCTX "${UNINSTALLREG}" "Publisher" "${COMPANYNAME}" WriteRegStr SHCTX "${UNINSTALLREG}" "Contact" "${HELPURL}" WriteRegStr SHCTX "${UNINSTALLREG}" "HelpLink" "${HELPURL}" WriteRegStr SHCTX "${UNINSTALLREG}" "URLUpdateInfo" "${UPDATEURL}" WriteRegStr SHCTX "${UNINSTALLREG}" "URLInfoAbout" "${ABOUTURL}" WriteRegDWORD SHCTX "${UNINSTALLREG}" "NoModify" 1 WriteRegDWORD SHCTX "${UNINSTALLREG}" "NoRepair" 1 WriteRegDWORD SHCTX "${UNINSTALLREG}" "EstimatedSize" $InstallSize WriteRegStr SHCTX "${UNINSTALLREG}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\" /$MultiUser.InstallMode" WriteRegStr SHCTX "${UNINSTALLREG}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /$MultiUser.InstallMode /S" ; Create uninstaller WriteUninstaller "$INSTDIR\Uninstall.exe" SectionEnd ;============================================================================== ; Descriptions ;============================================================================== ; Add descriptions as needed ;============================================================================== ; Uninstaller Sections ;============================================================================== Section "Uninstall" ; Remove Start Menu Folder !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuFolder RMDir /r "$SMPROGRAMS\$StartMenuFolder" ; Remove Files & Folders in Install Folder RMDir /r "$INSTDIR\core" RMDir /r "$INSTDIR\help" RMDir /r "$INSTDIR\PyQt5" RMDir /r "$INSTDIR\qt" RMDir /r "$INSTDIR\locale" Delete "$INSTDIR\*.exe" Delete "$INSTDIR\*.dll" Delete "$INSTDIR\*.pyd" Delete "$INSTDIR\*.zip" Delete "$INSTDIR\*.manifest" ; Remove Install Folder if empty RMDir "$INSTDIR" ReadRegStr $1 HKCR ".dupeguru" "" StrCmp $1 "${APPNAME}.File" 0 NotOwn ; only do this if we own it ReadRegStr $1 HKCR ".dupeguru" "backup_val" StrCmp $1 "" 0 Restore ; if backup="" then delete the whole key DeleteRegKey HKCR ".dupeGuru" Goto NotOwn Restore: WriteRegStr HKCR ".dupeguru" "" $1 DeleteRegValue HKCR ".dupeguru" "backup_val" NotOwn: DeleteRegKey HKCR "${APPNAME}.File" ;Delete key with association name settings ; Remove registry keys and vendor keys (if empty) DeleteRegKey SHCTX "${BASEREGKEY}" DeleteRegKey /ifempty SHCTX "${VENDORREGKEY}" DeleteRegKey SHCTX "${UNINSTALLREG}" SectionEnd ;============================================================================== ; Functions ;============================================================================== Function .onInit ${IfNot} ${AtLeastWin7} MessageBox MB_OK "Windows 7 and above required" Quit ${EndIf} !if ${BITS} == "64" SetRegView 64 !else SetRegView 32 !endif !insertmacro MULTIUSER_INIT ; it appears that the languages shown may not always be filtered correctly !define MUI_LANGDLL_ALLLANGUAGES !insertmacro MUI_LANGDLL_DISPLAY FunctionEnd Function un.onInit !if ${BITS} == "64" SetRegView 64 !else SetRegView 32 !endif !insertmacro MULTIUSER_UNINIT !insertmacro MUI_UNGETLANGUAGE FunctionEnddupeguru-4.3.1/setup.py000066400000000000000000000013621426171743600151060ustar00rootroot00000000000000from setuptools import setup, Extension from pathlib import Path exts = [ Extension( "core.pe._block", [ str(Path("core", "pe", "modules", "block.c")), str(Path("core", "pe", "modules", "common.c")), ], include_dirs=[str(Path("core", "pe", "modules"))], ), Extension( "core.pe._cache", [ str(Path("core", "pe", "modules", "cache.c")), str(Path("core", "pe", "modules", "common.c")), ], include_dirs=[str(Path("core", "pe", "modules"))], ), Extension("qt.pe._block_qt", [str(Path("qt", "pe", "modules", "block.c"))]), ] headers = [str(Path("core", "pe", "modules", "common.h"))] setup(ext_modules=exts, headers=headers) dupeguru-4.3.1/tox.ini000066400000000000000000000007621426171743600147120ustar00rootroot00000000000000[tox] envlist = py36,py37,py38,py39,py310 skipsdist = True skip_missing_interpreters = True [testenv] setenv = PYTHON="{envpython}" commands = python build.py --modules flake8 black --check . {posargs:py.test core hscommon} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-extra.txt [flake8] exclude = .tox,env,build,cocoalib,cocoa,help,./qt/dg_rc.py,cocoa/run_template.py,./pkg max-line-length = 120 select = C,E,F,W,B,B950 extend-ignore = E203, E501 dupeguru-4.3.1/win_version_info.temp000066400000000000000000000030411426171743600176340ustar00rootroot00000000000000# UTF-8 # # For more details about fixed file info 'ffi' see: # http://msdn.microsoft.com/en-us/library/ms646997.aspx VSVersionInfo( ffi=FixedFileInfo( # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) # Set not needed items to zero 0. filevers=({0}, {1}, {2}, 0), prodvers=({0}, {1}, {2}, 0), # Contains a bitmask that specifies the valid bits 'flags'r mask=0x3f, # Contains a bitmask that specifies the Boolean attributes of the file. flags=0x0, # The operating system for which this file was designed. # 0x4 - NT and there is no need to change it. OS=0x40004, # The general type of file. # 0x1 - the file is an application. fileType=0x1, # The function of the file. # 0x0 - the function is not defined for this fileType subtype=0x0, # Creation date and time stamp. date=(0, 0) ), kids=[ StringFileInfo( [ StringTable( u'040904B0', [StringStruct(u'CompanyName', u'Hardcoded Software'), StringStruct(u'FileDescription', u'dupeGuru is a tool to find duplicate files on your computer.'), StringStruct(u'FileVersion', u'{0}.{1}.{2}.0'), StringStruct(u'InternalName', u'dupeGuru'), StringStruct(u'LegalCopyright', u'© Hardcoded Software'), StringStruct(u'OriginalFilename', u'dupeguru-win{3}.exe'), StringStruct(u'ProductName', u'dupeGuru'), StringStruct(u'ProductVersion', u'{0}.{1}.{2}.0')]) ]), VarFileInfo([VarStruct(u'Translation', [1033, 1200])]) ] )